llmd 0.1.1 → 0.2.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.
Files changed (4) hide show
  1. package/README.md +37 -163
  2. package/dist/client.js +1 -1
  3. package/dist/llmd +481 -266
  4. package/package.json +2 -2
package/README.md CHANGED
@@ -2,23 +2,10 @@
2
2
 
3
3
  **Serve Markdown as beautiful HTML. Instantly.**
4
4
 
5
- A zero-config CLI tool for viewing Markdown files in your browser with syntax highlighting, live reload, and a clean interface. Built for developers reviewing LLM-generated documentation.
6
-
7
- ## Features
8
-
9
- - **Zero config** - Point at a directory and go
10
- - **Syntax highlighting** - Powered by Shiki
11
- - **Live reload** - Watch mode reloads on file changes
12
- - **Copy buttons** - One-click code copying
13
- - **Dark/light themes** - With 3 font options
14
- - **Fast** - Built with Bun, instant startup
15
- - **Sidebar navigation** - Browse files with directory structure
16
- - **Table of contents** - Auto-generated from headings
5
+ A minimal CLI tool for viewing Markdown files in your browser with syntax highlighting, live reload, and a clean interface. Built for developers reviewing LLM-generated documentation.
17
6
 
18
7
  ## Installation
19
8
 
20
- ### npm (Recommended)
21
-
22
9
  ```bash
23
10
  npm install -g llmd
24
11
  ```
@@ -31,173 +18,60 @@ npx llmd
31
18
 
32
19
  Requires Node.js 22 or later.
33
20
 
34
- ### From Source
21
+ ## Quick Start
35
22
 
36
- ```bash
37
- git clone https://github.com/pbzona/llmd.git
38
- cd llmd
39
- bun install
40
- bun run build:npm
41
- npm install -g .
42
- ```
43
-
44
- ## Usage
23
+ Want to see what llmd can do? **Try it on its own documentation:**
45
24
 
46
25
  ```bash
47
- # Serve current directory
48
- llmd
49
-
50
- # Serve specific directory
51
- llmd ./docs
52
-
53
- # Dark mode with live reload
54
- llmd ./docs --theme dark --watch
26
+ # Install llmd
27
+ npm install -g llmd
55
28
 
56
- # Custom port
57
- llmd ./docs --port 8080
29
+ # View the documentation
30
+ llmd docs
58
31
  ```
59
32
 
60
- ## Font Combinations
33
+ That's it! The docs will open in your browser. Click through the sidebar to explore.
61
34
 
62
- llmd includes 9 built-in font combinations:
63
-
64
- - **serif** - System serif fonts (Georgia, Times)
65
- - **sans** - System sans-serif fonts (default)
66
- - **mono** - System monospace fonts
67
- - **classic** - Baskerville headings + Geist body
68
- - **future** - Space Mono headings + Space Grotesk body
69
- - **modern** - Inter throughout + JetBrains Mono
70
- - **artsy** - Playfair Display + Fira Code
71
- - **literary** - Spectral headings + Newsreader body
72
- - **editorial** - Bitter headings + Lora body
73
-
74
- All custom fonts are loaded from Google Fonts CDN for fast, reliable delivery.
75
-
76
- ### Custom Fonts
77
-
78
- Create custom font combinations in your `themes.json` config file.
79
-
80
- **Location:** `~/.config/llmd/themes.json` (or `$XDG_CONFIG_HOME/llmd/themes.json`)
81
-
82
- **Simple Example (Auto-loaded from Google Fonts):**
35
+ ## Features
83
36
 
84
- ```json
85
- {
86
- "fontThemes": {
87
- "myfont": {
88
- "heading": "Montserrat, sans-serif",
89
- "body": "Open Sans, sans-serif",
90
- "code": "Source Code Pro, monospace"
91
- }
92
- }
93
- }
94
- ```
37
+ - **Simple setup** - Point at a directory and go
38
+ - **Syntax highlighting** - Powered by Shiki
39
+ - **Live reload** - Watch mode reloads on file changes
40
+ - **Copy buttons** - One-click code copying
41
+ - **Dark/light themes** - With 9 font combinations
42
+ - **Fast** - Built with Bun, instant startup
43
+ - **Sidebar navigation** - Browse files with directory structure
44
+ - **Table of contents** - Auto-generated from headings
45
+ - **Usage Analytics** - Track which docs you view most (local-only, opt-in)
95
46
 
