prev-cli 0.7.0 → 0.7.2
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/dist/cli.js +30 -12
- package/package.json +1 -1
- package/src/theme/MetadataBlock.tsx +146 -0
- package/src/theme/diagrams.tsx +15 -1
- package/src/theme/entry.tsx +27 -4
- package/src/theme/styles.css +108 -0
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { parseArgs } from "util";
|
|
5
|
+
import path5 from "path";
|
|
5
6
|
|
|
6
7
|
// src/vite/start.ts
|
|
7
8
|
import { createServer as createServer2, build, preview } from "vite";
|
|
@@ -71,6 +72,32 @@ async function ensureCacheDir(rootDir) {
|
|
|
71
72
|
import fg from "fast-glob";
|
|
72
73
|
import { readFile } from "fs/promises";
|
|
73
74
|
import path2 from "path";
|
|
75
|
+
function parseValue(value) {
|
|
76
|
+
const trimmed = value.trim();
|
|
77
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
78
|
+
const inner = trimmed.slice(1, -1);
|
|
79
|
+
if (inner.trim() === "")
|
|
80
|
+
return [];
|
|
81
|
+
return inner.split(",").map((item) => {
|
|
82
|
+
let v2 = item.trim();
|
|
83
|
+
if (v2.startsWith('"') && v2.endsWith('"') || v2.startsWith("'") && v2.endsWith("'")) {
|
|
84
|
+
v2 = v2.slice(1, -1);
|
|
85
|
+
}
|
|
86
|
+
return v2;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
let v = trimmed;
|
|
90
|
+
if (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'")) {
|
|
91
|
+
v = v.slice(1, -1);
|
|
92
|
+
}
|
|
93
|
+
if (v === "true")
|
|
94
|
+
return true;
|
|
95
|
+
if (v === "false")
|
|
96
|
+
return false;
|
|
97
|
+
if (!isNaN(Number(v)) && v !== "")
|
|
98
|
+
return Number(v);
|
|
99
|
+
return v;
|
|
100
|
+
}
|
|
74
101
|
function parseFrontmatter(content) {
|
|
75
102
|
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
|
76
103
|
const match = content.match(frontmatterRegex);
|
|
@@ -86,18 +113,9 @@ function parseFrontmatter(content) {
|
|
|
86
113
|
if (colonIndex === -1)
|
|
87
114
|
continue;
|
|
88
115
|
const key = line.slice(0, colonIndex).trim();
|
|
89
|
-
|
|
90
|
-
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
91
|
-
value = value.slice(1, -1);
|
|
92
|
-
}
|
|
93
|
-
if (value === "true")
|
|
94
|
-
value = true;
|
|
95
|
-
else if (value === "false")
|
|
96
|
-
value = false;
|
|
97
|
-
else if (!isNaN(Number(value)) && value !== "")
|
|
98
|
-
value = Number(value);
|
|
116
|
+
const rawValue = line.slice(colonIndex + 1);
|
|
99
117
|
if (key) {
|
|
100
|
-
frontmatter[key] =
|
|
118
|
+
frontmatter[key] = parseValue(rawValue);
|
|
101
119
|
}
|
|
102
120
|
}
|
|
103
121
|
return { frontmatter, content: restContent };
|
|
@@ -602,7 +620,7 @@ var { values, positionals } = parseArgs({
|
|
|
602
620
|
allowPositionals: true
|
|
603
621
|
});
|
|
604
622
|
var command = positionals[0] || "dev";
|
|
605
|
-
var rootDir = values.cwd || positionals[1] ||
|
|
623
|
+
var rootDir = path5.resolve(values.cwd || positionals[1] || ".");
|
|
606
624
|
function printHelp() {
|
|
607
625
|
console.log(`
|
|
608
626
|
prev - Zero-config documentation site generator
|
package/package.json
CHANGED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
type FrontmatterValue = string | number | boolean | string[] | unknown
|
|
4
|
+
|
|
5
|
+
interface MetadataBlockProps {
|
|
6
|
+
frontmatter: Record<string, FrontmatterValue>
|
|
7
|
+
title?: string
|
|
8
|
+
description?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Fields to skip (shown elsewhere or internal)
|
|
12
|
+
const SKIP_FIELDS = new Set(['title', 'description'])
|
|
13
|
+
|
|
14
|
+
// Fields with special icons
|
|
15
|
+
const FIELD_ICONS: Record<string, string> = {
|
|
16
|
+
date: '📅',
|
|
17
|
+
author: '👤',
|
|
18
|
+
tags: '🏷️',
|
|
19
|
+
status: '📌',
|
|
20
|
+
version: '📦',
|
|
21
|
+
repo: '🔗',
|
|
22
|
+
url: '🔗',
|
|
23
|
+
link: '🔗',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Detect the display type of a value
|
|
28
|
+
*/
|
|
29
|
+
function detectType(key: string, value: FrontmatterValue): 'date' | 'url' | 'boolean' | 'array' | 'number' | 'string' {
|
|
30
|
+
if (Array.isArray(value)) return 'array'
|
|
31
|
+
if (typeof value === 'boolean') return 'boolean'
|
|
32
|
+
if (typeof value === 'number') return 'number'
|
|
33
|
+
if (typeof value !== 'string') return 'string'
|
|
34
|
+
|
|
35
|
+
// Check for URL
|
|
36
|
+
if (value.startsWith('http://') || value.startsWith('https://')) return 'url'
|
|
37
|
+
|
|
38
|
+
// Check for date patterns
|
|
39
|
+
if (key === 'date' || key.includes('date') || key.includes('Date')) {
|
|
40
|
+
// ISO date or YYYY-MM-DD
|
|
41
|
+
if (/^\d{4}-\d{2}-\d{2}/.test(value)) return 'date'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return 'string'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Format a date string nicely
|
|
49
|
+
*/
|
|
50
|
+
function formatDate(value: string): string {
|
|
51
|
+
try {
|
|
52
|
+
const date = new Date(value)
|
|
53
|
+
if (isNaN(date.getTime())) return value
|
|
54
|
+
return date.toLocaleDateString('en-US', {
|
|
55
|
+
year: 'numeric',
|
|
56
|
+
month: 'short',
|
|
57
|
+
day: 'numeric'
|
|
58
|
+
})
|
|
59
|
+
} catch {
|
|
60
|
+
return value
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Render a single metadata value based on its type
|
|
66
|
+
*/
|
|
67
|
+
function renderValue(key: string, value: FrontmatterValue): React.ReactNode {
|
|
68
|
+
const type = detectType(key, value)
|
|
69
|
+
|
|
70
|
+
switch (type) {
|
|
71
|
+
case 'array':
|
|
72
|
+
return (
|
|
73
|
+
<span className="metadata-array">
|
|
74
|
+
{(value as string[]).map((item, i) => (
|
|
75
|
+
<span key={i} className="metadata-chip">{item}</span>
|
|
76
|
+
))}
|
|
77
|
+
</span>
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
case 'boolean':
|
|
81
|
+
return (
|
|
82
|
+
<span className={`metadata-boolean ${value ? 'is-true' : 'is-false'}`}>
|
|
83
|
+
{value ? '✓' : '✗'}
|
|
84
|
+
</span>
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
case 'url':
|
|
88
|
+
return (
|
|
89
|
+
<a href={value as string} className="metadata-link" target="_blank" rel="noopener noreferrer">
|
|
90
|
+
{(value as string).replace(/^https?:\/\//, '').split('/')[0]}
|
|
91
|
+
</a>
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
case 'date':
|
|
95
|
+
return <span className="metadata-date">{formatDate(value as string)}</span>
|
|
96
|
+
|
|
97
|
+
case 'number':
|
|
98
|
+
return <span className="metadata-number">{(value as number).toLocaleString()}</span>
|
|
99
|
+
|
|
100
|
+
default:
|
|
101
|
+
return <span className="metadata-string">{String(value)}</span>
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function MetadataBlock({ frontmatter, title, description }: MetadataBlockProps) {
|
|
106
|
+
// Filter out skipped fields and empty values
|
|
107
|
+
const fields = Object.entries(frontmatter).filter(
|
|
108
|
+
([key, value]) => !SKIP_FIELDS.has(key) && value !== undefined && value !== null && value !== ''
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
// Check for draft status
|
|
112
|
+
const isDraft = frontmatter.draft === true
|
|
113
|
+
|
|
114
|
+
// Only show title/description if they came from frontmatter (not extracted from H1)
|
|
115
|
+
const hasExplicitTitle = 'title' in frontmatter && frontmatter.title
|
|
116
|
+
const hasExplicitDescription = 'description' in frontmatter && frontmatter.description
|
|
117
|
+
|
|
118
|
+
// If no explicit metadata and no other fields, don't render the block
|
|
119
|
+
if (fields.length === 0 && !hasExplicitTitle && !hasExplicitDescription && !isDraft) {
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div className="metadata-block">
|
|
125
|
+
{hasExplicitTitle && <h1 className="metadata-title">{title}</h1>}
|
|
126
|
+
{hasExplicitDescription && <p className="metadata-description">{description}</p>}
|
|
127
|
+
|
|
128
|
+
{(fields.length > 0 || isDraft) && (
|
|
129
|
+
<div className="metadata-fields">
|
|
130
|
+
{isDraft && (
|
|
131
|
+
<span className="metadata-field metadata-draft">
|
|
132
|
+
<span className="metadata-chip is-draft">Draft</span>
|
|
133
|
+
</span>
|
|
134
|
+
)}
|
|
135
|
+
{fields.filter(([key]) => key !== 'draft').map(([key, value]) => (
|
|
136
|
+
<span key={key} className="metadata-field">
|
|
137
|
+
{FIELD_ICONS[key] && <span className="metadata-icon">{FIELD_ICONS[key]}</span>}
|
|
138
|
+
<span className="metadata-key">{key}:</span>
|
|
139
|
+
{renderValue(key, value)}
|
|
140
|
+
</span>
|
|
141
|
+
))}
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
)
|
|
146
|
+
}
|
package/src/theme/diagrams.tsx
CHANGED
|
@@ -69,6 +69,14 @@ function createDiagramControls(container: HTMLElement): void {
|
|
|
69
69
|
openFullscreenModal(svg.outerHTML)
|
|
70
70
|
})
|
|
71
71
|
|
|
72
|
+
// Scroll wheel zoom on container
|
|
73
|
+
container.addEventListener('wheel', (e) => {
|
|
74
|
+
e.preventDefault()
|
|
75
|
+
const delta = e.deltaY > 0 ? -0.1 : 0.1
|
|
76
|
+
scale = Math.min(Math.max(scale + delta, minScale), maxScale)
|
|
77
|
+
updateScale()
|
|
78
|
+
})
|
|
79
|
+
|
|
72
80
|
container.appendChild(controls)
|
|
73
81
|
}
|
|
74
82
|
|
|
@@ -165,10 +173,13 @@ function openFullscreenModal(svgHtml: string): void {
|
|
|
165
173
|
startX = e.clientX - translateX
|
|
166
174
|
startY = e.clientY - translateY
|
|
167
175
|
svgContainer.style.cursor = 'grabbing'
|
|
176
|
+
svgContainer.style.userSelect = 'none'
|
|
177
|
+
e.preventDefault() // Prevent text selection on drag start
|
|
168
178
|
})
|
|
169
179
|
|
|
170
180
|
document.addEventListener('mousemove', (e) => {
|
|
171
181
|
if (!isDragging) return
|
|
182
|
+
e.preventDefault()
|
|
172
183
|
translateX = e.clientX - startX
|
|
173
184
|
translateY = e.clientY - startY
|
|
174
185
|
updateTransform()
|
|
@@ -176,7 +187,10 @@ function openFullscreenModal(svgHtml: string): void {
|
|
|
176
187
|
|
|
177
188
|
document.addEventListener('mouseup', () => {
|
|
178
189
|
isDragging = false
|
|
179
|
-
if (svgContainer)
|
|
190
|
+
if (svgContainer) {
|
|
191
|
+
svgContainer.style.cursor = 'grab'
|
|
192
|
+
svgContainer.style.userSelect = ''
|
|
193
|
+
}
|
|
180
194
|
})
|
|
181
195
|
|
|
182
196
|
// Mouse wheel zoom
|
package/src/theme/entry.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { pages, sidebar } from 'virtual:prev-pages'
|
|
11
11
|
import { useDiagrams } from './diagrams'
|
|
12
12
|
import { Layout } from './Layout'
|
|
13
|
+
import { MetadataBlock } from './MetadataBlock'
|
|
13
14
|
import './styles.css'
|
|
14
15
|
|
|
15
16
|
// PageTree types (simplified from fumadocs-core)
|
|
@@ -61,10 +62,27 @@ function getPageComponent(file: string): React.ComponentType | null {
|
|
|
61
62
|
return mod?.default || null
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
interface PageMeta {
|
|
66
|
+
title?: string
|
|
67
|
+
description?: string
|
|
68
|
+
frontmatter?: Record<string, unknown>
|
|
69
|
+
}
|
|
70
|
+
|
|
64
71
|
// Page wrapper that renders diagrams after content loads
|
|
65
|
-
function PageWrapper({ Component }: { Component: React.ComponentType }) {
|
|
72
|
+
function PageWrapper({ Component, meta }: { Component: React.ComponentType; meta: PageMeta }) {
|
|
66
73
|
useDiagrams()
|
|
67
|
-
return
|
|
74
|
+
return (
|
|
75
|
+
<>
|
|
76
|
+
{meta.frontmatter && Object.keys(meta.frontmatter).length > 0 && (
|
|
77
|
+
<MetadataBlock
|
|
78
|
+
frontmatter={meta.frontmatter}
|
|
79
|
+
title={meta.title}
|
|
80
|
+
description={meta.description}
|
|
81
|
+
/>
|
|
82
|
+
)}
|
|
83
|
+
<Component />
|
|
84
|
+
</>
|
|
85
|
+
)
|
|
68
86
|
}
|
|
69
87
|
|
|
70
88
|
// Root layout with custom lightweight Layout
|
|
@@ -86,12 +104,17 @@ const rootRoute = createRootRoute({
|
|
|
86
104
|
})
|
|
87
105
|
|
|
88
106
|
// Create routes from pages
|
|
89
|
-
const pageRoutes = pages.map((page: { route: string; file: string }) => {
|
|
107
|
+
const pageRoutes = pages.map((page: { route: string; file: string; title?: string; description?: string; frontmatter?: Record<string, unknown> }) => {
|
|
90
108
|
const Component = getPageComponent(page.file)
|
|
109
|
+
const meta: PageMeta = {
|
|
110
|
+
title: page.title,
|
|
111
|
+
description: page.description,
|
|
112
|
+
frontmatter: page.frontmatter,
|
|
113
|
+
}
|
|
91
114
|
return createRoute({
|
|
92
115
|
getParentRoute: () => rootRoute,
|
|
93
116
|
path: page.route === '/' ? '/' : page.route,
|
|
94
|
-
component: Component ? () => <PageWrapper Component={Component} /> : () => null,
|
|
117
|
+
component: Component ? () => <PageWrapper Component={Component} meta={meta} /> : () => null,
|
|
95
118
|
})
|
|
96
119
|
})
|
|
97
120
|
|
package/src/theme/styles.css
CHANGED
|
@@ -349,6 +349,8 @@ body {
|
|
|
349
349
|
justify-content: center;
|
|
350
350
|
cursor: grab;
|
|
351
351
|
padding: 2rem;
|
|
352
|
+
-webkit-user-select: none;
|
|
353
|
+
user-select: none;
|
|
352
354
|
}
|
|
353
355
|
|
|
354
356
|
.diagram-modal-svg-container svg {
|
|
@@ -589,3 +591,109 @@ body {
|
|
|
589
591
|
background: var(--fd-accent);
|
|
590
592
|
color: var(--fd-accent-foreground);
|
|
591
593
|
}
|
|
594
|
+
|
|
595
|
+
/* ===========================================
|
|
596
|
+
Metadata Block Styles
|
|
597
|
+
=========================================== */
|
|
598
|
+
|
|
599
|
+
.metadata-block {
|
|
600
|
+
margin-bottom: 2rem;
|
|
601
|
+
padding-bottom: 1.5rem;
|
|
602
|
+
border-bottom: 1px solid var(--fd-border);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.metadata-title {
|
|
606
|
+
font-size: 2.25rem;
|
|
607
|
+
font-weight: 700;
|
|
608
|
+
margin: 0 0 0.5rem 0;
|
|
609
|
+
line-height: 1.2;
|
|
610
|
+
letter-spacing: -0.025em;
|
|
611
|
+
color: var(--fd-foreground);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
.metadata-description {
|
|
615
|
+
font-size: 1.125rem;
|
|
616
|
+
color: var(--fd-muted-foreground);
|
|
617
|
+
margin: 0 0 1rem 0;
|
|
618
|
+
line-height: 1.5;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
.metadata-fields {
|
|
622
|
+
display: flex;
|
|
623
|
+
flex-wrap: wrap;
|
|
624
|
+
gap: 0.75rem;
|
|
625
|
+
align-items: center;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
.metadata-field {
|
|
629
|
+
display: inline-flex;
|
|
630
|
+
align-items: center;
|
|
631
|
+
gap: 0.35rem;
|
|
632
|
+
font-size: 0.875rem;
|
|
633
|
+
color: var(--fd-muted-foreground);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
.metadata-icon {
|
|
637
|
+
font-size: 0.875rem;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.metadata-key {
|
|
641
|
+
font-weight: 500;
|
|
642
|
+
color: var(--fd-foreground);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.metadata-chip {
|
|
646
|
+
display: inline-block;
|
|
647
|
+
padding: 0.15rem 0.5rem;
|
|
648
|
+
background: var(--fd-muted);
|
|
649
|
+
border-radius: 0.25rem;
|
|
650
|
+
font-size: 0.8rem;
|
|
651
|
+
color: var(--fd-foreground);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
.metadata-chip.is-draft {
|
|
655
|
+
background: oklch(0.75 0.15 50);
|
|
656
|
+
color: oklch(0.25 0.05 50);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
.dark .metadata-chip.is-draft {
|
|
660
|
+
background: oklch(0.45 0.12 50);
|
|
661
|
+
color: oklch(0.95 0.02 50);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
.metadata-array {
|
|
665
|
+
display: inline-flex;
|
|
666
|
+
flex-wrap: wrap;
|
|
667
|
+
gap: 0.35rem;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
.metadata-boolean {
|
|
671
|
+
font-weight: 600;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
.metadata-boolean.is-true {
|
|
675
|
+
color: oklch(0.55 0.15 145);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
.metadata-boolean.is-false {
|
|
679
|
+
color: oklch(0.55 0.15 25);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.metadata-link {
|
|
683
|
+
color: var(--fd-primary);
|
|
684
|
+
text-decoration: underline;
|
|
685
|
+
text-decoration-color: var(--fd-border);
|
|
686
|
+
text-underline-offset: 2px;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
.metadata-link:hover {
|
|
690
|
+
text-decoration-color: var(--fd-primary);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
.metadata-date {
|
|
694
|
+
font-variant-numeric: tabular-nums;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
.metadata-number {
|
|
698
|
+
font-variant-numeric: tabular-nums;
|
|
699
|
+
}
|