prev-cli 0.1.1
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/dist/cli.d.ts +2 -0
- package/dist/cli.js +502 -0
- package/dist/theme/App.d.ts +1 -0
- package/dist/theme/Layout.d.ts +2 -0
- package/dist/theme/Sidebar.d.ts +1 -0
- package/dist/theme/entry.d.ts +1 -0
- package/dist/ui/button.d.ts +10 -0
- package/dist/ui/card.d.ts +8 -0
- package/dist/ui/index.d.ts +3 -0
- package/dist/ui/utils.d.ts +2 -0
- package/dist/utils/cache.d.ts +7 -0
- package/dist/utils/port.d.ts +2 -0
- package/dist/vite/config.d.ts +7 -0
- package/dist/vite/pages.d.ts +13 -0
- package/dist/vite/plugins/entry-plugin.d.ts +2 -0
- package/dist/vite/plugins/pages-plugin.d.ts +2 -0
- package/dist/vite/start.d.ts +6 -0
- package/package.json +68 -0
- package/src/theme/App.tsx +33 -0
- package/src/theme/Layout.tsx +19 -0
- package/src/theme/Sidebar.tsx +75 -0
- package/src/theme/entry.tsx +10 -0
- package/src/theme/styles.css +274 -0
- package/src/ui/button.tsx +48 -0
- package/src/ui/card.tsx +78 -0
- package/src/ui/index.ts +10 -0
- package/src/ui/utils.ts +6 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 lagz0ne
|
|
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/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { parseArgs } from "util";
|
|
5
|
+
|
|
6
|
+
// src/vite/start.ts
|
|
7
|
+
import { createServer as createServer2, build, preview } from "vite";
|
|
8
|
+
|
|
9
|
+
// src/vite/config.ts
|
|
10
|
+
import react from "@vitejs/plugin-react-swc";
|
|
11
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
12
|
+
import mdx from "@mdx-js/rollup";
|
|
13
|
+
import remarkGfm from "remark-gfm";
|
|
14
|
+
import rehypeHighlight from "rehype-highlight";
|
|
15
|
+
import rehypeMermaid from "rehype-mermaid";
|
|
16
|
+
import path4 from "path";
|
|
17
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
18
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
19
|
+
|
|
20
|
+
// src/utils/cache.ts
|
|
21
|
+
import { createHash } from "crypto";
|
|
22
|
+
import { readdir, rm, stat, mkdir } from "fs/promises";
|
|
23
|
+
import { exec } from "child_process";
|
|
24
|
+
import { promisify } from "util";
|
|
25
|
+
import path from "path";
|
|
26
|
+
import os from "os";
|
|
27
|
+
var execAsync = promisify(exec);
|
|
28
|
+
var DEFAULT_CACHE_ROOT = path.join(os.homedir(), ".cache/prev");
|
|
29
|
+
async function getCacheDir(rootDir, branch) {
|
|
30
|
+
const resolvedBranch = branch ?? await getCurrentBranch(rootDir);
|
|
31
|
+
const hash = createHash("sha1").update(`${rootDir}:${resolvedBranch}`).digest("hex").slice(0, 12);
|
|
32
|
+
return path.join(DEFAULT_CACHE_ROOT, hash);
|
|
33
|
+
}
|
|
34
|
+
async function getCurrentBranch(rootDir) {
|
|
35
|
+
try {
|
|
36
|
+
const { stdout } = await execAsync("git branch --show-current", { cwd: rootDir });
|
|
37
|
+
return stdout.trim() || "detached";
|
|
38
|
+
} catch {
|
|
39
|
+
return "no-git";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function cleanCache(options) {
|
|
43
|
+
const cacheRoot = options.cacheRoot ?? DEFAULT_CACHE_ROOT;
|
|
44
|
+
const maxAge = options.maxAgeDays * 24 * 60 * 60 * 1000;
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
let dirs;
|
|
47
|
+
try {
|
|
48
|
+
dirs = await readdir(cacheRoot);
|
|
49
|
+
} catch {
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
let removed = 0;
|
|
53
|
+
for (const dir of dirs) {
|
|
54
|
+
const fullPath = path.join(cacheRoot, dir);
|
|
55
|
+
try {
|
|
56
|
+
const info = await stat(fullPath);
|
|
57
|
+
if (info.isDirectory() && now - info.mtimeMs > maxAge) {
|
|
58
|
+
await rm(fullPath, { recursive: true });
|
|
59
|
+
removed++;
|
|
60
|
+
}
|
|
61
|
+
} catch {}
|
|
62
|
+
}
|
|
63
|
+
return removed;
|
|
64
|
+
}
|
|
65
|
+
async function ensureCacheDir(rootDir) {
|
|
66
|
+
const cacheDir = await getCacheDir(rootDir);
|
|
67
|
+
await mkdir(cacheDir, { recursive: true });
|
|
68
|
+
return cacheDir;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/vite/pages.ts
|
|
72
|
+
import fg from "fast-glob";
|
|
73
|
+
import { readFile } from "fs/promises";
|
|
74
|
+
import path2 from "path";
|
|
75
|
+
function fileToRoute(file) {
|
|
76
|
+
const withoutExt = file.replace(/\.mdx?$/, "");
|
|
77
|
+
if (withoutExt === "index") {
|
|
78
|
+
return "/";
|
|
79
|
+
}
|
|
80
|
+
if (withoutExt.endsWith("/index")) {
|
|
81
|
+
return "/" + withoutExt.slice(0, -6);
|
|
82
|
+
}
|
|
83
|
+
return "/" + withoutExt;
|
|
84
|
+
}
|
|
85
|
+
async function scanPages(rootDir) {
|
|
86
|
+
const files = await fg.glob("**/*.mdx", {
|
|
87
|
+
cwd: rootDir,
|
|
88
|
+
ignore: ["node_modules/**", "dist/**", ".cache/**"]
|
|
89
|
+
});
|
|
90
|
+
const pages = [];
|
|
91
|
+
for (const file of files) {
|
|
92
|
+
const fullPath = path2.join(rootDir, file);
|
|
93
|
+
const content = await readFile(fullPath, "utf-8");
|
|
94
|
+
const title = extractTitle(content, file);
|
|
95
|
+
pages.push({
|
|
96
|
+
route: fileToRoute(file),
|
|
97
|
+
title,
|
|
98
|
+
file
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return pages.sort((a, b) => a.route.localeCompare(b.route));
|
|
102
|
+
}
|
|
103
|
+
function extractTitle(content, file) {
|
|
104
|
+
const match = content.match(/^#\s+(.+)$/m);
|
|
105
|
+
if (match) {
|
|
106
|
+
return match[1].trim();
|
|
107
|
+
}
|
|
108
|
+
const basename = path2.basename(file, path2.extname(file));
|
|
109
|
+
if (basename === "index") {
|
|
110
|
+
const dirname = path2.dirname(file);
|
|
111
|
+
return dirname === "." ? "Home" : capitalize(path2.basename(dirname));
|
|
112
|
+
}
|
|
113
|
+
return capitalize(basename);
|
|
114
|
+
}
|
|
115
|
+
function capitalize(str) {
|
|
116
|
+
return str.charAt(0).toUpperCase() + str.slice(1).replace(/-/g, " ");
|
|
117
|
+
}
|
|
118
|
+
function buildSidebarTree(pages) {
|
|
119
|
+
const tree = [];
|
|
120
|
+
const map = new Map;
|
|
121
|
+
for (const page of pages) {
|
|
122
|
+
const segments = page.route.split("/").filter(Boolean);
|
|
123
|
+
if (segments.length === 0) {
|
|
124
|
+
tree.push({ title: page.title, route: page.route });
|
|
125
|
+
} else if (segments.length === 1) {
|
|
126
|
+
const item = { title: page.title, route: page.route };
|
|
127
|
+
map.set(segments[0], item);
|
|
128
|
+
tree.push(item);
|
|
129
|
+
} else {
|
|
130
|
+
const parentKey = segments[0];
|
|
131
|
+
let parent = map.get(parentKey);
|
|
132
|
+
if (!parent) {
|
|
133
|
+
parent = { title: capitalize(parentKey), children: [] };
|
|
134
|
+
map.set(parentKey, parent);
|
|
135
|
+
tree.push(parent);
|
|
136
|
+
}
|
|
137
|
+
if (!parent.children) {
|
|
138
|
+
parent.children = [];
|
|
139
|
+
}
|
|
140
|
+
parent.children.push({ title: page.title, route: page.route });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return tree;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/vite/plugins/pages-plugin.ts
|
|
147
|
+
var VIRTUAL_MODULE_ID = "virtual:prev-pages";
|
|
148
|
+
var RESOLVED_VIRTUAL_MODULE_ID = "\x00" + VIRTUAL_MODULE_ID;
|
|
149
|
+
function pagesPlugin(rootDir) {
|
|
150
|
+
return {
|
|
151
|
+
name: "prev-pages",
|
|
152
|
+
resolveId(id) {
|
|
153
|
+
if (id === VIRTUAL_MODULE_ID) {
|
|
154
|
+
return RESOLVED_VIRTUAL_MODULE_ID;
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
async load(id) {
|
|
158
|
+
if (id === RESOLVED_VIRTUAL_MODULE_ID) {
|
|
159
|
+
const pages = await scanPages(rootDir);
|
|
160
|
+
const sidebar = buildSidebarTree(pages);
|
|
161
|
+
return `
|
|
162
|
+
export const pages = ${JSON.stringify(pages)};
|
|
163
|
+
export const sidebar = ${JSON.stringify(sidebar)};
|
|
164
|
+
`;
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
handleHotUpdate({ file, server }) {
|
|
168
|
+
if (file.endsWith(".mdx")) {
|
|
169
|
+
const mod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_MODULE_ID);
|
|
170
|
+
if (mod) {
|
|
171
|
+
server.moduleGraph.invalidateModule(mod);
|
|
172
|
+
return [mod];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/vite/plugins/entry-plugin.ts
|
|
180
|
+
import path3 from "path";
|
|
181
|
+
import { fileURLToPath } from "url";
|
|
182
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
183
|
+
function findCliRoot() {
|
|
184
|
+
let dir = path3.dirname(fileURLToPath(import.meta.url));
|
|
185
|
+
for (let i = 0;i < 10; i++) {
|
|
186
|
+
const pkgPath = path3.join(dir, "package.json");
|
|
187
|
+
if (existsSync(pkgPath)) {
|
|
188
|
+
try {
|
|
189
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
190
|
+
if (pkg.name === "prev-cli") {
|
|
191
|
+
return dir;
|
|
192
|
+
}
|
|
193
|
+
} catch {}
|
|
194
|
+
}
|
|
195
|
+
const parent = path3.dirname(dir);
|
|
196
|
+
if (parent === dir)
|
|
197
|
+
break;
|
|
198
|
+
dir = parent;
|
|
199
|
+
}
|
|
200
|
+
return path3.dirname(path3.dirname(fileURLToPath(import.meta.url)));
|
|
201
|
+
}
|
|
202
|
+
var cliRoot = findCliRoot();
|
|
203
|
+
var srcRoot = path3.join(cliRoot, "src");
|
|
204
|
+
function getHtml(entryPath, forBuild = false) {
|
|
205
|
+
const scriptSrc = forBuild ? entryPath : `/@fs${entryPath}`;
|
|
206
|
+
return `<!DOCTYPE html>
|
|
207
|
+
<html lang="en">
|
|
208
|
+
<head>
|
|
209
|
+
<meta charset="UTF-8" />
|
|
210
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
211
|
+
<title>Documentation</title>
|
|
212
|
+
<!-- Preconnect to Google Fonts for faster loading -->
|
|
213
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
214
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
215
|
+
<!-- Preload critical fonts -->
|
|
216
|
+
<link rel="preload" href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700&family=IBM+Plex+Mono:wght@400;500&display=swap" as="style" />
|
|
217
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700&family=IBM+Plex+Mono:wght@400;500&display=swap" />
|
|
218
|
+
</head>
|
|
219
|
+
<body>
|
|
220
|
+
<div id="root"></div>
|
|
221
|
+
<script type="module" src="${scriptSrc}"></script>
|
|
222
|
+
</body>
|
|
223
|
+
</html>`;
|
|
224
|
+
}
|
|
225
|
+
function entryPlugin(rootDir) {
|
|
226
|
+
const entryPath = path3.join(srcRoot, "theme/entry.tsx");
|
|
227
|
+
let tempHtmlPath = null;
|
|
228
|
+
return {
|
|
229
|
+
name: "prev-entry",
|
|
230
|
+
config(config, { command }) {
|
|
231
|
+
if (command === "build" && rootDir) {
|
|
232
|
+
tempHtmlPath = path3.join(rootDir, "index.html");
|
|
233
|
+
writeFileSync(tempHtmlPath, getHtml(entryPath, true));
|
|
234
|
+
return {
|
|
235
|
+
build: {
|
|
236
|
+
rollupOptions: {
|
|
237
|
+
input: tempHtmlPath
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
buildEnd() {
|
|
244
|
+
if (tempHtmlPath && existsSync(tempHtmlPath)) {
|
|
245
|
+
unlinkSync(tempHtmlPath);
|
|
246
|
+
tempHtmlPath = null;
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
configureServer(server) {
|
|
250
|
+
const html = getHtml(entryPath, false);
|
|
251
|
+
server.middlewares.use(async (req, res, next) => {
|
|
252
|
+
const url = req.url || "/";
|
|
253
|
+
if (url === "/" || !url.includes(".") && !url.startsWith("/@")) {
|
|
254
|
+
try {
|
|
255
|
+
const transformed = await server.transformIndexHtml(url, html);
|
|
256
|
+
res.setHeader("Content-Type", "text/html");
|
|
257
|
+
res.statusCode = 200;
|
|
258
|
+
res.end(transformed);
|
|
259
|
+
return;
|
|
260
|
+
} catch (e) {
|
|
261
|
+
console.error("Entry plugin error:", e);
|
|
262
|
+
next();
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
next();
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/vite/config.ts
|
|
273
|
+
function findCliRoot2() {
|
|
274
|
+
let dir = path4.dirname(fileURLToPath2(import.meta.url));
|
|
275
|
+
for (let i = 0;i < 10; i++) {
|
|
276
|
+
const pkgPath = path4.join(dir, "package.json");
|
|
277
|
+
if (existsSync2(pkgPath)) {
|
|
278
|
+
try {
|
|
279
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
280
|
+
if (pkg.name === "prev-cli") {
|
|
281
|
+
return dir;
|
|
282
|
+
}
|
|
283
|
+
} catch {}
|
|
284
|
+
}
|
|
285
|
+
const parent = path4.dirname(dir);
|
|
286
|
+
if (parent === dir)
|
|
287
|
+
break;
|
|
288
|
+
dir = parent;
|
|
289
|
+
}
|
|
290
|
+
return path4.dirname(path4.dirname(fileURLToPath2(import.meta.url)));
|
|
291
|
+
}
|
|
292
|
+
var cliRoot2 = findCliRoot2();
|
|
293
|
+
var cliNodeModules = path4.join(cliRoot2, "node_modules");
|
|
294
|
+
var srcRoot2 = path4.join(cliRoot2, "src");
|
|
295
|
+
async function createViteConfig(options) {
|
|
296
|
+
const { rootDir, mode, port } = options;
|
|
297
|
+
const cacheDir = await ensureCacheDir(rootDir);
|
|
298
|
+
return {
|
|
299
|
+
root: rootDir,
|
|
300
|
+
mode,
|
|
301
|
+
cacheDir,
|
|
302
|
+
plugins: [
|
|
303
|
+
mdx({
|
|
304
|
+
remarkPlugins: [remarkGfm],
|
|
305
|
+
rehypePlugins: [
|
|
306
|
+
rehypeHighlight,
|
|
307
|
+
[rehypeMermaid, { strategy: "img-svg" }]
|
|
308
|
+
]
|
|
309
|
+
}),
|
|
310
|
+
react(),
|
|
311
|
+
tailwindcss(),
|
|
312
|
+
pagesPlugin(rootDir),
|
|
313
|
+
entryPlugin(rootDir)
|
|
314
|
+
],
|
|
315
|
+
resolve: {
|
|
316
|
+
alias: {
|
|
317
|
+
"@prev/ui": path4.join(srcRoot2, "ui"),
|
|
318
|
+
"@prev/theme": path4.join(srcRoot2, "theme"),
|
|
319
|
+
react: path4.join(cliNodeModules, "react"),
|
|
320
|
+
"react-dom": path4.join(cliNodeModules, "react-dom"),
|
|
321
|
+
"react-router-dom": path4.join(cliNodeModules, "react-router-dom")
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
optimizeDeps: {
|
|
325
|
+
entries: [],
|
|
326
|
+
include: [
|
|
327
|
+
"react",
|
|
328
|
+
"react-dom",
|
|
329
|
+
"react-dom/client",
|
|
330
|
+
"react-router-dom",
|
|
331
|
+
"react/jsx-runtime",
|
|
332
|
+
"react/jsx-dev-runtime"
|
|
333
|
+
],
|
|
334
|
+
exclude: [
|
|
335
|
+
"clsx",
|
|
336
|
+
"class-variance-authority",
|
|
337
|
+
"tailwind-merge"
|
|
338
|
+
]
|
|
339
|
+
},
|
|
340
|
+
ssr: {
|
|
341
|
+
noExternal: true
|
|
342
|
+
},
|
|
343
|
+
server: {
|
|
344
|
+
port,
|
|
345
|
+
strictPort: false,
|
|
346
|
+
fs: {
|
|
347
|
+
allow: [rootDir, cliRoot2]
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
preview: {
|
|
351
|
+
port,
|
|
352
|
+
strictPort: false
|
|
353
|
+
},
|
|
354
|
+
build: {
|
|
355
|
+
outDir: path4.join(rootDir, "dist")
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/utils/port.ts
|
|
361
|
+
import { createServer } from "net";
|
|
362
|
+
async function getRandomPort(minPort = 3000, maxPort = 9000) {
|
|
363
|
+
const port = Math.floor(Math.random() * (maxPort - minPort + 1)) + minPort;
|
|
364
|
+
if (await isPortAvailable(port)) {
|
|
365
|
+
return port;
|
|
366
|
+
}
|
|
367
|
+
return findAvailablePort(minPort, maxPort);
|
|
368
|
+
}
|
|
369
|
+
function isPortAvailable(port) {
|
|
370
|
+
return new Promise((resolve) => {
|
|
371
|
+
const server = createServer();
|
|
372
|
+
server.once("error", () => {
|
|
373
|
+
resolve(false);
|
|
374
|
+
});
|
|
375
|
+
server.once("listening", () => {
|
|
376
|
+
server.close();
|
|
377
|
+
resolve(true);
|
|
378
|
+
});
|
|
379
|
+
server.listen(port, "127.0.0.1");
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
async function findAvailablePort(minPort, maxPort) {
|
|
383
|
+
for (let port = minPort;port <= maxPort; port++) {
|
|
384
|
+
if (await isPortAvailable(port)) {
|
|
385
|
+
return port;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
throw new Error(`No available port found between ${minPort} and ${maxPort}`);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/vite/start.ts
|
|
392
|
+
async function startDev(rootDir, options = {}) {
|
|
393
|
+
const port = options.port ?? await getRandomPort();
|
|
394
|
+
const config = await createViteConfig({
|
|
395
|
+
rootDir,
|
|
396
|
+
mode: "development",
|
|
397
|
+
port
|
|
398
|
+
});
|
|
399
|
+
const server = await createServer2(config);
|
|
400
|
+
await server.listen();
|
|
401
|
+
console.log();
|
|
402
|
+
console.log(` prev dev server running at:`);
|
|
403
|
+
server.printUrls();
|
|
404
|
+
console.log();
|
|
405
|
+
return server;
|
|
406
|
+
}
|
|
407
|
+
async function buildSite(rootDir) {
|
|
408
|
+
const config = await createViteConfig({
|
|
409
|
+
rootDir,
|
|
410
|
+
mode: "production"
|
|
411
|
+
});
|
|
412
|
+
await build(config);
|
|
413
|
+
console.log();
|
|
414
|
+
console.log(` Build complete. Output in ./dist`);
|
|
415
|
+
console.log();
|
|
416
|
+
}
|
|
417
|
+
async function previewSite(rootDir, options = {}) {
|
|
418
|
+
const port = options.port ?? await getRandomPort();
|
|
419
|
+
const config = await createViteConfig({
|
|
420
|
+
rootDir,
|
|
421
|
+
mode: "production",
|
|
422
|
+
port
|
|
423
|
+
});
|
|
424
|
+
const server = await preview(config);
|
|
425
|
+
console.log();
|
|
426
|
+
console.log(` Preview server running at:`);
|
|
427
|
+
server.printUrls();
|
|
428
|
+
console.log();
|
|
429
|
+
return server;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// src/cli.ts
|
|
433
|
+
var { values, positionals } = parseArgs({
|
|
434
|
+
args: process.argv.slice(2),
|
|
435
|
+
options: {
|
|
436
|
+
port: { type: "string", short: "p" },
|
|
437
|
+
days: { type: "string", short: "d" },
|
|
438
|
+
help: { type: "boolean", short: "h" }
|
|
439
|
+
},
|
|
440
|
+
allowPositionals: true
|
|
441
|
+
});
|
|
442
|
+
var command = positionals[0] || "dev";
|
|
443
|
+
var rootDir = process.cwd();
|
|
444
|
+
function printHelp() {
|
|
445
|
+
console.log(`
|
|
446
|
+
prev - Zero-config documentation site generator
|
|
447
|
+
|
|
448
|
+
Usage:
|
|
449
|
+
prev [command] [options]
|
|
450
|
+
|
|
451
|
+
Commands:
|
|
452
|
+
dev Start development server (default)
|
|
453
|
+
build Build for production
|
|
454
|
+
preview Preview production build
|
|
455
|
+
clean Remove old cache directories
|
|
456
|
+
|
|
457
|
+
Options:
|
|
458
|
+
-p, --port <port> Specify port (dev/preview)
|
|
459
|
+
-d, --days <days> Cache age threshold for clean (default: 30)
|
|
460
|
+
-h, --help Show this help message
|
|
461
|
+
|
|
462
|
+
Examples:
|
|
463
|
+
prev Start dev server on random port
|
|
464
|
+
prev dev -p 3000 Start dev server on port 3000
|
|
465
|
+
prev build Build static site to ./dist
|
|
466
|
+
prev clean Remove caches older than 30 days
|
|
467
|
+
prev clean -d 7 Remove caches older than 7 days
|
|
468
|
+
`);
|
|
469
|
+
}
|
|
470
|
+
async function main() {
|
|
471
|
+
if (values.help) {
|
|
472
|
+
printHelp();
|
|
473
|
+
process.exit(0);
|
|
474
|
+
}
|
|
475
|
+
const port = values.port ? parseInt(values.port, 10) : undefined;
|
|
476
|
+
const days = values.days ? parseInt(values.days, 10) : 30;
|
|
477
|
+
try {
|
|
478
|
+
switch (command) {
|
|
479
|
+
case "dev":
|
|
480
|
+
await startDev(rootDir, { port });
|
|
481
|
+
break;
|
|
482
|
+
case "build":
|
|
483
|
+
await buildSite(rootDir);
|
|
484
|
+
break;
|
|
485
|
+
case "preview":
|
|
486
|
+
await previewSite(rootDir, { port });
|
|
487
|
+
break;
|
|
488
|
+
case "clean":
|
|
489
|
+
const removed = await cleanCache({ maxAgeDays: days });
|
|
490
|
+
console.log(`Removed ${removed} cache(s) older than ${days} days`);
|
|
491
|
+
break;
|
|
492
|
+
default:
|
|
493
|
+
console.error(`Unknown command: ${command}`);
|
|
494
|
+
printHelp();
|
|
495
|
+
process.exit(1);
|
|
496
|
+
}
|
|
497
|
+
} catch (error) {
|
|
498
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
499
|
+
process.exit(1);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
main();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function App(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function Sidebar(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './styles.css';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type VariantProps } from 'class-variance-authority';
|
|
3
|
+
declare const buttonVariants: (props?: ({
|
|
4
|
+
variant?: "default" | "link" | "destructive" | "outline" | "secondary" | "ghost" | null | undefined;
|
|
5
|
+
size?: "default" | "sm" | "lg" | "icon" | null | undefined;
|
|
6
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
7
|
+
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
|
8
|
+
}
|
|
9
|
+
declare const Button: React.ForwardRefExoticComponent<ButtonProps & React.RefAttributes<HTMLButtonElement>>;
|
|
10
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
declare const Card: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
|
|
3
|
+
declare const CardHeader: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
|
|
4
|
+
declare const CardTitle: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLHeadingElement> & React.RefAttributes<HTMLParagraphElement>>;
|
|
5
|
+
declare const CardDescription: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLParagraphElement> & React.RefAttributes<HTMLParagraphElement>>;
|
|
6
|
+
declare const CardContent: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
|
|
7
|
+
declare const CardFooter: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
|
|
8
|
+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function getCacheDir(rootDir: string, branch?: string): Promise<string>;
|
|
2
|
+
export declare function getCurrentBranch(rootDir: string): Promise<string>;
|
|
3
|
+
export declare function cleanCache(options: {
|
|
4
|
+
maxAgeDays: number;
|
|
5
|
+
cacheRoot?: string;
|
|
6
|
+
}): Promise<number>;
|
|
7
|
+
export declare function ensureCacheDir(rootDir: string): Promise<string>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface Page {
|
|
2
|
+
route: string;
|
|
3
|
+
title: string;
|
|
4
|
+
file: string;
|
|
5
|
+
}
|
|
6
|
+
export interface SidebarItem {
|
|
7
|
+
title: string;
|
|
8
|
+
route?: string;
|
|
9
|
+
children?: SidebarItem[];
|
|
10
|
+
}
|
|
11
|
+
export declare function fileToRoute(file: string): string;
|
|
12
|
+
export declare function scanPages(rootDir: string): Promise<Page[]>;
|
|
13
|
+
export declare function buildSidebarTree(pages: Page[]): SidebarItem[];
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface DevOptions {
|
|
2
|
+
port?: number;
|
|
3
|
+
}
|
|
4
|
+
export declare function startDev(rootDir: string, options?: DevOptions): Promise<import("vite").ViteDevServer>;
|
|
5
|
+
export declare function buildSite(rootDir: string): Promise<void>;
|
|
6
|
+
export declare function previewSite(rootDir: string, options?: DevOptions): Promise<import("vite").PreviewServer>;
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "prev-cli",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Transform MDX directories into beautiful documentation websites",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"prev": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"src/theme",
|
|
13
|
+
"src/ui"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"documentation",
|
|
17
|
+
"mdx",
|
|
18
|
+
"markdown",
|
|
19
|
+
"cli",
|
|
20
|
+
"static-site",
|
|
21
|
+
"vite",
|
|
22
|
+
"react"
|
|
23
|
+
],
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/lagz0ne/prev-cli.git"
|
|
27
|
+
},
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/lagz0ne/prev-cli/issues"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/lagz0ne/prev-cli#readme",
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "bun build src/cli.ts --outdir dist --target node --packages external && tsc --emitDeclarationOnly",
|
|
37
|
+
"dev": "tsc --watch",
|
|
38
|
+
"test": "bun test src",
|
|
39
|
+
"test:integration": "bun run build && bun test test/integration.test.ts",
|
|
40
|
+
"test:all": "bun run test && bun run test:integration"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@mdx-js/rollup": "^3.0.0",
|
|
44
|
+
"@tailwindcss/vite": "^4.0.0",
|
|
45
|
+
"@vitejs/plugin-react-swc": "^4.2.2",
|
|
46
|
+
"class-variance-authority": "^0.7.0",
|
|
47
|
+
"clsx": "^2.1.0",
|
|
48
|
+
"fast-glob": "^3.3.0",
|
|
49
|
+
"lucide-react": "^0.460.0",
|
|
50
|
+
"playwright": "^1.57.0",
|
|
51
|
+
"react": "^19.0.0",
|
|
52
|
+
"react-dom": "^19.0.0",
|
|
53
|
+
"react-router-dom": "^7.0.0",
|
|
54
|
+
"rehype-highlight": "^7.0.0",
|
|
55
|
+
"rehype-mermaid": "^3.0.0",
|
|
56
|
+
"remark-gfm": "^4.0.0",
|
|
57
|
+
"rolldown-vite": "^7.3.0",
|
|
58
|
+
"tailwind-merge": "^2.5.0",
|
|
59
|
+
"tailwindcss": "^4.0.0"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@types/node": "^22.0.0",
|
|
63
|
+
"@types/react": "^19.0.0",
|
|
64
|
+
"@types/react-dom": "^19.0.0",
|
|
65
|
+
"bun-types": "^1.3.5",
|
|
66
|
+
"typescript": "^5.7.0"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { BrowserRouter, Routes, Route } from 'react-router-dom'
|
|
3
|
+
import { Layout } from './Layout'
|
|
4
|
+
import { pages } from 'virtual:prev-pages'
|
|
5
|
+
|
|
6
|
+
// Dynamic imports for MDX pages
|
|
7
|
+
const pageModules = import.meta.glob('/**/*.mdx', { eager: true })
|
|
8
|
+
|
|
9
|
+
function getPageComponent(file: string) {
|
|
10
|
+
const mod = pageModules[`/${file}`] as { default: React.ComponentType }
|
|
11
|
+
return mod?.default
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function App() {
|
|
15
|
+
return (
|
|
16
|
+
<BrowserRouter>
|
|
17
|
+
<Routes>
|
|
18
|
+
<Route element={<Layout />}>
|
|
19
|
+
{pages.map((page: { route: string; file: string }) => {
|
|
20
|
+
const Component = getPageComponent(page.file)
|
|
21
|
+
return Component ? (
|
|
22
|
+
<Route
|
|
23
|
+
key={page.route}
|
|
24
|
+
path={page.route}
|
|
25
|
+
element={<Component />}
|
|
26
|
+
/>
|
|
27
|
+
) : null
|
|
28
|
+
})}
|
|
29
|
+
</Route>
|
|
30
|
+
</Routes>
|
|
31
|
+
</BrowserRouter>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Outlet } from 'react-router-dom'
|
|
3
|
+
import { Sidebar } from './Sidebar'
|
|
4
|
+
import './styles.css'
|
|
5
|
+
|
|
6
|
+
export function Layout() {
|
|
7
|
+
return (
|
|
8
|
+
<div className="min-h-screen bg-background text-foreground">
|
|
9
|
+
<div className="flex">
|
|
10
|
+
<Sidebar />
|
|
11
|
+
<main className="flex-1 p-8 max-w-4xl animate-fade-in">
|
|
12
|
+
<article className="prose max-w-none">
|
|
13
|
+
<Outlet />
|
|
14
|
+
</article>
|
|
15
|
+
</main>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Link, useLocation } from 'react-router-dom'
|
|
3
|
+
import { sidebar } from 'virtual:prev-pages'
|
|
4
|
+
import { cn } from '../ui/utils'
|
|
5
|
+
|
|
6
|
+
interface SidebarItem {
|
|
7
|
+
title: string
|
|
8
|
+
route?: string
|
|
9
|
+
children?: SidebarItem[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Sidebar() {
|
|
13
|
+
const location = useLocation()
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<aside className="w-64 border-r border-sidebar-border bg-sidebar p-4 min-h-screen sticky top-0">
|
|
17
|
+
<nav>
|
|
18
|
+
<ul className="space-y-1">
|
|
19
|
+
{(sidebar as SidebarItem[]).map((item, i) => (
|
|
20
|
+
<SidebarItemComponent
|
|
21
|
+
key={i}
|
|
22
|
+
item={item}
|
|
23
|
+
currentPath={location.pathname}
|
|
24
|
+
/>
|
|
25
|
+
))}
|
|
26
|
+
</ul>
|
|
27
|
+
</nav>
|
|
28
|
+
</aside>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function SidebarItemComponent({
|
|
33
|
+
item,
|
|
34
|
+
currentPath
|
|
35
|
+
}: {
|
|
36
|
+
item: SidebarItem
|
|
37
|
+
currentPath: string
|
|
38
|
+
}) {
|
|
39
|
+
const isActive = item.route === currentPath
|
|
40
|
+
|
|
41
|
+
if (item.children) {
|
|
42
|
+
return (
|
|
43
|
+
<li className="mt-4 first:mt-0">
|
|
44
|
+
<span className="font-semibold text-xs text-sidebar-foreground/60 uppercase tracking-wider">
|
|
45
|
+
{item.title}
|
|
46
|
+
</span>
|
|
47
|
+
<ul className="ml-3 mt-2 space-y-1">
|
|
48
|
+
{item.children.map((child, i) => (
|
|
49
|
+
<SidebarItemComponent
|
|
50
|
+
key={i}
|
|
51
|
+
item={child}
|
|
52
|
+
currentPath={currentPath}
|
|
53
|
+
/>
|
|
54
|
+
))}
|
|
55
|
+
</ul>
|
|
56
|
+
</li>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<li>
|
|
62
|
+
<Link
|
|
63
|
+
to={item.route || '/'}
|
|
64
|
+
className={cn(
|
|
65
|
+
'block py-1.5 px-2 rounded-md text-sm transition-colors',
|
|
66
|
+
isActive
|
|
67
|
+
? 'bg-sidebar-accent text-sidebar-accent-foreground font-medium'
|
|
68
|
+
: 'text-sidebar-foreground/70 hover:text-sidebar-foreground hover:bg-sidebar-accent/50'
|
|
69
|
+
)}
|
|
70
|
+
>
|
|
71
|
+
{item.title}
|
|
72
|
+
</Link>
|
|
73
|
+
</li>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { createRoot } from 'react-dom/client'
|
|
3
|
+
import './styles.css'
|
|
4
|
+
import { App } from './App'
|
|
5
|
+
|
|
6
|
+
const container = document.getElementById('root')
|
|
7
|
+
if (container) {
|
|
8
|
+
const root = createRoot(container)
|
|
9
|
+
root.render(<App />)
|
|
10
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/* Fonts loaded via HTML preload for better performance */
|
|
2
|
+
@import "tailwindcss";
|
|
3
|
+
|
|
4
|
+
@custom-variant dark (&:is(.dark *));
|
|
5
|
+
|
|
6
|
+
/* Scan CLI source files for Tailwind classes */
|
|
7
|
+
@source "../theme";
|
|
8
|
+
@source "../ui";
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
Design OS - Refined Utility Design System
|
|
12
|
+
A professional, editorial-inspired aesthetic with warm neutrals
|
|
13
|
+
Using Tailwind's stone palette for warmth
|
|
14
|
+
*/
|
|
15
|
+
@theme {
|
|
16
|
+
/* Font families - DM Sans for clean, geometric type */
|
|
17
|
+
--font-display: "DM Sans", system-ui, sans-serif;
|
|
18
|
+
--font-body: "DM Sans", system-ui, sans-serif;
|
|
19
|
+
--font-sans: "DM Sans", system-ui, sans-serif;
|
|
20
|
+
--font-mono: "IBM Plex Mono", ui-monospace, monospace;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* Shadcn theme integration */
|
|
24
|
+
@theme inline {
|
|
25
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
26
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
27
|
+
--radius-lg: var(--radius);
|
|
28
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
29
|
+
--color-background: var(--background);
|
|
30
|
+
--color-foreground: var(--foreground);
|
|
31
|
+
--color-card: var(--card);
|
|
32
|
+
--color-card-foreground: var(--card-foreground);
|
|
33
|
+
--color-popover: var(--popover);
|
|
34
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
35
|
+
--color-primary: var(--primary);
|
|
36
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
37
|
+
--color-secondary: var(--secondary);
|
|
38
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
39
|
+
--color-muted: var(--muted);
|
|
40
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
41
|
+
--color-accent: var(--accent);
|
|
42
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
43
|
+
--color-destructive: var(--destructive);
|
|
44
|
+
--color-border: var(--border);
|
|
45
|
+
--color-input: var(--input);
|
|
46
|
+
--color-ring: var(--ring);
|
|
47
|
+
--color-chart-1: var(--chart-1);
|
|
48
|
+
--color-chart-2: var(--chart-2);
|
|
49
|
+
--color-chart-3: var(--chart-3);
|
|
50
|
+
--color-chart-4: var(--chart-4);
|
|
51
|
+
--color-chart-5: var(--chart-5);
|
|
52
|
+
--color-sidebar: var(--sidebar);
|
|
53
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
54
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
55
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
56
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
57
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
58
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
59
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/*
|
|
63
|
+
Light theme - Warm Stone palette
|
|
64
|
+
Professional, editorial aesthetic with warmth
|
|
65
|
+
*/
|
|
66
|
+
:root {
|
|
67
|
+
--radius: 0.5rem;
|
|
68
|
+
/* Backgrounds & Foregrounds - Stone palette for warmth */
|
|
69
|
+
--background: oklch(0.985 0.001 106.424); /* stone-50 #FAFAF9 */
|
|
70
|
+
--foreground: oklch(0.216 0.006 56.043); /* stone-900 #1C1917 */
|
|
71
|
+
--card: oklch(1 0 0); /* white */
|
|
72
|
+
--card-foreground: oklch(0.216 0.006 56.043); /* stone-900 */
|
|
73
|
+
--popover: oklch(1 0 0); /* white */
|
|
74
|
+
--popover-foreground: oklch(0.216 0.006 56.043); /* stone-900 */
|
|
75
|
+
/* Primary - Stone dark */
|
|
76
|
+
--primary: oklch(0.216 0.006 56.043); /* stone-900 */
|
|
77
|
+
--primary-foreground: oklch(0.985 0.001 106.424); /* stone-50 */
|
|
78
|
+
/* Secondary - Stone light */
|
|
79
|
+
--secondary: oklch(0.970 0.001 106.424); /* stone-100 */
|
|
80
|
+
--secondary-foreground: oklch(0.216 0.006 56.043); /* stone-900 */
|
|
81
|
+
/* Muted - Stone for subdued text */
|
|
82
|
+
--muted: oklch(0.970 0.001 106.424); /* stone-100 */
|
|
83
|
+
--muted-foreground: oklch(0.444 0.011 73.639); /* stone-600 #57534E */
|
|
84
|
+
/* Accent - Lime green for subtle pops */
|
|
85
|
+
--accent: oklch(0.970 0.001 106.424); /* stone-100 */
|
|
86
|
+
--accent-foreground: oklch(0.216 0.006 56.043); /* stone-900 */
|
|
87
|
+
/* Destructive - Rose */
|
|
88
|
+
--destructive: oklch(0.586 0.253 17.585); /* rose-600 */
|
|
89
|
+
/* Borders & Inputs - Stone very light */
|
|
90
|
+
--border: oklch(0.923 0.003 48.717); /* stone-200 #E7E5E4 */
|
|
91
|
+
--input: oklch(0.923 0.003 48.717); /* stone-200 */
|
|
92
|
+
--ring: oklch(0.553 0.013 58.071); /* stone-500 */
|
|
93
|
+
/* Chart colors */
|
|
94
|
+
--chart-1: oklch(0.532 0.157 131.589); /* lime-600 #65A30D */
|
|
95
|
+
--chart-2: oklch(0.6 0.118 184.704); /* teal-600 */
|
|
96
|
+
--chart-3: oklch(0.398 0.07 227.392); /* cyan-800 */
|
|
97
|
+
--chart-4: oklch(0.828 0.189 84.429); /* yellow-400 */
|
|
98
|
+
--chart-5: oklch(0.769 0.188 70.08); /* amber-400 */
|
|
99
|
+
/* Sidebar - Stone */
|
|
100
|
+
--sidebar: oklch(0.985 0.001 106.424); /* stone-50 */
|
|
101
|
+
--sidebar-foreground: oklch(0.216 0.006 56.043); /* stone-900 */
|
|
102
|
+
--sidebar-primary: oklch(0.216 0.006 56.043); /* stone-900 */
|
|
103
|
+
--sidebar-primary-foreground: oklch(0.985 0.001 106.424); /* stone-50 */
|
|
104
|
+
--sidebar-accent: oklch(0.970 0.001 106.424); /* stone-100 */
|
|
105
|
+
--sidebar-accent-foreground: oklch(0.216 0.006 56.043); /* stone-900 */
|
|
106
|
+
--sidebar-border: oklch(0.923 0.003 48.717); /* stone-200 */
|
|
107
|
+
--sidebar-ring: oklch(0.553 0.013 58.071); /* stone-500 */
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/*
|
|
111
|
+
Dark theme - Warm Stone dark palette
|
|
112
|
+
*/
|
|
113
|
+
.dark {
|
|
114
|
+
/* Backgrounds & Foregrounds - Stone dark */
|
|
115
|
+
--background: oklch(0.216 0.006 56.043); /* stone-900 #1C1917 */
|
|
116
|
+
--foreground: oklch(0.985 0.001 106.424); /* stone-50 */
|
|
117
|
+
--card: oklch(0.268 0.007 34.298); /* stone-800 #292524 */
|
|
118
|
+
--card-foreground: oklch(0.985 0.001 106.424); /* stone-50 */
|
|
119
|
+
--popover: oklch(0.268 0.007 34.298); /* stone-800 */
|
|
120
|
+
--popover-foreground: oklch(0.985 0.001 106.424); /* stone-50 */
|
|
121
|
+
/* Primary - Stone light */
|
|
122
|
+
--primary: oklch(0.923 0.003 48.717); /* stone-200 */
|
|
123
|
+
--primary-foreground: oklch(0.216 0.006 56.043); /* stone-900 */
|
|
124
|
+
/* Secondary - Stone dark */
|
|
125
|
+
--secondary: oklch(0.318 0.008 43.185); /* stone-700 */
|
|
126
|
+
--secondary-foreground: oklch(0.985 0.001 106.424); /* stone-50 */
|
|
127
|
+
/* Muted - Stone dark */
|
|
128
|
+
--muted: oklch(0.318 0.008 43.185); /* stone-700 */
|
|
129
|
+
--muted-foreground: oklch(0.709 0.01 56.259); /* stone-400 #A8A29E */
|
|
130
|
+
/* Accent - Stone dark */
|
|
131
|
+
--accent: oklch(0.318 0.008 43.185); /* stone-700 */
|
|
132
|
+
--accent-foreground: oklch(0.985 0.001 106.424); /* stone-50 */
|
|
133
|
+
/* Destructive - Rose lighter for dark mode */
|
|
134
|
+
--destructive: oklch(0.712 0.194 13.428); /* rose-400 */
|
|
135
|
+
/* Borders & Inputs - Stone dark */
|
|
136
|
+
--border: oklch(0.370 0.010 67.558); /* stone-600 */
|
|
137
|
+
--input: oklch(0.370 0.010 67.558); /* stone-600 */
|
|
138
|
+
--ring: oklch(0.553 0.013 58.071); /* stone-500 */
|
|
139
|
+
/* Chart colors - brighter for dark mode */
|
|
140
|
+
--chart-1: oklch(0.648 0.2 131.684); /* lime-500 */
|
|
141
|
+
--chart-2: oklch(0.696 0.17 162.48); /* emerald-400 */
|
|
142
|
+
--chart-3: oklch(0.769 0.188 70.08); /* amber-400 */
|
|
143
|
+
--chart-4: oklch(0.627 0.265 303.9); /* purple-500 */
|
|
144
|
+
--chart-5: oklch(0.645 0.246 16.439); /* rose-500 */
|
|
145
|
+
/* Sidebar - Stone dark */
|
|
146
|
+
--sidebar: oklch(0.268 0.007 34.298); /* stone-800 */
|
|
147
|
+
--sidebar-foreground: oklch(0.985 0.001 106.424); /* stone-50 */
|
|
148
|
+
--sidebar-primary: oklch(0.648 0.2 131.684); /* lime-500 */
|
|
149
|
+
--sidebar-primary-foreground: oklch(0.216 0.006 56.043); /* stone-900 */
|
|
150
|
+
--sidebar-accent: oklch(0.318 0.008 43.185); /* stone-700 */
|
|
151
|
+
--sidebar-accent-foreground: oklch(0.985 0.001 106.424); /* stone-50 */
|
|
152
|
+
--sidebar-border: oklch(0.370 0.010 67.558); /* stone-600 */
|
|
153
|
+
--sidebar-ring: oklch(0.553 0.013 58.071); /* stone-500 */
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* Base layer styles */
|
|
157
|
+
@layer base {
|
|
158
|
+
* {
|
|
159
|
+
@apply border-border outline-ring/50;
|
|
160
|
+
}
|
|
161
|
+
body {
|
|
162
|
+
@apply bg-background text-foreground font-sans antialiased;
|
|
163
|
+
}
|
|
164
|
+
h1, h2, h3, h4, h5, h6 {
|
|
165
|
+
font-family: var(--font-display);
|
|
166
|
+
@apply font-semibold tracking-tight;
|
|
167
|
+
}
|
|
168
|
+
code, pre, kbd {
|
|
169
|
+
font-family: var(--font-mono);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* Subtle page fade-in animation */
|
|
174
|
+
@layer utilities {
|
|
175
|
+
.animate-fade-in {
|
|
176
|
+
animation: fade-in 200ms ease-out;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
@keyframes fade-in {
|
|
180
|
+
from {
|
|
181
|
+
opacity: 0;
|
|
182
|
+
}
|
|
183
|
+
to {
|
|
184
|
+
opacity: 1;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.animate-collapse-down {
|
|
189
|
+
animation: collapse-down 200ms ease-out;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.animate-collapse-up {
|
|
193
|
+
animation: collapse-up 200ms ease-out;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@keyframes collapse-down {
|
|
197
|
+
from {
|
|
198
|
+
height: 0;
|
|
199
|
+
}
|
|
200
|
+
to {
|
|
201
|
+
height: var(--radix-collapsible-content-height);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@keyframes collapse-up {
|
|
206
|
+
from {
|
|
207
|
+
height: var(--radix-collapsible-content-height);
|
|
208
|
+
}
|
|
209
|
+
to {
|
|
210
|
+
height: 0;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* Prose styling with stone palette */
|
|
216
|
+
@layer base {
|
|
217
|
+
.prose {
|
|
218
|
+
--tw-prose-body: var(--foreground);
|
|
219
|
+
--tw-prose-headings: var(--foreground);
|
|
220
|
+
--tw-prose-lead: var(--muted-foreground);
|
|
221
|
+
--tw-prose-links: var(--primary);
|
|
222
|
+
--tw-prose-bold: var(--foreground);
|
|
223
|
+
--tw-prose-counters: var(--muted-foreground);
|
|
224
|
+
--tw-prose-bullets: var(--muted-foreground);
|
|
225
|
+
--tw-prose-hr: var(--border);
|
|
226
|
+
--tw-prose-quotes: var(--foreground);
|
|
227
|
+
--tw-prose-quote-borders: var(--border);
|
|
228
|
+
--tw-prose-captions: var(--muted-foreground);
|
|
229
|
+
--tw-prose-kbd: var(--foreground);
|
|
230
|
+
--tw-prose-kbd-shadows: var(--ring);
|
|
231
|
+
--tw-prose-code: var(--foreground);
|
|
232
|
+
--tw-prose-pre-code: var(--foreground);
|
|
233
|
+
--tw-prose-pre-bg: var(--muted);
|
|
234
|
+
--tw-prose-th-borders: var(--border);
|
|
235
|
+
--tw-prose-td-borders: var(--border);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.prose :where(code):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
|
239
|
+
@apply bg-muted px-1.5 py-0.5 rounded-sm text-sm font-mono;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.prose :where(code):not(:where([class~="not-prose"], [class~="not-prose"] *))::before,
|
|
243
|
+
.prose :where(code):not(:where([class~="not-prose"], [class~="not-prose"] *))::after {
|
|
244
|
+
content: none;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.prose :where(pre):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
|
248
|
+
@apply bg-muted border border-border rounded-lg;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.prose :where(a):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
|
252
|
+
@apply text-foreground font-medium underline underline-offset-4 decoration-border hover:decoration-foreground transition-colors;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.prose :where(h1, h2, h3, h4, h5, h6):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
|
256
|
+
@apply font-display;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.prose :where(blockquote):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
|
260
|
+
@apply border-l-4 border-border pl-4 italic text-muted-foreground;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.prose :where(table):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
|
264
|
+
@apply w-full border-collapse;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.prose :where(th):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
|
268
|
+
@apply border-b border-border px-4 py-2 text-left font-semibold;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.prose :where(td):not(:where([class~="not-prose"], [class~="not-prose"] *)) {
|
|
272
|
+
@apply border-b border-border px-4 py-2;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
3
|
+
import { cn } from './utils'
|
|
4
|
+
|
|
5
|
+
const buttonVariants = cva(
|
|
6
|
+
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
11
|
+
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
|
12
|
+
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
|
13
|
+
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
14
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
15
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
16
|
+
},
|
|
17
|
+
size: {
|
|
18
|
+
default: 'h-10 px-4 py-2',
|
|
19
|
+
sm: 'h-9 rounded-md px-3',
|
|
20
|
+
lg: 'h-11 rounded-md px-8',
|
|
21
|
+
icon: 'h-10 w-10',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
defaultVariants: {
|
|
25
|
+
variant: 'default',
|
|
26
|
+
size: 'default',
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
export interface ButtonProps
|
|
32
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
33
|
+
VariantProps<typeof buttonVariants> {}
|
|
34
|
+
|
|
35
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
36
|
+
({ className, variant, size, ...props }, ref) => {
|
|
37
|
+
return (
|
|
38
|
+
<button
|
|
39
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
40
|
+
ref={ref}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
Button.displayName = 'Button'
|
|
47
|
+
|
|
48
|
+
export { Button, buttonVariants }
|
package/src/ui/card.tsx
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { cn } from './utils'
|
|
3
|
+
|
|
4
|
+
const Card = React.forwardRef<
|
|
5
|
+
HTMLDivElement,
|
|
6
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
7
|
+
>(({ className, ...props }, ref) => (
|
|
8
|
+
<div
|
|
9
|
+
ref={ref}
|
|
10
|
+
className={cn(
|
|
11
|
+
'rounded-lg border bg-card text-card-foreground shadow-sm',
|
|
12
|
+
className
|
|
13
|
+
)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
))
|
|
17
|
+
Card.displayName = 'Card'
|
|
18
|
+
|
|
19
|
+
const CardHeader = React.forwardRef<
|
|
20
|
+
HTMLDivElement,
|
|
21
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
22
|
+
>(({ className, ...props }, ref) => (
|
|
23
|
+
<div
|
|
24
|
+
ref={ref}
|
|
25
|
+
className={cn('flex flex-col space-y-1.5 p-6', className)}
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
))
|
|
29
|
+
CardHeader.displayName = 'CardHeader'
|
|
30
|
+
|
|
31
|
+
const CardTitle = React.forwardRef<
|
|
32
|
+
HTMLParagraphElement,
|
|
33
|
+
React.HTMLAttributes<HTMLHeadingElement>
|
|
34
|
+
>(({ className, ...props }, ref) => (
|
|
35
|
+
<h3
|
|
36
|
+
ref={ref}
|
|
37
|
+
className={cn(
|
|
38
|
+
'text-2xl font-semibold leading-none tracking-tight',
|
|
39
|
+
className
|
|
40
|
+
)}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
))
|
|
44
|
+
CardTitle.displayName = 'CardTitle'
|
|
45
|
+
|
|
46
|
+
const CardDescription = React.forwardRef<
|
|
47
|
+
HTMLParagraphElement,
|
|
48
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
49
|
+
>(({ className, ...props }, ref) => (
|
|
50
|
+
<p
|
|
51
|
+
ref={ref}
|
|
52
|
+
className={cn('text-sm text-muted-foreground', className)}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
))
|
|
56
|
+
CardDescription.displayName = 'CardDescription'
|
|
57
|
+
|
|
58
|
+
const CardContent = React.forwardRef<
|
|
59
|
+
HTMLDivElement,
|
|
60
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
61
|
+
>(({ className, ...props }, ref) => (
|
|
62
|
+
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
|
63
|
+
))
|
|
64
|
+
CardContent.displayName = 'CardContent'
|
|
65
|
+
|
|
66
|
+
const CardFooter = React.forwardRef<
|
|
67
|
+
HTMLDivElement,
|
|
68
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
69
|
+
>(({ className, ...props }, ref) => (
|
|
70
|
+
<div
|
|
71
|
+
ref={ref}
|
|
72
|
+
className={cn('flex items-center p-6 pt-0', className)}
|
|
73
|
+
{...props}
|
|
74
|
+
/>
|
|
75
|
+
))
|
|
76
|
+
CardFooter.displayName = 'CardFooter'
|
|
77
|
+
|
|
78
|
+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
package/src/ui/index.ts
ADDED