@henryavila/mdprobe 0.1.0 → 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.
- package/README.md +173 -185
- package/README.pt-BR.md +392 -0
- package/bin/cli.js +78 -60
- package/dist/assets/index-Cp_TccJA.css +1 -0
- package/dist/assets/index-DuVgC81Y.js +2 -0
- package/dist/index.html +3 -3
- package/package.json +5 -2
- package/schema.json +2 -2
- package/skills/mdprobe/SKILL.md +143 -278
- package/src/annotations.js +1 -1
- package/src/export.js +1 -1
- package/src/mcp.js +258 -0
- package/src/open-browser.js +27 -0
- package/src/server.js +93 -10
- package/src/setup-ui.js +108 -0
- package/src/setup.js +203 -0
- package/src/singleton.js +236 -0
- package/src/ui/app.jsx +21 -12
- package/src/ui/components/RightPanel.jsx +128 -69
- package/src/ui/hooks/useAnnotations.js +6 -1
- package/src/ui/hooks/useClientLibs.js +2 -2
- package/src/ui/hooks/useWebSocket.js +7 -3
- package/src/ui/index.html +1 -1
- package/src/ui/state/store.js +9 -0
- package/src/ui/styles/themes.css +43 -3
- package/dist/assets/index-DPysqH1p.js +0 -2
- package/dist/assets/index-nl9v2RuJ.css +0 -1
package/README.pt-BR.md
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# mdProbe
|
|
4
|
+
|
|
5
|
+
Visualizador e revisor de markdown com live reload, anotações persistentes e integração com agentes de IA.
|
|
6
|
+
|
|
7
|
+
Abra arquivos `.md` no browser, anote inline, aprove seções e exporte feedback estruturado em YAML — tudo pelo terminal.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## O que o mdProbe é
|
|
12
|
+
|
|
13
|
+
- Uma **ferramenta CLI** que renderiza markdown no browser com live reload
|
|
14
|
+
- Um **sistema de anotações** onde você seleciona texto e adiciona comentários com tags (bug, question, suggestion, nitpick)
|
|
15
|
+
- Um **workflow de revisão** com aprovação por seção (aprovar/rejeitar por heading)
|
|
16
|
+
- Um **servidor MCP** que permite agentes de IA (Claude Code, Cursor, etc.) abrir arquivos, ler anotações e resolver feedback programaticamente
|
|
17
|
+
|
|
18
|
+
## O que o mdProbe não é
|
|
19
|
+
|
|
20
|
+
- Não é um editor de markdown — você edita no seu editor, o mdprobe renderiza e anota
|
|
21
|
+
- Não é um gerador de sites estáticos — ele roda um servidor local para preview ao vivo
|
|
22
|
+
- Não é exclusivo para IA — funciona perfeitamente como ferramenta standalone de revisão
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Instalação
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install -g @henryavila/mdprobe
|
|
30
|
+
mdprobe setup
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
O wizard de setup configura seu nome de autor, instala a skill de IA nas IDEs detectadas (Claude Code, Cursor, Gemini), registra o servidor MCP e adiciona um hook PostToolUse.
|
|
34
|
+
|
|
35
|
+
Para ambientes não-interativos: `mdprobe setup --yes --author "Seu Nome"`
|
|
36
|
+
|
|
37
|
+
Ou execute sem instalar:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx @henryavila/mdprobe README.md
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Requisitos:** Node.js 20+, um browser.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Início Rápido
|
|
48
|
+
|
|
49
|
+
### Visualizar e editar
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
mdprobe README.md
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Abre o markdown renderizado no browser. Edite o arquivo fonte — o browser atualiza instantaneamente via WebSocket.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
mdprobe docs/
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Descobre todos os `.md` recursivamente e mostra um seletor de arquivos.
|
|
62
|
+
|
|
63
|
+
### Anotar
|
|
64
|
+
|
|
65
|
+
Selecione qualquer texto no browser → escolha uma tag → escreva um comentário → salve.
|
|
66
|
+
|
|
67
|
+
| Tag | Significado |
|
|
68
|
+
|-----|-------------|
|
|
69
|
+
| `bug` | Algo está errado |
|
|
70
|
+
| `question` | Precisa de esclarecimento |
|
|
71
|
+
| `suggestion` | Ideia de melhoria |
|
|
72
|
+
| `nitpick` | Detalhe menor de estilo/texto |
|
|
73
|
+
|
|
74
|
+
Anotações são armazenadas em arquivos sidecar `.annotations.yaml` — legíveis por humanos, amigáveis para git.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Servidor Singleton
|
|
79
|
+
|
|
80
|
+
O mdProbe roda uma **única instância do servidor**. Múltiplas invocações compartilham o mesmo servidor ao invés de iniciar duplicatas:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
mdprobe README.md # Inicia servidor na porta 3000, abre browser
|
|
84
|
+
mdprobe CHANGELOG.md # Detecta servidor rodando, adiciona arquivo, abre browser, sai
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
A segunda invocação adiciona seus arquivos ao servidor existente e sai imediatamente. O browser mostra todos os arquivos na sidebar.
|
|
88
|
+
|
|
89
|
+
**Como funciona:** Um lock file em `/tmp/mdprobe.lock` registra o PID, porta e URL do servidor rodando. Novas invocações leem o lock file, verificam se o servidor está vivo via health check HTTP, e entram via `POST /api/add-files`. Ao desligar (`Ctrl+C`), o lock file é removido automaticamente.
|
|
90
|
+
|
|
91
|
+
**Recuperação de lock stale:** Se uma instância anterior crashou, a próxima invocação detecta o processo morto e inicia normalmente.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Dois Workflows de Revisão
|
|
96
|
+
|
|
97
|
+
O mdProbe suporta dois workflows de revisão distintos para contextos diferentes:
|
|
98
|
+
|
|
99
|
+
### 1. Revisão bloqueante (`--once`) — para CI/CD e scripts
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
mdprobe spec.md --once
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Bloqueia o processo até você clicar **"Finish Review"** na UI. Ao finalizar, as anotações são salvas em `spec.annotations.yaml` e o processo sai com a lista de arquivos criados. Útil para pipelines que precisam de aprovação humana antes de continuar.
|
|
106
|
+
|
|
107
|
+
O modo `--once` sempre cria uma **instância isolada** — não participa do singleton. Isso garante que sessões de revisão tenham ciclo de vida independente.
|
|
108
|
+
|
|
109
|
+
### 2. Revisão assistida por IA (MCP) — para agentes de código
|
|
110
|
+
|
|
111
|
+
Ao trabalhar com agentes de IA (Claude Code, Cursor, etc.), o workflow é diferente. O agente **não usa `--once`**. Em vez disso:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
Agente escreve spec.md
|
|
115
|
+
↓
|
|
116
|
+
Agente chama mdprobe_view → browser abre, servidor continua rodando
|
|
117
|
+
↓
|
|
118
|
+
Humano lê, anota, aprova/rejeita seções
|
|
119
|
+
↓
|
|
120
|
+
Humano diz ao agente via chat: "terminei de revisar"
|
|
121
|
+
↓
|
|
122
|
+
Agente chama mdprobe_annotations → lê todo o feedback
|
|
123
|
+
↓
|
|
124
|
+
Agente corrige bugs, responde perguntas, avalia sugestões
|
|
125
|
+
↓
|
|
126
|
+
Agente reporta mudanças, pede confirmação
|
|
127
|
+
↓
|
|
128
|
+
Agente chama mdprobe_update → resolve anotações
|
|
129
|
+
↓
|
|
130
|
+
Humano vê itens resolvidos em tempo real (esmaecidos)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
O servidor continua rodando durante toda a conversa. O agente lê anotações sob demanda — sem bloqueio, sem sair do processo. Múltiplos arquivos podem ser revisados na mesma sessão via servidor singleton.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Funcionalidades
|
|
138
|
+
|
|
139
|
+
### Renderização
|
|
140
|
+
|
|
141
|
+
Tabelas GFM, syntax highlighting (highlight.js), diagramas Mermaid, math/LaTeX (KaTeX), frontmatter YAML/TOML, HTML raw, imagens do diretório fonte.
|
|
142
|
+
|
|
143
|
+
### Live Reload
|
|
144
|
+
|
|
145
|
+
Mudanças detectadas via chokidar, enviadas por WebSocket. Debounce de 100ms. Posição de scroll preservada.
|
|
146
|
+
|
|
147
|
+
### Aprovação de Seções
|
|
148
|
+
|
|
149
|
+
Cada heading ganha botões de aprovar/rejeitar. Aprovar um pai cascateia para todos os filhos. Barra de progresso mostra seções revisadas vs total.
|
|
150
|
+
|
|
151
|
+
### Detecção de Drift
|
|
152
|
+
|
|
153
|
+
Banner de aviso quando o arquivo fonte muda após as anotações terem sido criadas.
|
|
154
|
+
|
|
155
|
+
### Temas
|
|
156
|
+
|
|
157
|
+
Cinco temas baseados no Catppuccin: Mocha (escuro, padrão), Macchiato, Frappe, Latte, Light.
|
|
158
|
+
|
|
159
|
+
### Atalhos de Teclado
|
|
160
|
+
|
|
161
|
+
| Tecla | Ação |
|
|
162
|
+
|-------|------|
|
|
163
|
+
| `[` | Toggle painel esquerdo (arquivos + TOC) |
|
|
164
|
+
| `]` | Toggle painel direito (anotações) |
|
|
165
|
+
| `\` | Modo foco (esconde ambos os painéis) |
|
|
166
|
+
| `j` / `k` | Próxima / anterior anotação |
|
|
167
|
+
| `?` | Overlay de ajuda |
|
|
168
|
+
| `Ctrl+Enter` | Salvar anotação |
|
|
169
|
+
|
|
170
|
+
### Exportação
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
mdprobe export spec.md --report # Relatório de revisão em markdown
|
|
174
|
+
mdprobe export spec.md --inline # Anotações inseridas no fonte
|
|
175
|
+
mdprobe export spec.md --json # JSON puro
|
|
176
|
+
mdprobe export spec.md --sarif # SARIF 2.1.0 (integração CI/CD)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Integração com Agentes de IA
|
|
182
|
+
|
|
183
|
+
O mdProbe inclui um servidor MCP (Model Context Protocol) e um arquivo de skill (`SKILL.md`) que ensina agentes de IA a usar o workflow de revisão. Isso habilita um loop bidirecional: o agente escreve markdown, o humano anota, o agente lê o feedback e resolve.
|
|
184
|
+
|
|
185
|
+
### Setup
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
mdprobe setup
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Wizard interativo que:
|
|
192
|
+
1. Instala o `SKILL.md` nas IDEs detectadas (Claude Code, Cursor, Gemini)
|
|
193
|
+
2. Registra o servidor MCP (`mdprobe mcp`) na config do Claude Code
|
|
194
|
+
3. Adiciona um hook PostToolUse que lembra o agente de usar mdprobe ao editar `.md`
|
|
195
|
+
4. Configura seu nome de autor
|
|
196
|
+
|
|
197
|
+
Não-interativo: `mdprobe setup --yes --author "Seu Nome"`
|
|
198
|
+
Remover tudo: `mdprobe setup --remove`
|
|
199
|
+
|
|
200
|
+
### Ferramentas MCP
|
|
201
|
+
|
|
202
|
+
Após o setup, agentes de IA podem chamar estas ferramentas:
|
|
203
|
+
|
|
204
|
+
| Ferramenta | Propósito |
|
|
205
|
+
|------------|-----------|
|
|
206
|
+
| `mdprobe_view` | Abrir `.md` no browser |
|
|
207
|
+
| `mdprobe_annotations` | Ler anotações e status das seções |
|
|
208
|
+
| `mdprobe_update` | Resolver, responder, adicionar ou deletar anotações |
|
|
209
|
+
| `mdprobe_status` | Verificar se o servidor está rodando |
|
|
210
|
+
|
|
211
|
+
O servidor MCP participa do singleton — se um servidor iniciado via CLI já estiver rodando, o agente o reutiliza.
|
|
212
|
+
|
|
213
|
+
### Registro Manual do MCP
|
|
214
|
+
|
|
215
|
+
Se preferir não usar `mdprobe setup`:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
claude mcp add --scope user --transport stdio mdprobe -- mdprobe mcp
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Referência CLI
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
mdprobe [arquivos...] [opções]
|
|
227
|
+
|
|
228
|
+
Opções:
|
|
229
|
+
--port <n> Porta (padrão: 3000, auto-incrementa se ocupada)
|
|
230
|
+
--once Revisão bloqueante — servidor isolado, sai ao "Finish Review"
|
|
231
|
+
--no-open Não abrir browser automaticamente
|
|
232
|
+
--help, -h Mostrar ajuda
|
|
233
|
+
--version, -v Mostrar versão
|
|
234
|
+
|
|
235
|
+
Subcomandos:
|
|
236
|
+
setup Setup interativo (skill + MCP + hook)
|
|
237
|
+
setup --remove Desinstalar tudo
|
|
238
|
+
setup --yes [--author] Setup não-interativo
|
|
239
|
+
mcp Iniciar servidor MCP (stdio, para agentes de IA)
|
|
240
|
+
config [key] [value] Gerenciar configuração
|
|
241
|
+
export <path> [flags] Exportar anotações (--report, --inline, --json, --sarif)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## API como Biblioteca
|
|
247
|
+
|
|
248
|
+
### Embutir no seu próprio servidor
|
|
249
|
+
|
|
250
|
+
```javascript
|
|
251
|
+
import { createHandler } from '@henryavila/mdprobe'
|
|
252
|
+
|
|
253
|
+
const handler = createHandler({
|
|
254
|
+
resolveFile: (req) => '/path/to/file.md',
|
|
255
|
+
listFiles: () => [
|
|
256
|
+
{ id: 'spec', path: '/docs/spec.md', label: 'Especificação' },
|
|
257
|
+
],
|
|
258
|
+
basePath: '/review',
|
|
259
|
+
author: 'Review Bot',
|
|
260
|
+
onComplete: (result) => {
|
|
261
|
+
console.log(`Revisão concluída: ${result.annotations} anotações`)
|
|
262
|
+
},
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
import http from 'node:http'
|
|
266
|
+
http.createServer(handler).listen(3000)
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Trabalhando com anotações programaticamente
|
|
270
|
+
|
|
271
|
+
```javascript
|
|
272
|
+
import { AnnotationFile } from '@henryavila/mdprobe/annotations'
|
|
273
|
+
|
|
274
|
+
const af = await AnnotationFile.load('spec.annotations.yaml')
|
|
275
|
+
|
|
276
|
+
// Consultar
|
|
277
|
+
const open = af.getOpen()
|
|
278
|
+
const bugs = af.getByTag('bug')
|
|
279
|
+
|
|
280
|
+
// Modificar
|
|
281
|
+
af.add({
|
|
282
|
+
selectors: {
|
|
283
|
+
position: { startLine: 10, startColumn: 1, endLine: 10, endColumn: 40 },
|
|
284
|
+
quote: { exact: 'texto selecionado', prefix: '', suffix: '' },
|
|
285
|
+
},
|
|
286
|
+
comment: 'Isso precisa de esclarecimento',
|
|
287
|
+
tag: 'question',
|
|
288
|
+
author: 'Henry',
|
|
289
|
+
})
|
|
290
|
+
af.resolve(bugs[0].id)
|
|
291
|
+
await af.save('spec.annotations.yaml')
|
|
292
|
+
|
|
293
|
+
// Exportar
|
|
294
|
+
import { exportJSON, exportSARIF } from '@henryavila/mdprobe/export'
|
|
295
|
+
const sarif = exportSARIF(af, 'spec.md')
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Schema de Anotações
|
|
301
|
+
|
|
302
|
+
Formato do arquivo sidecar (`<arquivo>.annotations.yaml`):
|
|
303
|
+
|
|
304
|
+
```yaml
|
|
305
|
+
version: 1
|
|
306
|
+
source: spec.md
|
|
307
|
+
source_hash: "sha256:abc123..."
|
|
308
|
+
sections:
|
|
309
|
+
- heading: Introdução
|
|
310
|
+
level: 2
|
|
311
|
+
status: approved
|
|
312
|
+
annotations:
|
|
313
|
+
- id: "a1b2c3d4"
|
|
314
|
+
selectors:
|
|
315
|
+
position: { startLine: 15, startColumn: 1, endLine: 15, endColumn: 42 }
|
|
316
|
+
quote: { exact: "O sistema deve suportar usuários concorrentes" }
|
|
317
|
+
comment: "Quantos usuários concorrentes?"
|
|
318
|
+
tag: question
|
|
319
|
+
status: open
|
|
320
|
+
author: Henry
|
|
321
|
+
created_at: "2026-04-08T10:30:00.000Z"
|
|
322
|
+
replies:
|
|
323
|
+
- author: Agente
|
|
324
|
+
comment: "Meta é 500 concorrentes."
|
|
325
|
+
created_at: "2026-04-08T11:00:00.000Z"
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
JSON Schema disponível em `@henryavila/mdprobe/schema.json`.
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## API HTTP
|
|
333
|
+
|
|
334
|
+
Disponível quando o servidor está rodando:
|
|
335
|
+
|
|
336
|
+
| Método | Endpoint | Descrição |
|
|
337
|
+
|--------|----------|-----------|
|
|
338
|
+
| `GET` | `/api/files` | Listar arquivos markdown |
|
|
339
|
+
| `GET` | `/api/file?path=<arquivo>` | HTML renderizado + TOC + frontmatter |
|
|
340
|
+
| `GET` | `/api/annotations?path=<arquivo>` | Anotações + seções + status de drift |
|
|
341
|
+
| `POST` | `/api/annotations` | Criar/atualizar/deletar anotações |
|
|
342
|
+
| `POST` | `/api/sections` | Aprovar/rejeitar/resetar seções |
|
|
343
|
+
| `GET` | `/api/export?path=<arquivo>&format=<fmt>` | Exportar (json, report, inline, sarif) |
|
|
344
|
+
| `GET` | `/api/status` | Identidade do servidor, PID, porta, lista de arquivos |
|
|
345
|
+
| `POST` | `/api/add-files` | Adicionar arquivos a servidor rodando (singleton join) |
|
|
346
|
+
|
|
347
|
+
WebSocket em `/ws` para atualizações em tempo real.
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## Desenvolvimento
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
git clone https://github.com/henryavila/mdprobe.git
|
|
355
|
+
cd mdprobe
|
|
356
|
+
npm install
|
|
357
|
+
npm run build:ui
|
|
358
|
+
npm test
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### Estrutura do Projeto
|
|
362
|
+
|
|
363
|
+
```
|
|
364
|
+
bin/cli.js Entry point da CLI
|
|
365
|
+
src/
|
|
366
|
+
server.js Servidor HTTP + WebSocket
|
|
367
|
+
singleton.js Lock file + coordenação singleton cross-process
|
|
368
|
+
mcp.js Servidor MCP (4 tools, transporte stdio)
|
|
369
|
+
renderer.js Markdown → HTML (unified/remark/rehype)
|
|
370
|
+
annotations.js CRUD de anotações + aprovação de seções
|
|
371
|
+
export.js Exportação: report, inline, JSON, SARIF
|
|
372
|
+
setup.js Registro de skill + MCP + hook nas IDEs
|
|
373
|
+
setup-ui.js Wizard interativo de setup
|
|
374
|
+
handler.js API de biblioteca para embedding
|
|
375
|
+
config.js Config do usuário (~/.mdprobe.json)
|
|
376
|
+
open-browser.js Abertura de browser cross-platform
|
|
377
|
+
hash.js Detecção de drift via SHA-256
|
|
378
|
+
anchoring.js Matching de posição de texto
|
|
379
|
+
ui/
|
|
380
|
+
components/ Componentes Preact
|
|
381
|
+
hooks/ WebSocket, keyboard, tema, anotações
|
|
382
|
+
state/store.js Estado via Preact Signals
|
|
383
|
+
styles/themes.css Temas Catppuccin
|
|
384
|
+
schema.json JSON Schema para YAML de anotações
|
|
385
|
+
skills/mdprobe/ Skill para agentes de IA (SKILL.md)
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
## Licença
|
|
391
|
+
|
|
392
|
+
MIT © [Henry Avila](https://github.com/henryavila)
|
package/bin/cli.js
CHANGED
|
@@ -9,6 +9,8 @@ import { AnnotationFile } from '../src/annotations.js'
|
|
|
9
9
|
import { exportReport, exportInline, exportJSON, exportSARIF } from '../src/export.js'
|
|
10
10
|
import { createServer as createMdprobeServer } from '../src/server.js'
|
|
11
11
|
import { findMarkdownFiles, extractFlag, hasFlag } from '../src/cli-utils.js'
|
|
12
|
+
import { openBrowser } from '../src/open-browser.js'
|
|
13
|
+
import { discoverExistingServer, joinExistingServer, writeLockFile, registerShutdownHandlers } from '../src/singleton.js'
|
|
12
14
|
|
|
13
15
|
// ---------------------------------------------------------------------------
|
|
14
16
|
// Resolve paths
|
|
@@ -29,18 +31,22 @@ const rawArgs = process.argv.slice(2)
|
|
|
29
31
|
function printUsage() {
|
|
30
32
|
console.log(`Usage: mdprobe [files...] [options]
|
|
31
33
|
|
|
32
|
-
Markdown viewer + reviewer with live reload and persistent annotations.
|
|
34
|
+
mdProbe — Markdown viewer + reviewer with live reload and persistent annotations.
|
|
33
35
|
|
|
34
36
|
Options:
|
|
35
37
|
--port <n> Port number (default: 3000)
|
|
36
38
|
--once Review mode (single pass, then exit)
|
|
39
|
+
--no-open Don't auto-open browser
|
|
37
40
|
--help, -h Show help
|
|
38
41
|
--version, -v Show version
|
|
39
42
|
|
|
40
43
|
Subcommands:
|
|
44
|
+
setup Interactive setup (skill + MCP + hook)
|
|
45
|
+
setup --remove Uninstall everything
|
|
46
|
+
setup --yes [--author] Non-interactive setup
|
|
47
|
+
mcp Start MCP server (stdio, used by Claude Code)
|
|
41
48
|
config [key] [value] Manage configuration
|
|
42
49
|
export <path> [flags] Export annotations (--report, --inline, --json, --sarif)
|
|
43
|
-
install --plugin Install Claude Code skill/plugin
|
|
44
50
|
`)
|
|
45
51
|
}
|
|
46
52
|
|
|
@@ -112,30 +118,21 @@ async function main() {
|
|
|
112
118
|
process.exit(0)
|
|
113
119
|
}
|
|
114
120
|
|
|
115
|
-
// ----
|
|
116
|
-
if (subcommand === '
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
const os = await import('node:os')
|
|
120
|
-
const fs = await import('node:fs/promises')
|
|
121
|
-
const destDir = join(os.homedir(), '.claude', 'skills', 'mdprobe')
|
|
122
|
-
const srcFile = join(PROJECT_ROOT, 'skills', 'mdprobe', 'SKILL.md')
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
await fs.mkdir(destDir, { recursive: true })
|
|
126
|
-
const content = await fs.readFile(srcFile, 'utf-8')
|
|
127
|
-
await fs.writeFile(join(destDir, 'SKILL.md'), content, 'utf-8')
|
|
128
|
-
console.log(`Plugin installed to ${destDir}/SKILL.md`)
|
|
129
|
-
console.log('Claude Code will now suggest mdprobe for rich markdown output.')
|
|
130
|
-
} catch (err) {
|
|
131
|
-
fatal(`Error installing plugin: ${err.message}`)
|
|
132
|
-
}
|
|
133
|
-
} else {
|
|
134
|
-
fatal('Usage: mdprobe install --plugin')
|
|
135
|
-
}
|
|
121
|
+
// ---- setup subcommand ----
|
|
122
|
+
if (subcommand === 'setup') {
|
|
123
|
+
const { runSetup } = await import('../src/setup-ui.js')
|
|
124
|
+
await runSetup(args.slice(1))
|
|
136
125
|
process.exit(0)
|
|
137
126
|
}
|
|
138
127
|
|
|
128
|
+
// ---- mcp subcommand ----
|
|
129
|
+
if (subcommand === 'mcp') {
|
|
130
|
+
const { startMcpServer } = await import('../src/mcp.js')
|
|
131
|
+
await startMcpServer()
|
|
132
|
+
// MCP server runs until parent process terminates
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
139
136
|
// ---- export subcommand ----
|
|
140
137
|
if (subcommand === 'export') {
|
|
141
138
|
const mdPath = args[1]
|
|
@@ -200,9 +197,9 @@ async function main() {
|
|
|
200
197
|
|
|
201
198
|
// ---- Serve mode (default) ----
|
|
202
199
|
|
|
203
|
-
// Check if author is configured; prompt if missing
|
|
200
|
+
// Check if author is configured; prompt if missing (only in interactive terminals)
|
|
204
201
|
let currentAuthor = await getAuthor()
|
|
205
|
-
if (currentAuthor === 'anonymous') {
|
|
202
|
+
if (currentAuthor === 'anonymous' && process.stdin.isTTY) {
|
|
206
203
|
const { createInterface } = await import('node:readline')
|
|
207
204
|
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
208
205
|
const name = await new Promise(resolve => {
|
|
@@ -274,23 +271,23 @@ async function main() {
|
|
|
274
271
|
}
|
|
275
272
|
}
|
|
276
273
|
|
|
277
|
-
//
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
274
|
+
// --once mode: always isolated (never singleton)
|
|
275
|
+
if (onceFlag) {
|
|
276
|
+
try {
|
|
277
|
+
const server = await createMdprobeServer({
|
|
278
|
+
files: mdFiles,
|
|
279
|
+
port,
|
|
280
|
+
open: false,
|
|
281
|
+
once: true,
|
|
282
|
+
author: currentAuthor,
|
|
283
|
+
})
|
|
284
|
+
console.log(`Server listening at ${server.url}`)
|
|
285
|
+
if (!noOpenFlag) {
|
|
286
|
+
try { await openBrowser(server.url) } catch { /* ignore */ }
|
|
287
|
+
}
|
|
290
288
|
console.log(`Review mode: ${mdFiles.length} file(s)`)
|
|
291
289
|
mdFiles.forEach(f => console.log(` - ${basename(f)}`))
|
|
292
290
|
|
|
293
|
-
// Block until user clicks "Finish Review" in the UI
|
|
294
291
|
const result = await server.finishPromise
|
|
295
292
|
console.log('\nReview complete.')
|
|
296
293
|
if (result.yamlPaths?.length > 0) {
|
|
@@ -300,30 +297,51 @@ async function main() {
|
|
|
300
297
|
}
|
|
301
298
|
await server.close()
|
|
302
299
|
process.exit(0)
|
|
300
|
+
} catch (err) {
|
|
301
|
+
fatal(`Error: ${err.message}`)
|
|
303
302
|
}
|
|
303
|
+
return
|
|
304
|
+
}
|
|
304
305
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
cmd = 'xdg-open'
|
|
322
|
-
args = [server.url]
|
|
306
|
+
// --- Singleton mode: reuse existing server if running ---
|
|
307
|
+
try {
|
|
308
|
+
const existing = await discoverExistingServer()
|
|
309
|
+
|
|
310
|
+
if (existing) {
|
|
311
|
+
console.log(`Found running mdprobe at ${existing.url}`)
|
|
312
|
+
const result = await joinExistingServer(existing.url, mdFiles)
|
|
313
|
+
if (result.ok) {
|
|
314
|
+
console.log(`Added ${mdFiles.length} file(s) to existing server`)
|
|
315
|
+
if (!noOpenFlag) {
|
|
316
|
+
const fileUrl = mdFiles.length === 1
|
|
317
|
+
? `${existing.url}/${basename(mdFiles[0])}`
|
|
318
|
+
: existing.url
|
|
319
|
+
try { await openBrowser(fileUrl) } catch { /* ignore */ }
|
|
320
|
+
}
|
|
321
|
+
process.exit(0)
|
|
323
322
|
}
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
|
|
323
|
+
console.log('Could not join existing server, starting new instance...')
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Start new server
|
|
327
|
+
const server = await createMdprobeServer({
|
|
328
|
+
files: mdFiles,
|
|
329
|
+
port,
|
|
330
|
+
open: false,
|
|
331
|
+
author: currentAuthor,
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
await writeLockFile({
|
|
335
|
+
pid: process.pid,
|
|
336
|
+
port: server.port,
|
|
337
|
+
url: server.url,
|
|
338
|
+
startedAt: new Date().toISOString(),
|
|
339
|
+
})
|
|
340
|
+
registerShutdownHandlers(server)
|
|
341
|
+
|
|
342
|
+
console.log(`Server listening at ${server.url}`)
|
|
343
|
+
if (!noOpenFlag) {
|
|
344
|
+
try { await openBrowser(server.url) } catch { /* ignore */ }
|
|
327
345
|
}
|
|
328
346
|
} catch (err) {
|
|
329
347
|
fatal(`Error: ${err.message}`)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[data-theme=mocha]{--bg-primary: #1e1e2e;--bg-secondary: #181825;--bg-tertiary: #313244;--text-primary: #cdd6f4;--text-secondary: #a6adc8;--text-muted: #6c7086;--border: #45475a;--border-subtle: #313244;--accent: #89b4fa;--accent-hover: #74c7ec;--tag-bug: #f38ba8;--tag-question: #89b4fa;--tag-suggestion: #a6e3a1;--tag-nitpick: #f9e2af;--highlight-open: rgba(137, 180, 250, .2);--highlight-resolved: rgba(166, 227, 161, .15);--status-approved: #a6e3a1;--status-rejected: #f38ba8;--status-pending: #45475a;--scrollbar-thumb: #45475a;--scrollbar-track: #1e1e2e}[data-theme=macchiato]{--bg-primary: #24273a;--bg-secondary: #1e2030;--bg-tertiary: #363a4f;--text-primary: #cad3f5;--text-secondary: #a5adcb;--text-muted: #6e738d;--border: #494d64;--border-subtle: #363a4f;--accent: #8aadf4;--accent-hover: #7dc4e4;--tag-bug: #ed8796;--tag-question: #8aadf4;--tag-suggestion: #a6da95;--tag-nitpick: #eed49f;--highlight-open: rgba(138, 173, 244, .2);--highlight-resolved: rgba(166, 218, 149, .15);--status-approved: #a6da95;--status-rejected: #ed8796;--status-pending: #494d64;--scrollbar-thumb: #494d64;--scrollbar-track: #24273a}[data-theme=frappe]{--bg-primary: #303446;--bg-secondary: #292c3c;--bg-tertiary: #414559;--text-primary: #c6d0f5;--text-secondary: #a5adce;--text-muted: #737994;--border: #51576d;--border-subtle: #414559;--accent: #8caaee;--accent-hover: #85c1dc;--tag-bug: #e78284;--tag-question: #8caaee;--tag-suggestion: #a6d189;--tag-nitpick: #e5c890;--highlight-open: rgba(140, 170, 238, .2);--highlight-resolved: rgba(166, 209, 137, .15);--status-approved: #a6d189;--status-rejected: #e78284;--status-pending: #51576d;--scrollbar-thumb: #51576d;--scrollbar-track: #303446}[data-theme=latte]{--bg-primary: #eff1f5;--bg-secondary: #e6e9ef;--bg-tertiary: #ccd0da;--text-primary: #4c4f69;--text-secondary: #5c5f77;--text-muted: #9ca0b0;--border: #bcc0cc;--border-subtle: #ccd0da;--accent: #1e66f5;--accent-hover: #209fb5;--tag-bug: #d20f39;--tag-question: #1e66f5;--tag-suggestion: #40a02b;--tag-nitpick: #df8e1d;--highlight-open: rgba(30, 102, 245, .15);--highlight-resolved: rgba(64, 160, 43, .1);--status-approved: #40a02b;--status-rejected: #d20f39;--status-pending: #ccd0da;--scrollbar-thumb: #bcc0cc;--scrollbar-track: #eff1f5}[data-theme=light]{--bg-primary: #ffffff;--bg-secondary: #f8f9fa;--bg-tertiary: #e9ecef;--text-primary: #212529;--text-secondary: #495057;--text-muted: #adb5bd;--border: #dee2e6;--border-subtle: #e9ecef;--accent: #0d6efd;--accent-hover: #0b5ed7;--tag-bug: #dc3545;--tag-question: #0d6efd;--tag-suggestion: #198754;--tag-nitpick: #fd7e14;--highlight-open: rgba(13, 110, 253, .12);--highlight-resolved: rgba(25, 135, 84, .08);--status-approved: #198754;--status-rejected: #dc3545;--status-pending: #e9ecef;--scrollbar-thumb: #dee2e6;--scrollbar-track: #ffffff}:root{--panel-collapsed-width: 48px;--panel-width: clamp(240px, 18vw, 380px)}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html,body{height:100%}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}#app{display:grid;grid-template-columns:auto 1fr auto;grid-template-rows:48px auto 1fr 32px;height:100vh;background:var(--bg-primary);color:var(--text-primary)}.header{grid-row:1;grid-column:1 / -1;display:flex;align-items:center;padding:0 16px;background:var(--bg-secondary);border-bottom:1px solid var(--border);gap:12px;z-index:10}.header h1{font-size:14px;font-weight:600;white-space:nowrap}.left-panel{grid-row:3;width:var(--panel-width);transition:width .2s ease;overflow:hidden;background:var(--bg-secondary);border-right:1px solid var(--border);display:flex;flex-direction:column}.left-panel.collapsed{width:var(--panel-collapsed-width)}.left-panel .panel-collapsed-indicator{display:none;width:var(--panel-collapsed-width);align-items:center;justify-content:center;height:100%;cursor:pointer}.left-panel.collapsed .panel-collapsed-indicator{display:flex;flex-direction:column;gap:8px;font-size:12px;color:var(--text-muted)}.left-panel.collapsed .panel-content{display:none}.right-panel{grid-row:3;width:var(--panel-width);transition:width .2s ease;overflow:hidden;background:var(--bg-secondary);border-left:1px solid var(--border);display:flex;flex-direction:column}.right-panel.collapsed{width:var(--panel-collapsed-width)}.right-panel .panel-collapsed-indicator{display:none;width:var(--panel-collapsed-width);align-items:center;justify-content:center;height:100%;cursor:pointer}.right-panel.collapsed .panel-collapsed-indicator{display:flex;flex-direction:column;gap:8px;font-size:12px;color:var(--text-muted)}.right-panel.collapsed .panel-content{display:none}.panel-header{display:flex;align-items:center;justify-content:space-between;padding:12px 12px 8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--text-muted)}.panel-content{flex:1;overflow-y:auto;padding:4px 8px}.content-area-wrapper{grid-row:3;overflow-y:auto;min-width:0}.content-area{padding:32px clamp(24px,3vw,56px);width:100%;line-height:1.7}.annotation-highlight{background:var(--highlight-open);cursor:pointer;border-radius:2px;padding:0 2px;transition:outline-color .15s}.annotation-highlight.resolved{background:var(--highlight-resolved);opacity:.3}.annotation-highlight.selected{outline:2px solid var(--accent);outline-offset:1px}.tag{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;line-height:1.4}.tag-bug{background:var(--tag-bug);color:#fff}.tag-question{background:var(--tag-question);color:#fff}.tag-suggestion{background:var(--tag-suggestion);color:#fff}.tag-nitpick{background:var(--tag-nitpick);color:#fff}.annotation-card{padding:12px;border-radius:8px;background:var(--bg-tertiary);border:1px solid var(--border-subtle);margin-bottom:8px;cursor:pointer;transition:border-color .15s,box-shadow .15s}.annotation-card:hover{border-color:var(--accent)}.annotation-card.selected{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent)}.annotation-card .meta{display:flex;align-items:center;gap:8px;margin-bottom:6px;font-size:12px;color:var(--text-muted)}.annotation-card .quote{font-style:italic;color:var(--text-secondary);border-left:3px solid var(--accent);padding-left:8px;margin:8px 0;font-size:13px}.annotation-card .body{font-size:13px;color:var(--text-primary);line-height:1.5}.section-status{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:500}.section-status.approved{background:var(--status-approved);color:#fff}.section-status.rejected{background:var(--status-rejected);color:#fff}.section-status.pending{background:var(--status-pending);color:var(--text-secondary)}.section-status.indeterminate{background:var(--tag-nitpick);color:var(--bg-primary);opacity:.85}.popover{position:fixed;z-index:101;background:var(--bg-secondary);border:1px solid var(--border);border-radius:10px;box-shadow:0 8px 32px #00000059,0 2px 8px #0003;min-width:320px;display:flex;flex-direction:column}.popover--enter{animation:popover-in .15s ease-out}@keyframes popover-in{0%{opacity:0;transform:translateY(4px) scale(.97)}to{opacity:1;transform:translateY(0) scale(1)}}.popover__header{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-bottom:1px solid var(--border);cursor:grab;-webkit-user-select:none;user-select:none;border-radius:10px 10px 0 0}.popover__header:active{cursor:grabbing}.popover__title{font-size:13px;font-weight:600;color:var(--text-primary);letter-spacing:.01em}.popover__close{background:none;border:none;color:var(--text-muted);font-size:18px;line-height:1;cursor:pointer;padding:2px 6px;border-radius:4px;transition:all .15s}.popover__close:hover{background:var(--bg-tertiary);color:var(--text-primary)}.annotation-form{padding:14px}.annotation-form__quote{font-style:italic;color:var(--text-secondary);border-left:3px solid var(--accent);padding:6px 10px;margin-bottom:12px;font-size:13px;line-height:1.5;max-height:80px;overflow-y:auto;background:#89b4fa0d;border-radius:0 4px 4px 0}.annotation-form__tags{display:flex;gap:6px;margin-bottom:12px;flex-wrap:wrap}.tag-pill{padding:4px 12px;border-radius:20px;border:1px solid transparent;font-size:12px;font-weight:500;cursor:pointer;transition:all .15s;background:var(--bg-tertiary);color:var(--text-secondary);line-height:1.4}.tag-pill:hover{border-color:var(--border)}.tag-pill--active{border-color:currentColor}.tag-pill--question{color:var(--tag-question)}.tag-pill--question.tag-pill--active{background:#89b4fa26}.tag-pill--bug{color:var(--tag-bug)}.tag-pill--bug.tag-pill--active{background:#f38ba826}.tag-pill--suggestion{color:var(--tag-suggestion)}.tag-pill--suggestion.tag-pill--active{background:#a6e3a126}.tag-pill--nitpick{color:var(--tag-nitpick)}.tag-pill--nitpick.tag-pill--active{background:#f9e2af26}.annotation-form textarea{width:100%;min-height:180px;padding:10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-primary);color:var(--text-primary);font-family:inherit;font-size:13px;resize:vertical;line-height:1.5}.annotation-form textarea:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 2px #89b4fa40}.annotation-form__actions{display:flex;align-items:center;gap:8px;margin-top:10px;justify-content:flex-end}.annotation-form__hint{font-size:11px;color:var(--text-muted);margin-right:auto}.btn{padding:6px 14px;border-radius:6px;border:1px solid var(--border);background:var(--bg-tertiary);color:var(--text-primary);cursor:pointer;font-family:inherit;font-size:13px;font-weight:500;transition:all .15s;line-height:1.4;white-space:nowrap}.btn:hover{background:var(--accent);color:#fff;border-color:var(--accent)}.btn:active{transform:scale(.97)}.btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.btn-primary,.btn--primary{background:var(--accent);color:#fff;border-color:var(--accent)}.btn-primary:hover,.btn--primary:hover{background:var(--accent-hover);border-color:var(--accent-hover)}.btn-danger{background:var(--tag-bug);color:#fff;border-color:var(--tag-bug)}.btn-danger:hover{opacity:.9}.btn-sm{padding:3px 8px;font-size:11px}.btn-ghost,.btn--ghost{background:transparent;border-color:transparent;color:var(--text-secondary)}.btn-ghost:hover,.btn--ghost:hover{background:var(--bg-tertiary);color:var(--text-primary);border-color:transparent}.status-bar{grid-row:4;grid-column:1 / -1;display:flex;align-items:center;padding:0 16px;gap:16px;font-size:11px;color:var(--text-muted);background:var(--bg-secondary);border-top:1px solid var(--border);z-index:10}.status-bar .separator{width:1px;height:14px;background:var(--border)}.progress-bar{height:4px;background:var(--bg-tertiary);border-radius:2px;overflow:hidden}.progress-bar .fill{height:100%;background:var(--accent);transition:width .3s ease;border-radius:2px}.toc-item{padding:4px 12px;cursor:pointer;font-size:13px;color:var(--text-secondary);border-radius:4px;transition:background .1s,color .1s;display:flex;align-items:center;gap:4px;line-height:1.4}.toc-item:hover{background:var(--bg-tertiary);color:var(--text-primary)}.toc-item.active{color:var(--accent);background:var(--bg-tertiary)}.toc-item .badge{font-size:10px;background:var(--tag-question);color:#fff;border-radius:8px;padding:1px 5px;margin-left:4px;line-height:1.4}.toc-item.level-2{padding-left:24px}.toc-item.level-3{padding-left:36px}.toc-item.level-4{padding-left:48px}.toc-item.level-5{padding-left:60px}.toc-item.level-6{padding-left:72px}.toc-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}.toc-dot.dot-approved{background:var(--status-approved)}.toc-dot.dot-rejected{background:var(--status-rejected)}.section-status-label.approved{color:var(--status-approved)}.section-status-label.rejected{color:var(--status-rejected)}.file-item{padding:6px 12px;cursor:pointer;font-size:13px;color:var(--text-secondary);border-radius:4px;display:flex;align-items:center;gap:8px;transition:background .1s,color .1s}.file-item:hover{background:var(--bg-tertiary);color:var(--text-primary)}.file-item.active{background:var(--bg-tertiary);color:var(--accent);font-weight:500}.file-item .icon{font-size:14px;flex-shrink:0}.file-item .name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.reply{padding:8px 12px;border-left:2px solid var(--border);margin-left:12px;margin-top:8px}.reply .author{font-weight:600;font-size:12px;color:var(--text-secondary)}.reply .text{font-size:13px;margin-top:2px;color:var(--text-primary);line-height:1.5}.reply .timestamp{font-size:11px;color:var(--text-muted);margin-left:8px;font-weight:400}.reply-input{display:flex;gap:8px;margin-top:8px;margin-left:12px}.reply-input input{flex:1;padding:6px 8px;border:1px solid var(--border);border-radius:6px;background:var(--bg-primary);color:var(--text-primary);font-family:inherit;font-size:13px}.reply-input input:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 2px #89b4fa40}.content-area h1{font-size:2em;margin:24px 0 16px;border-bottom:1px solid var(--border);padding-bottom:8px;font-weight:700}.content-area h2{font-size:1.5em;margin:20px 0 12px;font-weight:600}.content-area h3{font-size:1.2em;margin:16px 0 8px;font-weight:600}.content-area h4{font-size:1.05em;margin:14px 0 6px;font-weight:600}.content-area p{margin:0 0 12px}.content-area ul,.content-area ol{margin:0 0 12px;padding-left:24px}.content-area li{margin-bottom:4px}.content-area li>ul,.content-area li>ol{margin-bottom:0}.content-area code{background:var(--bg-tertiary);padding:2px 6px;border-radius:3px;font-size:.9em;font-family:SF Mono,Cascadia Code,Fira Code,Consolas,monospace}.content-area pre{background:var(--bg-tertiary);padding:16px;border-radius:8px;overflow-x:auto;margin:0 0 16px;line-height:1.5}.content-area pre code{background:none;padding:0;border-radius:0;font-size:13px}.content-area blockquote{border-left:4px solid var(--accent);padding:8px 16px;margin:0 0 12px;color:var(--text-secondary);background:var(--bg-tertiary);border-radius:0 8px 8px 0}.content-area blockquote p:last-child{margin-bottom:0}.content-area table{border-collapse:collapse;width:100%;margin:0 0 16px}.content-area th,.content-area td{border:1px solid var(--border);padding:8px 12px;text-align:left}.content-area th{background:var(--bg-secondary);font-weight:600}.content-area img{max-width:100%;border-radius:8px}.content-area a{color:var(--accent);text-decoration:none}.content-area a:hover{text-decoration:underline}.content-area hr{border:none;height:1px;background:var(--border);margin:24px 0}.shortcut-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background:#00000080;z-index:199}.shortcut-modal{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:200;background:var(--bg-secondary);border:1px solid var(--border);border-radius:12px;padding:24px;box-shadow:0 8px 32px #0000004d;min-width:400px;max-width:520px;max-height:80vh;overflow-y:auto}.shortcut-modal h2{font-size:16px;font-weight:600;margin-bottom:16px}.shortcut-group{margin-bottom:16px}.shortcut-group-title{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--text-muted);margin-bottom:8px}.shortcut-row{display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid var(--border-subtle);font-size:13px}.shortcut-row:last-child{border-bottom:none}.shortcut-label{color:var(--text-secondary)}.shortcut-key{display:inline-block;padding:2px 8px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:4px;font-family:SF Mono,Cascadia Code,Consolas,monospace;font-size:12px;min-width:24px;text-align:center;line-height:1.5}.theme-picker{display:flex;gap:6px;align-items:center}.theme-swatch{width:24px;height:24px;border-radius:50%;cursor:pointer;border:2px solid transparent;transition:border-color .15s,transform .15s;flex-shrink:0}.theme-swatch:hover,.theme-swatch.active{border-color:var(--accent);transform:scale(1.1)}.export-option{display:block;width:100%;padding:8px 12px;text-align:left;border:none;background:none;color:var(--text-primary);cursor:pointer;font-size:13px;font-family:inherit}.export-option:hover{background:var(--bg-tertiary)}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:var(--scrollbar-track)}::-webkit-scrollbar-thumb{background:var(--scrollbar-thumb);border-radius:4px}::-webkit-scrollbar-thumb:hover{background:var(--text-muted)}*{scrollbar-width:thin;scrollbar-color:var(--scrollbar-thumb) var(--scrollbar-track)}.drift-banner{grid-row:2;grid-column:1 / -1;padding:8px 16px;background:var(--tag-question);color:#fff;font-size:13px;display:flex;align-items:center;gap:8px}.drift-banner .dismiss{margin-left:auto;cursor:pointer;opacity:.8;transition:opacity .15s}.drift-banner .dismiss:hover{opacity:1}.orphaned-section{border-top:1px solid var(--border);padding-top:8px;margin-top:8px}.orphaned-section-header{display:flex;align-items:center;gap:6px;cursor:pointer;padding:6px 4px;font-size:12px;font-weight:500;color:var(--tag-bug)}.orphaned-section-header:hover{opacity:.8}.annotation-card.orphaned{opacity:.65;border-left-color:var(--tag-bug);border-left-style:dashed}.annotation-card.orphaned .quote{text-decoration:line-through}.flex{display:flex}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-4{gap:4px}.gap-8{gap:8px}.gap-12{gap:12px}.ml-auto{margin-left:auto}.text-muted{color:var(--text-muted)}.text-sm{font-size:12px}.text-xs{font-size:11px}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.content-area .hljs-keyword,.content-area .hljs-selector-tag,.content-area .hljs-built_in,.content-area .hljs-type{color:var(--tag-bug)}.content-area .hljs-string,.content-area .hljs-attr,.content-area .hljs-symbol,.content-area .hljs-template-tag,.content-area .hljs-template-variable{color:var(--tag-suggestion)}.content-area .hljs-number,.content-area .hljs-literal,.content-area .hljs-regexp{color:var(--tag-nitpick)}.content-area .hljs-title,.content-area .hljs-title.function_,.content-area .hljs-title.class_{color:var(--accent)}.content-area .hljs-comment,.content-area .hljs-doctag{color:var(--text-muted);font-style:italic}.content-area .hljs-variable,.content-area .hljs-variable.language_,.content-area .hljs-params{color:var(--text-primary)}.content-area .hljs-meta,.content-area .hljs-meta .hljs-keyword{color:var(--tag-question)}.content-area .hljs-subst{color:var(--text-secondary)}.content-area .hljs-addition{color:var(--tag-suggestion);background:#a6e3a11a}.content-area .hljs-deletion{color:var(--tag-bug);background:#f38ba81a}.content-area .mermaid{background:var(--bg-tertiary);border:1px dashed var(--border);border-radius:8px;padding:16px;text-align:center;font-family:inherit;white-space:pre-wrap;min-height:60px}.content-area .mermaid svg{max-width:100%}.content-area .math-display{display:block;text-align:center;padding:12px 16px;margin:12px 0;background:var(--bg-tertiary);border-radius:6px;font-family:KaTeX_Main,Times New Roman,serif;font-size:1.1em;overflow-x:auto}.content-area .math-inline{font-family:KaTeX_Main,Times New Roman,serif;padding:1px 4px;background:var(--bg-tertiary);border-radius:3px}@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important;scroll-behavior:auto!important}}
|