doxla 0.5.2 → 0.6.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/package.json
CHANGED
|
@@ -14,7 +14,7 @@ interface IndexPageProps {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
function getPreview(content: string): string {
|
|
17
|
-
// Strip the first heading and get the first non-empty paragraph
|
|
17
|
+
// Strip the first heading and get the first non-empty text paragraph
|
|
18
18
|
const lines = content.split("\n");
|
|
19
19
|
let foundHeading = false;
|
|
20
20
|
const previewLines: string[] = [];
|
|
@@ -29,7 +29,14 @@ function getPreview(content: string): string {
|
|
|
29
29
|
if (previewLines.length > 0) break;
|
|
30
30
|
continue;
|
|
31
31
|
}
|
|
32
|
-
|
|
32
|
+
// Strip HTML tags in a loop to handle nested/malformed markup
|
|
33
|
+
let textOnly = trimmed;
|
|
34
|
+
while (/<[^>]+>/.test(textOnly)) {
|
|
35
|
+
textOnly = textOnly.replace(/<[^>]+>/g, "");
|
|
36
|
+
}
|
|
37
|
+
textOnly = textOnly.replace(/[<>]/g, "").trim();
|
|
38
|
+
if (textOnly === "") continue;
|
|
39
|
+
previewLines.push(textOnly);
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
const preview = previewLines.join(" ");
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import { Search, Sun, Moon } from "lucide-react";
|
|
3
3
|
import type { Theme } from "../../App";
|
|
4
|
+
import type { DocFile } from "../../types/manifest";
|
|
4
5
|
import { Input } from "../ui/Input";
|
|
5
6
|
import { Button } from "../ui/Button";
|
|
7
|
+
import { MobileNav } from "./MobileNav";
|
|
6
8
|
import logoSrc from "../../assets/logo.svg";
|
|
7
9
|
|
|
8
10
|
interface HeaderProps {
|
|
9
11
|
repoName: string;
|
|
12
|
+
docs: DocFile[];
|
|
10
13
|
theme: Theme;
|
|
11
14
|
onToggleTheme: () => void;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
|
-
export function Header({ repoName, theme, onToggleTheme }: HeaderProps) {
|
|
17
|
+
export function Header({ repoName, docs, theme, onToggleTheme }: HeaderProps) {
|
|
15
18
|
const [searchInput, setSearchInput] = useState("");
|
|
16
19
|
|
|
17
20
|
const handleSearch = (e: React.FormEvent) => {
|
|
@@ -22,7 +25,9 @@ export function Header({ repoName, theme, onToggleTheme }: HeaderProps) {
|
|
|
22
25
|
};
|
|
23
26
|
|
|
24
27
|
return (
|
|
25
|
-
<header className="sticky top-0 z-10 flex h-14 items-center gap-4 border-b border-border bg-background px-6">
|
|
28
|
+
<header className="sticky top-0 z-10 flex h-14 items-center gap-4 border-b border-border bg-background px-4 sm:px-6">
|
|
29
|
+
<MobileNav docs={docs} />
|
|
30
|
+
|
|
26
31
|
<a href="#/" className="flex items-center gap-2 font-semibold">
|
|
27
32
|
<img src={logoSrc} alt="Doxla" className="h-5 w-5 object-contain" />
|
|
28
33
|
<span>{repoName}</span>
|
|
@@ -36,10 +41,10 @@ export function Header({ repoName, theme, onToggleTheme }: HeaderProps) {
|
|
|
36
41
|
placeholder="Search docs..."
|
|
37
42
|
value={searchInput}
|
|
38
43
|
onChange={(e) => setSearchInput(e.target.value)}
|
|
39
|
-
className="w-64 pl-9"
|
|
44
|
+
className="w-full sm:w-64 pl-9"
|
|
40
45
|
/>
|
|
41
46
|
</div>
|
|
42
|
-
<Button type="submit" size="sm">
|
|
47
|
+
<Button type="submit" size="sm" className="hidden sm:inline-flex">
|
|
43
48
|
Search
|
|
44
49
|
</Button>
|
|
45
50
|
</form>
|
|
@@ -14,7 +14,7 @@ interface LayoutProps {
|
|
|
14
14
|
export function Layout({ manifest, theme, onToggleTheme, children }: LayoutProps) {
|
|
15
15
|
return (
|
|
16
16
|
<div className="min-h-screen">
|
|
17
|
-
<Header repoName={manifest.repoName} theme={theme} onToggleTheme={onToggleTheme} />
|
|
17
|
+
<Header repoName={manifest.repoName} docs={manifest.docs} theme={theme} onToggleTheme={onToggleTheme} />
|
|
18
18
|
<div className="flex">
|
|
19
19
|
<Sidebar docs={manifest.docs} />
|
|
20
20
|
<main className="flex-1 overflow-auto">
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { Menu } from "lucide-react";
|
|
3
|
+
import { Button } from "../ui/Button";
|
|
4
|
+
import {
|
|
5
|
+
Sheet,
|
|
6
|
+
SheetOverlay,
|
|
7
|
+
SheetContent,
|
|
8
|
+
SheetHeader,
|
|
9
|
+
SheetTitle,
|
|
10
|
+
} from "../ui/Sheet";
|
|
11
|
+
import { ScrollArea } from "../ui/ScrollArea";
|
|
12
|
+
import { FileTree } from "../FileTree";
|
|
13
|
+
import type { DocFile } from "../../types/manifest";
|
|
14
|
+
|
|
15
|
+
interface MobileNavProps {
|
|
16
|
+
docs: DocFile[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function MobileNav({ docs }: MobileNavProps) {
|
|
20
|
+
const [open, setOpen] = useState(false);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!open) return;
|
|
24
|
+
|
|
25
|
+
const close = () => setOpen(false);
|
|
26
|
+
window.addEventListener("hashchange", close);
|
|
27
|
+
return () => window.removeEventListener("hashchange", close);
|
|
28
|
+
}, [open]);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<>
|
|
32
|
+
<Button
|
|
33
|
+
size="icon"
|
|
34
|
+
variant="ghost"
|
|
35
|
+
className="md:hidden"
|
|
36
|
+
onClick={() => setOpen(true)}
|
|
37
|
+
aria-label="Open navigation"
|
|
38
|
+
>
|
|
39
|
+
<Menu className="h-5 w-5" />
|
|
40
|
+
</Button>
|
|
41
|
+
|
|
42
|
+
<Sheet open={open} onClose={() => setOpen(false)}>
|
|
43
|
+
<SheetOverlay />
|
|
44
|
+
<SheetContent side="left" aria-label="Navigation menu">
|
|
45
|
+
<SheetHeader>
|
|
46
|
+
<SheetTitle>Documents</SheetTitle>
|
|
47
|
+
</SheetHeader>
|
|
48
|
+
<ScrollArea className="flex-1 px-3 pb-4">
|
|
49
|
+
<FileTree docs={docs} />
|
|
50
|
+
</ScrollArea>
|
|
51
|
+
</SheetContent>
|
|
52
|
+
</Sheet>
|
|
53
|
+
</>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useEffect,
|
|
5
|
+
type ReactNode,
|
|
6
|
+
} from "react";
|
|
7
|
+
import { createPortal } from "react-dom";
|
|
8
|
+
import { X } from "lucide-react";
|
|
9
|
+
import { cn } from "../../lib/utils";
|
|
10
|
+
|
|
11
|
+
interface SheetContextValue {
|
|
12
|
+
open: boolean;
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const SheetContext = createContext<SheetContextValue>({
|
|
17
|
+
open: false,
|
|
18
|
+
onClose: () => {},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
interface SheetProps {
|
|
22
|
+
open: boolean;
|
|
23
|
+
onClose: () => void;
|
|
24
|
+
children: ReactNode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function Sheet({ open, onClose, children }: SheetProps) {
|
|
28
|
+
return (
|
|
29
|
+
<SheetContext.Provider value={{ open, onClose }}>
|
|
30
|
+
{children}
|
|
31
|
+
</SheetContext.Provider>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function SheetOverlay() {
|
|
36
|
+
const { open, onClose } = useContext(SheetContext);
|
|
37
|
+
if (!open) return null;
|
|
38
|
+
|
|
39
|
+
return createPortal(
|
|
40
|
+
<div
|
|
41
|
+
className="fixed inset-0 z-40 bg-black/60"
|
|
42
|
+
onClick={onClose}
|
|
43
|
+
aria-hidden="true"
|
|
44
|
+
/>,
|
|
45
|
+
document.body
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface SheetContentProps {
|
|
50
|
+
side?: "left" | "right";
|
|
51
|
+
className?: string;
|
|
52
|
+
"aria-label"?: string;
|
|
53
|
+
children: ReactNode;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function SheetContent({
|
|
57
|
+
side = "left",
|
|
58
|
+
className,
|
|
59
|
+
"aria-label": ariaLabel,
|
|
60
|
+
children,
|
|
61
|
+
}: SheetContentProps) {
|
|
62
|
+
const { open, onClose } = useContext(SheetContext);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!open) return;
|
|
66
|
+
|
|
67
|
+
const prev = document.body.style.overflow;
|
|
68
|
+
document.body.style.overflow = "hidden";
|
|
69
|
+
|
|
70
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
71
|
+
if (e.key === "Escape") onClose();
|
|
72
|
+
};
|
|
73
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
74
|
+
|
|
75
|
+
return () => {
|
|
76
|
+
document.body.style.overflow = prev;
|
|
77
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
78
|
+
};
|
|
79
|
+
}, [open, onClose]);
|
|
80
|
+
|
|
81
|
+
if (!open) return null;
|
|
82
|
+
|
|
83
|
+
return createPortal(
|
|
84
|
+
<div
|
|
85
|
+
role="dialog"
|
|
86
|
+
aria-modal="true"
|
|
87
|
+
aria-label={ariaLabel}
|
|
88
|
+
className={cn(
|
|
89
|
+
"fixed top-0 z-50 flex h-full w-72 flex-col border-r border-border bg-background shadow-lg",
|
|
90
|
+
side === "left" ? "left-0" : "right-0",
|
|
91
|
+
className
|
|
92
|
+
)}
|
|
93
|
+
>
|
|
94
|
+
<button
|
|
95
|
+
onClick={onClose}
|
|
96
|
+
className="absolute right-3 top-3 rounded-sm p-1 text-muted-foreground hover:text-foreground"
|
|
97
|
+
aria-label="Close"
|
|
98
|
+
>
|
|
99
|
+
<X className="h-4 w-4" />
|
|
100
|
+
</button>
|
|
101
|
+
{children}
|
|
102
|
+
</div>,
|
|
103
|
+
document.body
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function SheetHeader({
|
|
108
|
+
className,
|
|
109
|
+
children,
|
|
110
|
+
}: {
|
|
111
|
+
className?: string;
|
|
112
|
+
children: ReactNode;
|
|
113
|
+
}) {
|
|
114
|
+
return (
|
|
115
|
+
<div className={cn("px-4 pt-4 pb-2", className)}>{children}</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function SheetTitle({
|
|
120
|
+
className,
|
|
121
|
+
children,
|
|
122
|
+
}: {
|
|
123
|
+
className?: string;
|
|
124
|
+
children: ReactNode;
|
|
125
|
+
}) {
|
|
126
|
+
return (
|
|
127
|
+
<h2 className={cn("text-sm font-semibold text-muted-foreground", className)}>
|
|
128
|
+
{children}
|
|
129
|
+
</h2>
|
|
130
|
+
);
|
|
131
|
+
}
|