96
- Google Fonts are **loaded automatically** with weights 400 and 700. Just specify the font family names - no need to construct Google Fonts URLs manually!
97
-
98
- **Advanced Example (Custom Weights/Styles):**
99
-
100
- ```json
101
- {
102
- "fontThemes": {
103
- "custom": {
104
- "heading": "Poppins, sans-serif",
105
- "body": "Inter, sans-serif",
106
- "code": "Fira Code, monospace",
107
- "googleFontsUrl": "https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&family=Inter:wght@300;400;500&family=Fira+Code:wght@400;500&display=swap"
108
- }
109
- }
110
- }
111
- ```
47
+ ## Documentation
112
48
 
113
- Use `googleFontsUrl` only if you need specific weights (like 300, 500, 600) or styles (italic, etc.). Get custom URLs from [Google Fonts](https://fonts.google.com).
49
+ - [Installation](./docs/installation.md) - Installation methods
50
+ - [Usage](./docs/usage.md) - Command-line options and examples
51
+ - [Themes](./docs/themes.md) - Built-in and custom color themes
52
+ - [Fonts](./docs/fonts.md) - Built-in and custom font combinations
53
+ - [Analytics](./docs/analytics.md) - Local usage tracking
114
54
 
115
- **Usage:**
55
+ ## Basic Usage
116
56
 
117
57
  ```bash
118
- llmd --fonts myfont
119
- ```
120
-
121
- **Requirements:**
122
-
123
- - `heading`, `body`, and `code` properties are required
124
- - Font names should include CSS fallbacks (e.g., `"Roboto, sans-serif"`)
125
- - System fonts (Arial, Georgia, etc.) don't load from Google Fonts
126
- - Invalid font names will show helpful errors listing all available fonts
127
-
128
- ## Color Themes
129
-
130
- llmd includes 6 built-in color themes:
131
-
132
- - **dark** - Default dark theme (default)
133
- - **light** - Warm light theme
134
- - **nord** - Nord-inspired cool theme
135
- - **dracula** - Dracula-inspired purple theme
136
- - **solarized** - Solarized Light theme
137
- - **monokai** - Monokai-inspired theme
138
-
139
- ### Custom Themes
58
+ # View llmd documentation
59
+ llmd docs
140
60
 
141
- Create custom color themes in your `themes.json` config file.
142
-
143
- **Location:** `~/.config/llmd/themes.json` (or `$XDG_CONFIG_HOME/llmd/themes.json`)
144
-
145
- **Format:**
146
-
147
- ```json
148
- {
149
- "colorThemes": {
150
- "mytheme": {
151
- "bg": "#1a1a1a",
152
- "fg": "#e0e0e0",
153
- "border": "#333",
154
- "hover": "#2a2a2a",
155
- "accent": "#4a9eff",
156
- "codeBg": "#2d2d2d",
157
- "sidebarBg": "#151515",
158
- "folderIcon": "#a78bfa",
159
- "fileIcon": "#fbbf24"
160
- }
161
- }
162
- }
163
- ```
61
+ # Serve current directory
62
+ llmd
164
63
 
165
- **Unified Config:**
166
- You can combine both color themes and font themes in a single file:
167
-
168
- ```json
169
- {
170
- "colorThemes": {
171
- "mytheme": { ... }
172
- },
173
- "fontThemes": {
174
- "myfont": { ... }
175
- }
176
- }
177
- ```
64
+ # Serve specific directory
65
+ llmd ./docs
178
66
 
179
- **Usage:**
67
+ # Dark mode with live reload
68
+ llmd ./docs --theme dark --watch
180
69
 
181
- ```bash
182
- llmd --theme mytheme --fonts myfont
70
+ # Open directly to analytics
71
+ llmd analytics
183
72
  ```
184
73
 
185
- If a theme or font is not found, llmd will list all available options.
186
-
187
- **Note:** The old flat format for color themes (without `colorThemes` key) is still supported for backward compatibility.
188
-
189
- ## Options
190
-
191
- | Flag | Description | Default |
192
- | ---------------------- | ---------------------------------------------------------------------------------------------------------- | ------------ |
193
- | `--port <number>` | Port (0 = random) | `0` (random) |
194
- | `--host <string>` | Host interface | `localhost` |
195
- | `--theme <name>` | Color theme: `dark`, `light`, `nord`, `dracula`, `solarized`, `monokai`, or custom | `dark` |
196
- | `--fonts <name>` | Font combination: `serif`, `sans`, `mono`, `classic`, `future`, `modern`, `artsy`, `literary`, `editorial` | `sans` |
197
- | `--open / --no-open` | Auto-open browser | `--open` |
198
- | `--watch / --no-watch` | Live reload on changes | `--no-watch` |
199
- | `-h, --help` | Show help | |
200
- | `--version` | Show version | |
74
+ See [Usage](./docs/usage.md) for all options.
201
75
 
202
76
  ## Development
203
77
 
@@ -220,7 +94,7 @@ bun run build
220
94
  - **Runtime**: Bun
221
95
  - **Markdown**: marked (GFM support)
222
96
  - **Highlighting**: Shiki (VS Code themes)
223
- - **Server**: Bun.serve() with WebSocket
97
+ - **Server**: Node.js http + ws
224
98
  - **Bundler**: Bun's built-in bundler
225
99
 
226
100
  ## License
package/dist/client.js CHANGED
@@ -1 +1 @@
1
- var B=()=>{let t=document.querySelectorAll(".dir-label");for(let C of Array.from(t)){let p=C,y=document.createElement("span");y.className="dir-chevron",y.innerHTML='<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>',p.insertBefore(y,p.firstChild),p.style.cursor="pointer",p.addEventListener("click",(m)=>{m.preventDefault();let o=p.closest(".dir-item");if(o)o.classList.toggle("collapsed")})}},M=()=>{let t=document.querySelector(".toc");if(!t)return;let C=t.querySelector("h3");if(!C)return;let p=document.createElement("span");p.className="toc-chevron",p.innerHTML='<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>',C.style.cursor="pointer",C.insertBefore(p,C.firstChild),t.classList.add("collapsed"),C.addEventListener("click",()=>{t.classList.toggle("collapsed")})};if(typeof window<"u")window.addEventListener("DOMContentLoaded",()=>{B(),M()});var q=()=>{document.querySelectorAll("pre code").forEach((t)=>{let C=t.parentElement;if(!C)return;if(C.querySelector(".copy-button"))return;let p=document.createElement("button");p.className="copy-button",p.textContent="Copy",p.setAttribute("aria-label","Copy code to clipboard"),p.addEventListener("click",async()=>{try{await navigator.clipboard.writeText(t.textContent||""),p.textContent="Copied!",p.classList.add("copied"),setTimeout(()=>{p.textContent="Copy",p.classList.remove("copied")},2000)}catch(y){console.error("Failed to copy:",y),p.textContent="Failed",setTimeout(()=>{p.textContent="Copy"},2000)}}),C.appendChild(p)})};if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",q);else q();var O=(t)=>{let p=`${window.location.protocol==="https:"?"wss:":"ws:"}//${window.location.host}/_ws`,y=null,m=null,o=()=>{y=new WebSocket(p),y.addEventListener("open",()=>{console.log("[llmd] Connected to file watcher"),y?.send(JSON.stringify({type:"watch",file:t}))}),y.addEventListener("message",(E)=>{try{let f=JSON.parse(E.data);if(f.type==="reload"&&f.file===t)console.log(`[llmd] File changed: ${t}, reloading...`),window.location.reload()}catch(f){console.error("[llmd] Failed to parse message:",f)}}),y.addEventListener("close",()=>{console.log("[llmd] Disconnected from file watcher"),m=window.setTimeout(()=>{console.log("[llmd] Reconnecting..."),o()},2000)}),y.addEventListener("error",(E)=>{console.error("[llmd] WebSocket error:",E),y?.close()})};o(),window.addEventListener("beforeunload",()=>{if(m)clearTimeout(m);y?.close()})};window.connectFileWatcher=O;var k=()=>{let t=document.querySelector(".sidebar");if(!t)return;let C=document.createElement("div");C.className="sidebar-resize-handle",t.appendChild(C);let p=!1,y=0,m=0,o=(u)=>{p=!0,y=u.clientX,m=t.offsetWidth,document.body.style.cursor="ew-resize",document.body.style.userSelect="none",u.preventDefault()},E=(u)=>{if(!p)return;let A=u.clientX-y,j=m+A,x=Math.max(200,Math.min(600,j));t.style.width=`${x}px`,localStorage.setItem("llmd-sidebar-width",x.toString())},f=()=>{if(!p)return;p=!1,document.body.style.cursor="",document.body.style.userSelect=""};C.addEventListener("mousedown",o),document.addEventListener("mousemove",E),document.addEventListener("mouseup",f);let L=localStorage.getItem("llmd-sidebar-width");if(L){let u=Number.parseInt(L,10);if(u>=200&&u<=600)t.style.width=`${u}px`}};if(typeof window<"u")window.addEventListener("DOMContentLoaded",()=>{k()});console.log("[llmd] Client initialized");
1
+ var Q=()=>{let y=document.querySelectorAll(".dir-label");for(let m of Array.from(y)){let C=m,L=document.createElement("span");L.className="dir-chevron",L.innerHTML='<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>',C.insertBefore(L,C.firstChild),C.style.cursor="pointer",C.addEventListener("click",(q)=>{q.preventDefault();let B=C.closest(".dir-item");if(B)B.classList.toggle("collapsed")})}},Y=()=>{let y=document.querySelector(".toc");if(!y)return;let m=y.querySelector("h3");if(!m)return;let C=document.createElement("span");C.className="toc-chevron",C.innerHTML='<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>',m.style.cursor="pointer",m.insertBefore(C,m.firstChild),y.classList.add("collapsed"),m.addEventListener("click",()=>{y.classList.toggle("collapsed")})};if(typeof window<"u")window.addEventListener("DOMContentLoaded",()=>{Q(),Y()});var u=()=>{for(let y of Array.from(document.querySelectorAll("pre code"))){let m=y.parentElement;if(!m)continue;if(m.querySelector(".copy-button"))continue;let C=document.createElement("button");C.className="copy-button",C.textContent="Copy",C.setAttribute("aria-label","Copy code to clipboard"),C.addEventListener("click",async()=>{try{await navigator.clipboard.writeText(y.textContent||""),C.textContent="Copied!",C.classList.add("copied"),setTimeout(()=>{C.textContent="Copy",C.classList.remove("copied")},2000)}catch(L){console.error("Failed to copy:",L),C.textContent="Failed",setTimeout(()=>{C.textContent="Copy"},2000)}}),m.appendChild(C)}};if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",u);else u();var Z=(y)=>{let C=`${window.location.protocol==="https:"?"wss:":"ws:"}//${window.location.host}/_ws`,L=null,q=null,B=()=>{L=new WebSocket(C),L.addEventListener("open",()=>{console.log("[llmd] Connected to file watcher"),L?.send(JSON.stringify({type:"watch",file:y}))}),L.addEventListener("message",(M)=>{try{let x=JSON.parse(M.data);if(x.type==="reload"&&x.file===y)console.log(`[llmd] File changed: ${y}, reloading...`),window.location.reload()}catch(x){console.error("[llmd] Failed to parse message:",x)}}),L.addEventListener("close",()=>{console.log("[llmd] Disconnected from file watcher"),q=window.setTimeout(()=>{console.log("[llmd] Reconnecting..."),B()},2000)}),L.addEventListener("error",(M)=>{console.error("[llmd] WebSocket error:",M),L?.close()})};B(),window.addEventListener("beforeunload",()=>{if(q)clearTimeout(q);L?.close()})};window.connectFileWatcher=Z;var _=()=>{let y=document.querySelector(".sidebar");if(!y)return;let m=document.createElement("div");m.className="sidebar-resize-handle",y.appendChild(m);let C=!1,L=0,q=0,B=(A)=>{C=!0,L=A.clientX,q=y.offsetWidth,document.body.style.cursor="ew-resize",document.body.style.userSelect="none",A.preventDefault()},M=(A)=>{if(!C)return;let K=A.clientX-L,O=q+K,k=Math.max(200,Math.min(600,O));y.style.width=`${k}px`,localStorage.setItem("llmd-sidebar-width",k.toString())},x=()=>{if(!C)return;C=!1,document.body.style.cursor="",document.body.style.userSelect=""};m.addEventListener("mousedown",B),document.addEventListener("mousemove",M),document.addEventListener("mouseup",x);let j=localStorage.getItem("llmd-sidebar-width");if(j){let A=Number.parseInt(j,10);if(A>=200&&A<=600)y.style.width=`${A}px`}};if(typeof window<"u")window.addEventListener("DOMContentLoaded",()=>{_()});var E=(y,m,C)=>{fetch("/api/events",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:y,path:m,resourceType:C})}).catch(()=>{})},G=(y)=>{E("open",y,"dir")},J=(y)=>{E("view",y,"file")};window.trackDirectoryOpen=G;window.trackFileView=J;console.log("[llmd] Client initialized");