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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doxla",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "Improve documentation discoverability within repos",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- previewLines.push(trimmed);
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
+ }