docutrack 0.1.0 → 0.1.6
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/LICENSE +21 -0
- package/README.md +114 -65
- package/bin/docutrack.js +73 -67
- package/package.json +40 -38
- package/src/commands/init.js +245 -80
- package/src/commands/install-global.js +93 -0
- package/src/commands/scan.js +3 -15
- package/src/commands/setup.js +126 -0
- package/src/hooks/global-on-stop.js +18 -0
- package/src/hooks/global-post-tool-use.js +25 -0
- package/src/utils/daemon.js +48 -0
- package/src/utils/queue.js +11 -10
- package/src/utils/queue.test.js +54 -0
- package/src/viewer/index.html +1545 -1411
- package/src/viewer/server.js +383 -652
- package/src/viewer/server.test.js +50 -0
- package/templates/agents/documentalista.md +47 -28
- package/templates/claude-snippet.md +53 -39
- package/templates/hooks/on-stop.js +59 -11
- package/templates/hooks/post-tool-use.js +12 -8
package/src/viewer/index.html
CHANGED
|
@@ -1,1411 +1,1545 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en" data-theme="light">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>DocuTrack</title>
|
|
7
|
-
<style>
|
|
8
|
-
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
-
|
|
10
|
-
:root {
|
|
11
|
-
--bg: #ffffff;
|
|
12
|
-
--bg-sidebar: #f8fafc;
|
|
13
|
-
--bg-hover: #f1f5f9;
|
|
14
|
-
--bg-active: #eef2ff;
|
|
15
|
-
--border: #e2e8f0;
|
|
16
|
-
--text: #0f172a;
|
|
17
|
-
--text-muted: #64748b;
|
|
18
|
-
--text-sidebar: #374151;
|
|
19
|
-
--accent: #6366f1;
|
|
20
|
-
--accent-hover: #4f46e5;
|
|
21
|
-
--accent-light: #eef2ff;
|
|
22
|
-
--code-bg: #f1f5f9;
|
|
23
|
-
--code-text: #1e293b;
|
|
24
|
-
--green: #10b981; --green-bg: #dcfce7; --green-text: #166534;
|
|
25
|
-
--blue-bg: #dbeafe; --blue-text: #1e40af;
|
|
26
|
-
--yellow: #f59e0b; --yellow-bg: #fef3c7; --yellow-text: #92400e;
|
|
27
|
-
--orange-bg: #ffedd5; --orange-text: #9a3412;
|
|
28
|
-
--purple-bg: #ede9fe; --purple-text: #6d28d9;
|
|
29
|
-
--red: #ef4444; --red-bg: #fee2e2; --red-text: #991b1b;
|
|
30
|
-
--sidebar-w: 272px;
|
|
31
|
-
--header-h: 56px;
|
|
32
|
-
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
33
|
-
--font-mono: "Cascadia Code", "Fira Code", Consolas, monospace;
|
|
34
|
-
--radius: 6px;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
[data-theme="dark"] {
|
|
38
|
-
--bg: #0f172a; --bg-sidebar: #1e293b; --bg-hover: #334155; --bg-active: #312e81;
|
|
39
|
-
--border: #334155; --text: #f1f5f9; --text-muted: #94a3b8; --text-sidebar: #cbd5e1;
|
|
40
|
-
--accent: #818cf8; --accent-hover: #a5b4fc; --accent-light: #1e1b4b;
|
|
41
|
-
--code-bg: #1e293b; --code-text: #e2e8f0;
|
|
42
|
-
--green-bg: #14532d; --green-text: #86efac;
|
|
43
|
-
--blue-bg: #1e3a5f; --blue-text: #93c5fd;
|
|
44
|
-
--yellow-bg: #78350f; --yellow-text: #fde68a;
|
|
45
|
-
--orange-bg: #7c2d12; --orange-text: #fed7aa;
|
|
46
|
-
--purple-bg: #3b0764; --purple-text: #d8b4fe;
|
|
47
|
-
--red-bg: #7f1d1d; --red-text: #fca5a5;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
html, body { height: 100%; font-family: var(--font); color: var(--text); background: var(--bg); font-size: 15px; line-height: 1.6; }
|
|
51
|
-
|
|
52
|
-
/* ── Layout ── */
|
|
53
|
-
#app { display: grid; grid-template-columns: var(--sidebar-w) 1fr; grid-template-rows: var(--header-h) 1fr; height: 100vh; }
|
|
54
|
-
|
|
55
|
-
/* ── Header ── */
|
|
56
|
-
#header { grid-column: 1 / -1; background: var(--bg); border-bottom: 1px solid var(--border); display: flex; align-items: center; padding: 0 20px; gap: 16px; position: sticky; top: 0; z-index: 100; }
|
|
57
|
-
.logo { display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: 15px; color: var(--text); text-decoration: none; white-space: nowrap; cursor: pointer; }
|
|
58
|
-
.logo-icon { width: 28px; height: 28px; background: var(--accent); border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 14px; flex-shrink: 0; }
|
|
59
|
-
.header-sep { width: 1px; height: 20px; background: var(--border); flex-shrink: 0; }
|
|
60
|
-
#project-name { font-size: 13px; color: var(--text-muted); white-space: nowrap; }
|
|
61
|
-
.header-right { margin-left: auto; display: flex; align-items: center; gap: 8px; }
|
|
62
|
-
#coverage-badge { display: flex; align-items: center; gap: 6px; background: var(--bg-sidebar); border: 1px solid var(--border); border-radius: 20px; padding: 3px 10px; font-size: 12px; font-weight: 600; color: var(--text-muted); white-space: nowrap; }
|
|
63
|
-
.coverage-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--green); flex-shrink: 0; }
|
|
64
|
-
#theme-toggle { width: 32px; height: 32px; border-radius: var(--radius); border: 1px solid var(--border); background: var(--bg); cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 16px; }
|
|
65
|
-
#theme-toggle:hover { background: var(--bg-hover); }
|
|
66
|
-
.live-pill { display: flex; align-items: center; gap: 4px; font-size: 11px; color: var(--text-muted); }
|
|
67
|
-
.live-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--green); animation: pulse 2s infinite; flex-shrink: 0; }
|
|
68
|
-
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} }
|
|
69
|
-
|
|
70
|
-
/* ── Search ── */
|
|
71
|
-
#search-wrap { flex: 1; max-width: 380px; position: relative; }
|
|
72
|
-
#search { width: 100%; padding: 6px 12px 6px 34px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg-sidebar); color: var(--text); font-size: 13px; outline: none; }
|
|
73
|
-
#search:focus { border-color: var(--accent); background: var(--bg); }
|
|
74
|
-
#search::placeholder { color: var(--text-muted); }
|
|
75
|
-
.search-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); font-size: 13px; pointer-events: none; color: var(--text-muted); }
|
|
76
|
-
.search-kbd { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); font-size: 10px; color: var(--text-muted); background: var(--border); padding: 1px 5px; border-radius: 3px; pointer-events: none; }
|
|
77
|
-
|
|
78
|
-
/* ── Sidebar ── */
|
|
79
|
-
#sidebar { background: var(--bg-sidebar); border-right: 1px solid var(--border); overflow-y: auto; padding: 16px 0 80px; }
|
|
80
|
-
.nav-section { margin-bottom: 4px; }
|
|
81
|
-
.nav-section-title { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); padding: 8px 20px 4px; }
|
|
82
|
-
.nav-item { display: flex; align-items: center; gap: 8px; padding: 6px 20px; font-size: 13px; color: var(--text-sidebar); cursor: pointer; border-left: 2px solid transparent; }
|
|
83
|
-
.nav-item:hover { background: var(--bg-hover); color: var(--text); }
|
|
84
|
-
.nav-item.active { background: var(--bg-active); color: var(--accent); border-left-color: var(--accent); font-weight: 500; }
|
|
85
|
-
.nav-icon { font-size: 14px; width: 16px; text-align: center; flex-shrink: 0; }
|
|
86
|
-
.nav-empty { font-size: 12px; color: var(--text-muted); padding: 4px 20px 8px; font-style: italic; }
|
|
87
|
-
.nav-badge { margin-left: auto; font-size: 11px; background: var(--accent-light); color: var(--accent); border-radius: 10px; padding: 0 6px; font-weight: 600; }
|
|
88
|
-
.nav-stale { margin-left: auto; font-size: 10px; background: var(--yellow-bg); color: var(--yellow-text); border-radius: 10px; padding: 0 5px; font-weight: 700; }
|
|
89
|
-
.stale-banner { background: var(--yellow-bg); border-top: 1px solid var(--yellow); padding: 8px 20px; font-size: 12px; color: var(--yellow-text); }
|
|
90
|
-
|
|
91
|
-
/* Coverage bar */
|
|
92
|
-
#coverage-section { margin-top: 16px; padding: 0 16px; }
|
|
93
|
-
.coverage-label { font-size: 11px; color: var(--text-muted); margin-bottom: 6px; display: flex; justify-content: space-between; }
|
|
94
|
-
.coverage-bar { height: 4px; background: var(--border); border-radius: 2px; overflow: hidden; }
|
|
95
|
-
.coverage-fill { height: 100%; background: var(--green); border-radius: 2px; transition: width .5s; }
|
|
96
|
-
.coverage-fill.warn { background: var(--yellow); }
|
|
97
|
-
.coverage-fill.bad { background: var(--red); }
|
|
98
|
-
|
|
99
|
-
/* ── Main ── */
|
|
100
|
-
#main { overflow-y: auto; display: flex; justify-content: center; align-items: flex-start; min-height: 0; }
|
|
101
|
-
#doc-wrap { width: 100%; max-width: 800px; padding: 40px 48px 120px; }
|
|
102
|
-
|
|
103
|
-
/* Pending banner */
|
|
104
|
-
#pending-banner { display: none; background: var(--yellow-bg); border: 1px solid var(--yellow); border-radius: var(--radius); padding: 10px 16px; margin-bottom: 24px; font-size: 13px; color: var(--yellow-text); align-items: center; gap: 8px; }
|
|
105
|
-
#pending-banner.show { display: flex; }
|
|
106
|
-
|
|
107
|
-
/* ── Markdown ── */
|
|
108
|
-
#doc-content h1 { font-size: 28px; font-weight: 800; margin-bottom: 8px; padding-bottom: 12px; border-bottom: 1px solid var(--border); }
|
|
109
|
-
#doc-content h2 { font-size: 20px; font-weight: 700; margin-top: 36px; margin-bottom: 12px; }
|
|
110
|
-
#doc-content h3 { font-size: 16px; font-weight: 600; margin-top: 24px; margin-bottom: 8px; }
|
|
111
|
-
#doc-content h4 { font-size: 14px; font-weight: 600; margin-top: 16px; margin-bottom: 6px; text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted); }
|
|
112
|
-
#doc-content p { margin-bottom: 14px; }
|
|
113
|
-
#doc-content ul, #doc-content ol { margin-bottom: 14px; padding-left: 24px; }
|
|
114
|
-
#doc-content li { margin-bottom: 4px; }
|
|
115
|
-
#doc-content a { color: var(--accent); text-decoration: none; }
|
|
116
|
-
#doc-content a:hover { text-decoration: underline; }
|
|
117
|
-
#doc-content blockquote { border-left: 3px solid var(--accent); padding: 8px 16px; margin: 16px 0; color: var(--text-muted); background: var(--accent-light); border-radius: 0 var(--radius) var(--radius) 0; }
|
|
118
|
-
#doc-content blockquote p { margin: 0; }
|
|
119
|
-
#doc-content code { font-family: var(--font-mono); font-size: 13px; background: var(--code-bg); color: var(--code-text); padding: 2px 6px; border-radius: 4px; }
|
|
120
|
-
#doc-content pre { background: var(--code-bg); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; overflow-x: auto; margin: 16px 0; }
|
|
121
|
-
#doc-content pre code { background: none; padding: 0; font-size: 13px; line-height: 1.6; }
|
|
122
|
-
#doc-content table { width: 100%; border-collapse: collapse; margin: 16px 0; font-size: 14px; }
|
|
123
|
-
#doc-content th { background: var(--bg-sidebar); font-weight: 600; text-align: left; padding: 10px 14px; border: 1px solid var(--border); }
|
|
124
|
-
#doc-content td { padding: 9px 14px; border: 1px solid var(--border); }
|
|
125
|
-
#doc-content tr:nth-child(even) td { background: var(--bg-sidebar); }
|
|
126
|
-
#doc-content hr { border: none; border-top: 1px solid var(--border); margin: 28px 0; }
|
|
127
|
-
.mermaid-wrap { background: var(--bg-sidebar); border: 1px solid var(--border); border-radius: var(--radius); padding: 24px; margin: 20px 0; overflow-x: auto; text-align: center; }
|
|
128
|
-
|
|
129
|
-
/* ── API Explorer ── */
|
|
130
|
-
.api-explorer { width: 100%; }
|
|
131
|
-
.api-explorer-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; margin-bottom: 28px; padding-bottom: 20px; border-bottom: 1px solid var(--border); flex-wrap: wrap; }
|
|
132
|
-
.base-url-wrap { display: flex; flex-direction: column; gap: 4px; min-width: 260px; }
|
|
133
|
-
.base-url-label { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); }
|
|
134
|
-
.base-url-row { display: flex; gap: 6px; align-items: center; }
|
|
135
|
-
.base-url-input { padding: 6px 10px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); color: var(--text); font-size: 13px; font-family: var(--font-mono); width: 260px; outline: none; }
|
|
136
|
-
.base-url-input:focus { border-color: var(--accent); }
|
|
137
|
-
.auth-toggle { padding: 6px 12px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); color: var(--text-muted); font-size: 12px; cursor: pointer; white-space: nowrap; }
|
|
138
|
-
.auth-toggle:hover { background: var(--bg-hover); }
|
|
139
|
-
.auth-row { margin-top: 8px; display: none; gap: 6px; align-items: center; }
|
|
140
|
-
.auth-row.show { display: flex; }
|
|
141
|
-
.auth-input { flex: 1; padding: 6px 10px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); color: var(--text); font-size: 13px; font-family: var(--font-mono); outline: none; }
|
|
142
|
-
.auth-input:focus { border-color: var(--accent); }
|
|
143
|
-
.auth-label { font-size: 12px; color: var(--text-muted); white-space: nowrap; }
|
|
144
|
-
|
|
145
|
-
.api-group { margin-bottom: 24px; }
|
|
146
|
-
.api-group-title { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; color: var(--text-muted); margin-bottom: 8px; padding: 0 2px; }
|
|
147
|
-
|
|
148
|
-
.endpoint-card { border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 6px; overflow: hidden; transition: box-shadow .15s; }
|
|
149
|
-
.endpoint-card.open { box-shadow: 0 2px 8px rgba(0,0,0,.06); }
|
|
150
|
-
.endpoint-header { display: flex; align-items: center; gap: 12px; padding: 11px 16px; cursor: pointer; background: var(--bg); user-select: none; }
|
|
151
|
-
.endpoint-header:hover { background: var(--bg-hover); }
|
|
152
|
-
.endpoint-path { font-family: var(--font-mono); font-size: 13px; font-weight: 500; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
153
|
-
.endpoint-summary { font-size: 13px; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 280px; }
|
|
154
|
-
.endpoint-chevron { color: var(--text-muted); font-size: 16px; transition: transform .2s; flex-shrink: 0; }
|
|
155
|
-
.endpoint-card.open .endpoint-chevron { transform: rotate(90deg); }
|
|
156
|
-
.endpoint-body { display: none; border-top: 1px solid var(--border); background: var(--bg); }
|
|
157
|
-
.endpoint-card.open .endpoint-body { display: block; }
|
|
158
|
-
.endpoint-section { padding: 16px 20px; border-bottom: 1px solid var(--border); }
|
|
159
|
-
.endpoint-section:last-child { border-bottom: none; }
|
|
160
|
-
.section-title { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); margin-bottom: 12px; }
|
|
161
|
-
|
|
162
|
-
/* Method badges */
|
|
163
|
-
.method-badge { display: inline-flex; align-items: center; padding: 3px 8px; border-radius: 4px; font-size: 11px; font-weight: 700; letter-spacing: .05em; font-family: var(--font-mono); flex-shrink: 0; min-width: 54px; justify-content: center; }
|
|
164
|
-
.badge-get { background: var(--green-bg); color: var(--green-text); }
|
|
165
|
-
.badge-post { background: var(--blue-bg); color: var(--blue-text); }
|
|
166
|
-
.badge-put { background: var(--orange-bg); color: var(--orange-text); }
|
|
167
|
-
.badge-patch { background: var(--purple-bg); color: var(--purple-text); }
|
|
168
|
-
.badge-delete { background: var(--red-bg); color: var(--red-text); }
|
|
169
|
-
.badge-options,.badge-head { background: var(--code-bg); color: var(--text-muted); }
|
|
170
|
-
|
|
171
|
-
/* Params table */
|
|
172
|
-
.params-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
173
|
-
.params-table th { font-size: 11px; font-weight: 600; text-align: left; padding: 6px 10px; background: var(--bg-sidebar); border: 1px solid var(--border); color: var(--text-muted); }
|
|
174
|
-
.params-table td { padding: 7px 10px; border: 1px solid var(--border); font-family: var(--font-mono); font-size: 12px; }
|
|
175
|
-
.param-required { background: var(--red-bg); color: var(--red-text); padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 700; font-family: var(--font); }
|
|
176
|
-
.param-optional { background: var(--code-bg); color: var(--text-muted); padding: 1px 6px; border-radius: 3px; font-size: 10px; font-family: var(--font); }
|
|
177
|
-
|
|
178
|
-
/* Try it out */
|
|
179
|
-
.try-section { padding: 16px 20px; background: var(--bg-sidebar); }
|
|
180
|
-
.try-btn { display: inline-flex; align-items: center; gap: 6px; padding: 7px 16px; background: var(--accent); color: white; border: none; border-radius: var(--radius); font-size: 13px; font-weight: 600; cursor: pointer; }
|
|
181
|
-
.try-btn:hover { background: var(--accent-hover); }
|
|
182
|
-
.try-btn:disabled { opacity: .5; cursor: not-allowed; }
|
|
183
|
-
.try-form { margin-top: 14px; display: none; }
|
|
184
|
-
.try-form.show { display: block; }
|
|
185
|
-
.try-field { margin-bottom: 10px; }
|
|
186
|
-
.try-field label { display: block; font-size: 12px; font-weight: 600; margin-bottom: 4px; color: var(--text-muted); font-family: var(--font-mono); }
|
|
187
|
-
.try-field input, .try-field textarea { width: 100%; padding: 7px 10px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); color: var(--text); font-size: 13px; font-family: var(--font-mono); outline: none; }
|
|
188
|
-
.try-field input:focus, .try-field textarea:focus { border-color: var(--accent); }
|
|
189
|
-
.try-field textarea { min-height: 90px; resize: vertical; }
|
|
190
|
-
.try-actions { display: flex; gap: 8px; margin-top: 12px; }
|
|
191
|
-
.execute-btn { display: inline-flex; align-items: center; gap: 6px; padding: 8px 20px; background: var(--accent); color: white; border: none; border-radius: var(--radius); font-size: 13px; font-weight: 600; cursor: pointer; }
|
|
192
|
-
.execute-btn:hover { background: var(--accent-hover); }
|
|
193
|
-
.execute-btn:disabled { opacity: .5; cursor: not-allowed; }
|
|
194
|
-
.cancel-try-btn { padding: 8px 14px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); color: var(--text-muted); font-size: 13px; cursor: pointer; }
|
|
195
|
-
.cancel-try-btn:hover { background: var(--bg-hover); }
|
|
196
|
-
|
|
197
|
-
/* Response panel */
|
|
198
|
-
.response-panel { margin-top: 14px; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
|
|
199
|
-
.response-bar { display: flex; align-items: center; gap: 10px; padding: 8px 14px; background: var(--bg-sidebar); border-bottom: 1px solid var(--border); font-size: 12px; }
|
|
200
|
-
.status-chip { font-size: 12px; font-weight: 700; font-family: var(--font-mono); padding: 2px 8px; border-radius: 4px; }
|
|
201
|
-
.status-2xx { background: var(--green-bg); color: var(--green-text); }
|
|
202
|
-
.status-3xx { background: var(--blue-bg); color: var(--blue-text); }
|
|
203
|
-
.status-4xx { background: var(--yellow-bg); color: var(--yellow-text); }
|
|
204
|
-
.status-5xx { background: var(--red-bg); color: var(--red-text); }
|
|
205
|
-
.response-time { color: var(--text-muted); margin-left: auto; }
|
|
206
|
-
.response-body { padding: 14px; font-family: var(--font-mono); font-size: 12px; line-height: 1.6; overflow-x: auto; max-height: 360px; overflow-y: auto; white-space: pre; background: var(--bg); }
|
|
207
|
-
.response-headers { padding: 10px 14px; border-bottom: 1px solid var(--border); font-size: 12px; color: var(--text-muted); font-family: var(--font-mono); }
|
|
208
|
-
.cors-warn { margin-top: 10px; padding: 8px 12px; background: var(--yellow-bg); border-radius: var(--radius); font-size: 12px; color: var(--yellow-text); }
|
|
209
|
-
|
|
210
|
-
/* Welcome / empty states */
|
|
211
|
-
.empty-state { text-align: center; padding: 60px 40px; }
|
|
212
|
-
.empty-icon { font-size: 48px; margin-bottom: 16px; }
|
|
213
|
-
.empty-state h2 { font-size: 22px; font-weight: 700; margin-bottom: 8px; }
|
|
214
|
-
.empty-state p { color: var(--text-muted); max-width: 380px; margin: 0 auto 16px; }
|
|
215
|
-
.empty-state code { background: var(--code-bg); padding: 4px 10px; border-radius: var(--radius); font-family: var(--font-mono); font-size: 13px; }
|
|
216
|
-
|
|
217
|
-
/* Bootstrap panel */
|
|
218
|
-
.bootstrap-wrap { max-width: 580px; margin: 0 auto; padding: 48px 0; }
|
|
219
|
-
.bootstrap-hero { text-align: center; margin-bottom: 40px; }
|
|
220
|
-
.bootstrap-hero-icon { font-size: 52px; margin-bottom: 16px; }
|
|
221
|
-
.bootstrap-hero h1 { font-size: 26px; font-weight: 800; margin-bottom: 8px; }
|
|
222
|
-
.bootstrap-hero p { color: var(--text-muted); font-size: 15px; max-width: 420px; margin: 0 auto; }
|
|
223
|
-
.bootstrap-steps { display: flex; flex-direction: column; gap: 12px; margin-bottom: 32px; }
|
|
224
|
-
.bootstrap-step { display: flex; align-items: flex-start; gap: 14px; padding: 16px 18px; background: var(--bg-sidebar); border: 1px solid var(--border); border-radius: var(--radius); }
|
|
225
|
-
.bootstrap-step.done { border-color: var(--green); background: var(--green-bg); }
|
|
226
|
-
.bootstrap-step.active { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-light); }
|
|
227
|
-
.step-num { width: 28px; height: 28px; border-radius: 50%; background: var(--accent); color: white; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 700; flex-shrink: 0; margin-top: 1px; }
|
|
228
|
-
.bootstrap-step.done .step-num { background: var(--green); }
|
|
229
|
-
.step-content { flex: 1; min-width: 0; }
|
|
230
|
-
.step-title { font-size: 14px; font-weight: 600; margin-bottom: 3px; }
|
|
231
|
-
.step-desc { font-size: 13px; color: var(--text-muted); }
|
|
232
|
-
.step-desc code { font-family: var(--font-mono); background: var(--code-bg); padding: 1px 5px; border-radius: 3px; font-size: 12px; }
|
|
233
|
-
.step-check { font-size: 18px; flex-shrink: 0; margin-top: 4px; }
|
|
234
|
-
.bootstrap-btn { width: 100%; padding: 14px 24px; background: var(--accent); color: white; border: none; border-radius: var(--radius); font-size: 15px; font-weight: 700; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 10px; transition: background .15s; }
|
|
235
|
-
.bootstrap-btn:hover { background: var(--accent-hover); }
|
|
236
|
-
.bootstrap-btn:disabled { opacity: .6; cursor: not-allowed; }
|
|
237
|
-
.bootstrap-result { margin-top: 20px; padding: 16px 18px; background: var(--green-bg); border: 1px solid var(--green); border-radius: var(--radius); }
|
|
238
|
-
.bootstrap-result-title { font-weight: 700; color: var(--green-text); margin-bottom: 8px; font-size: 14px; }
|
|
239
|
-
.bootstrap-result-files { font-family: var(--font-mono); font-size: 12px; color: var(--green-text); line-height: 1.8; }
|
|
240
|
-
.bootstrap-claude { margin-top: 12px; padding: 14px 16px; background: var(--accent-light); border: 1px solid var(--accent); border-radius: var(--radius); }
|
|
241
|
-
.bootstrap-claude-label { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--accent); margin-bottom: 8px; }
|
|
242
|
-
.bootstrap-claude-cmd { font-family: var(--font-mono); font-size: 13px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 14px; cursor: pointer; user-select: all; color: var(--text); }
|
|
243
|
-
.bootstrap-claude-cmd:hover { border-color: var(--accent); }
|
|
244
|
-
.copy-hint { font-size: 11px; color: var(--text-muted); margin-top: 6px; }
|
|
245
|
-
|
|
246
|
-
/* ── Health Check ── */
|
|
247
|
-
.health-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 32px; padding-bottom: 20px; border-bottom: 1px solid var(--border); }
|
|
248
|
-
.health-title { font-size: 26px; font-weight: 800; margin-bottom: 4px; }
|
|
249
|
-
.health-subtitle { font-size: 13px; color: var(--text-muted); }
|
|
250
|
-
.health-overall { display: flex; align-items: center; gap: 8px; padding: 8px 16px; border-radius: 20px; font-size: 13px; font-weight: 700; }
|
|
251
|
-
.health-ok { background: var(--green-bg); color: var(--green-text); }
|
|
252
|
-
.health-warn { background: var(--yellow-bg); color: var(--yellow-text); }
|
|
253
|
-
.health-crit { background: var(--red-bg); color: var(--red-text); }
|
|
254
|
-
.health-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin-bottom: 32px; }
|
|
255
|
-
.health-card { background: var(--bg-sidebar); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px 18px; }
|
|
256
|
-
.health-card-val { font-size: 32px; font-weight: 800; line-height: 1; margin-bottom: 4px; }
|
|
257
|
-
.health-card-label { font-size: 12px; color: var(--text-muted); font-weight: 500; }
|
|
258
|
-
.health-card.issue .health-card-val { color: var(--red); }
|
|
259
|
-
.health-card.warn-card .health-card-val { color: var(--yellow); }
|
|
260
|
-
.health-card.ok .health-card-val { color: var(--green); }
|
|
261
|
-
.health-section-title { font-size: 14px; font-weight: 700; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
|
|
262
|
-
.health-section { margin-bottom: 28px; }
|
|
263
|
-
.drift-row { display: flex; align-items: flex-start; gap: 12px; padding: 12px 14px; border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 6px; background: var(--bg); }
|
|
264
|
-
.drift-sev { font-size: 10px; font-weight: 800; padding: 2px 7px; border-radius: 3px; flex-shrink: 0; margin-top: 2px; }
|
|
265
|
-
.drift-sev.high { background: var(--red-bg); color: var(--red-text); }
|
|
266
|
-
.drift-sev.medium { background: var(--yellow-bg); color: var(--yellow-text); }
|
|
267
|
-
.drift-sev.low { background: var(--code-bg); color: var(--text-muted); }
|
|
268
|
-
.drift-module { font-weight: 600; font-size: 13px; }
|
|
269
|
-
.drift-detail { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
|
|
270
|
-
.drift-detail span { font-family: var(--font-mono); background: var(--code-bg); padding: 0 4px; border-radius: 3px; font-size: 11px; margin-right: 3px; }
|
|
271
|
-
.complexity-row { display: flex; align-items: center; gap: 10px; padding: 10px 14px; border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 6px; background: var(--bg); }
|
|
272
|
-
.complexity-score { width: 38px; height: 38px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 800; flex-shrink: 0; }
|
|
273
|
-
.score-bad { background: var(--red-bg); color: var(--red-text); }
|
|
274
|
-
.score-warn { background: var(--yellow-bg); color: var(--yellow-text); }
|
|
275
|
-
.score-ok { background: var(--green-bg); color: var(--green-text); }
|
|
276
|
-
.complexity-file { font-family: var(--font-mono); font-size: 12px; font-weight: 500; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
277
|
-
.complexity-tags { display: flex; gap: 6px; flex-shrink: 0; flex-wrap: wrap; }
|
|
278
|
-
.complexity-tag { font-size: 11px; background: var(--code-bg); color: var(--text-muted); padding: 1px 7px; border-radius: 3px; }
|
|
279
|
-
.complexity-tag.crit { background: var(--red-bg); color: var(--red-text); }
|
|
280
|
-
.health-empty { text-align: center; padding: 20px; color: var(--text-muted); font-size: 13px; }
|
|
281
|
-
.health-refresh { padding: 7px 14px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); color: var(--text-muted); font-size: 12px; cursor: pointer; }
|
|
282
|
-
.health-refresh:hover { background: var(--bg-hover); }
|
|
283
|
-
|
|
284
|
-
/* Misc */
|
|
285
|
-
.spinner { display: inline-block; width: 16px; height: 16px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin .6s linear infinite; }
|
|
286
|
-
@keyframes spin { to { transform: rotate(360deg); } }
|
|
287
|
-
#sidebar::-webkit-scrollbar, #main::-webkit-scrollbar { width: 5px; }
|
|
288
|
-
#sidebar::-webkit-scrollbar-thumb, #main::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
289
|
-
@media (max-width: 768px) { #app { grid-template-columns: 1fr; } #sidebar { display: none; } #doc-wrap { padding: 24px 20px 60px; } }
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
<div
|
|
329
|
-
</div>
|
|
330
|
-
<div class="
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
<
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
<span>
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
<
|
|
345
|
-
</div>
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
<div id="
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
</div>
|
|
359
|
-
<div id="
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
<
|
|
373
|
-
<
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
//
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
document.
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
state.active
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
if (
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
<
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
<span
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
state.
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
const
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
</div
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
<
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
})
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
const
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
const
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
const
|
|
1267
|
-
const
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
<
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-theme="light">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>DocuTrack</title>
|
|
7
|
+
<style>
|
|
8
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
|
|
10
|
+
:root {
|
|
11
|
+
--bg: #ffffff;
|
|
12
|
+
--bg-sidebar: #f8fafc;
|
|
13
|
+
--bg-hover: #f1f5f9;
|
|
14
|
+
--bg-active: #eef2ff;
|
|
15
|
+
--border: #e2e8f0;
|
|
16
|
+
--text: #0f172a;
|
|
17
|
+
--text-muted: #64748b;
|
|
18
|
+
--text-sidebar: #374151;
|
|
19
|
+
--accent: #6366f1;
|
|
20
|
+
--accent-hover: #4f46e5;
|
|
21
|
+
--accent-light: #eef2ff;
|
|
22
|
+
--code-bg: #f1f5f9;
|
|
23
|
+
--code-text: #1e293b;
|
|
24
|
+
--green: #10b981; --green-bg: #dcfce7; --green-text: #166534;
|
|
25
|
+
--blue-bg: #dbeafe; --blue-text: #1e40af;
|
|
26
|
+
--yellow: #f59e0b; --yellow-bg: #fef3c7; --yellow-text: #92400e;
|
|
27
|
+
--orange-bg: #ffedd5; --orange-text: #9a3412;
|
|
28
|
+
--purple-bg: #ede9fe; --purple-text: #6d28d9;
|
|
29
|
+
--red: #ef4444; --red-bg: #fee2e2; --red-text: #991b1b;
|
|
30
|
+
--sidebar-w: 272px;
|
|
31
|
+
--header-h: 56px;
|
|
32
|
+
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
33
|
+
--font-mono: "Cascadia Code", "Fira Code", Consolas, monospace;
|
|
34
|
+
--radius: 6px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
[data-theme="dark"] {
|
|
38
|
+
--bg: #0f172a; --bg-sidebar: #1e293b; --bg-hover: #334155; --bg-active: #312e81;
|
|
39
|
+
--border: #334155; --text: #f1f5f9; --text-muted: #94a3b8; --text-sidebar: #cbd5e1;
|
|
40
|
+
--accent: #818cf8; --accent-hover: #a5b4fc; --accent-light: #1e1b4b;
|
|
41
|
+
--code-bg: #1e293b; --code-text: #e2e8f0;
|
|
42
|
+
--green-bg: #14532d; --green-text: #86efac;
|
|
43
|
+
--blue-bg: #1e3a5f; --blue-text: #93c5fd;
|
|
44
|
+
--yellow-bg: #78350f; --yellow-text: #fde68a;
|
|
45
|
+
--orange-bg: #7c2d12; --orange-text: #fed7aa;
|
|
46
|
+
--purple-bg: #3b0764; --purple-text: #d8b4fe;
|
|
47
|
+
--red-bg: #7f1d1d; --red-text: #fca5a5;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
html, body { height: 100%; font-family: var(--font); color: var(--text); background: var(--bg); font-size: 15px; line-height: 1.6; }
|
|
51
|
+
|
|
52
|
+
/* ── Layout ── */
|
|
53
|
+
#app { display: grid; grid-template-columns: var(--sidebar-w) 1fr; grid-template-rows: var(--header-h) 1fr; height: 100vh; }
|
|
54
|
+
|
|
55
|
+
/* ── Header ── */
|
|
56
|
+
#header { grid-column: 1 / -1; background: var(--bg); border-bottom: 1px solid var(--border); display: flex; align-items: center; padding: 0 20px; gap: 16px; position: sticky; top: 0; z-index: 100; }
|
|
57
|
+
.logo { display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: 15px; color: var(--text); text-decoration: none; white-space: nowrap; cursor: pointer; }
|
|
58
|
+
.logo-icon { width: 28px; height: 28px; background: var(--accent); border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 14px; flex-shrink: 0; }
|
|
59
|
+
.header-sep { width: 1px; height: 20px; background: var(--border); flex-shrink: 0; }
|
|
60
|
+
#project-name { font-size: 13px; color: var(--text-muted); white-space: nowrap; }
|
|
61
|
+
.header-right { margin-left: auto; display: flex; align-items: center; gap: 8px; }
|
|
62
|
+
#coverage-badge { display: flex; align-items: center; gap: 6px; background: var(--bg-sidebar); border: 1px solid var(--border); border-radius: 20px; padding: 3px 10px; font-size: 12px; font-weight: 600; color: var(--text-muted); white-space: nowrap; }
|
|
63
|
+
.coverage-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--green); flex-shrink: 0; }
|
|
64
|
+
#theme-toggle { width: 32px; height: 32px; border-radius: var(--radius); border: 1px solid var(--border); background: var(--bg); cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 16px; }
|
|
65
|
+
#theme-toggle:hover { background: var(--bg-hover); }
|
|
66
|
+
.live-pill { display: flex; align-items: center; gap: 4px; font-size: 11px; color: var(--text-muted); }
|
|
67
|
+
.live-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--green); animation: pulse 2s infinite; flex-shrink: 0; }
|
|
68
|
+
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} }
|
|
69
|
+
|
|
70
|
+
/* ── Search ── */
|
|
71
|
+
#search-wrap { flex: 1; max-width: 380px; position: relative; }
|
|
72
|
+
#search { width: 100%; padding: 6px 12px 6px 34px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg-sidebar); color: var(--text); font-size: 13px; outline: none; }
|
|
73
|
+
#search:focus { border-color: var(--accent); background: var(--bg); }
|
|
74
|
+
#search::placeholder { color: var(--text-muted); }
|
|
75
|
+
.search-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); font-size: 13px; pointer-events: none; color: var(--text-muted); }
|
|
76
|
+
.search-kbd { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); font-size: 10px; color: var(--text-muted); background: var(--border); padding: 1px 5px; border-radius: 3px; pointer-events: none; }
|
|
77
|
+
|
|
78
|
+
/* ── Sidebar ── */
|
|
79
|
+
#sidebar { background: var(--bg-sidebar); border-right: 1px solid var(--border); overflow-y: auto; padding: 16px 0 80px; }
|
|
80
|
+
.nav-section { margin-bottom: 4px; }
|
|
81
|
+
.nav-section-title { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); padding: 8px 20px 4px; }
|
|
82
|
+
.nav-item { display: flex; align-items: center; gap: 8px; padding: 6px 20px; font-size: 13px; color: var(--text-sidebar); cursor: pointer; border-left: 2px solid transparent; }
|
|
83
|
+
.nav-item:hover { background: var(--bg-hover); color: var(--text); }
|
|
84
|
+
.nav-item.active { background: var(--bg-active); color: var(--accent); border-left-color: var(--accent); font-weight: 500; }
|
|
85
|
+
.nav-icon { font-size: 14px; width: 16px; text-align: center; flex-shrink: 0; }
|
|
86
|
+
.nav-empty { font-size: 12px; color: var(--text-muted); padding: 4px 20px 8px; font-style: italic; }
|
|
87
|
+
.nav-badge { margin-left: auto; font-size: 11px; background: var(--accent-light); color: var(--accent); border-radius: 10px; padding: 0 6px; font-weight: 600; }
|
|
88
|
+
.nav-stale { margin-left: auto; font-size: 10px; background: var(--yellow-bg); color: var(--yellow-text); border-radius: 10px; padding: 0 5px; font-weight: 700; }
|
|
89
|
+
.stale-banner { background: var(--yellow-bg); border-top: 1px solid var(--yellow); padding: 8px 20px; font-size: 12px; color: var(--yellow-text); }
|
|
90
|
+
|
|
91
|
+
/* Coverage bar */
|
|
92
|
+
#coverage-section { margin-top: 16px; padding: 0 16px; }
|
|
93
|
+
.coverage-label { font-size: 11px; color: var(--text-muted); margin-bottom: 6px; display: flex; justify-content: space-between; }
|
|
94
|
+
.coverage-bar { height: 4px; background: var(--border); border-radius: 2px; overflow: hidden; }
|
|
95
|
+
.coverage-fill { height: 100%; background: var(--green); border-radius: 2px; transition: width .5s; }
|
|
96
|
+
.coverage-fill.warn { background: var(--yellow); }
|
|
97
|
+
.coverage-fill.bad { background: var(--red); }
|
|
98
|
+
|
|
99
|
+
/* ── Main ── */
|
|
100
|
+
#main { overflow-y: auto; display: flex; justify-content: center; align-items: flex-start; min-height: 0; }
|
|
101
|
+
#doc-wrap { width: 100%; max-width: 800px; padding: 40px 48px 120px; }
|
|
102
|
+
|
|
103
|
+
/* Pending banner */
|
|
104
|
+
#pending-banner { display: none; background: var(--yellow-bg); border: 1px solid var(--yellow); border-radius: var(--radius); padding: 10px 16px; margin-bottom: 24px; font-size: 13px; color: var(--yellow-text); align-items: center; gap: 8px; }
|
|
105
|
+
#pending-banner.show { display: flex; }
|
|
106
|
+
|
|
107
|
+
/* ── Markdown ── */
|
|
108
|
+
#doc-content h1 { font-size: 28px; font-weight: 800; margin-bottom: 8px; padding-bottom: 12px; border-bottom: 1px solid var(--border); }
|
|
109
|
+
#doc-content h2 { font-size: 20px; font-weight: 700; margin-top: 36px; margin-bottom: 12px; }
|
|
110
|
+
#doc-content h3 { font-size: 16px; font-weight: 600; margin-top: 24px; margin-bottom: 8px; }
|
|
111
|
+
#doc-content h4 { font-size: 14px; font-weight: 600; margin-top: 16px; margin-bottom: 6px; text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted); }
|
|
112
|
+
#doc-content p { margin-bottom: 14px; }
|
|
113
|
+
#doc-content ul, #doc-content ol { margin-bottom: 14px; padding-left: 24px; }
|
|
114
|
+
#doc-content li { margin-bottom: 4px; }
|
|
115
|
+
#doc-content a { color: var(--accent); text-decoration: none; }
|
|
116
|
+
#doc-content a:hover { text-decoration: underline; }
|
|
117
|
+
#doc-content blockquote { border-left: 3px solid var(--accent); padding: 8px 16px; margin: 16px 0; color: var(--text-muted); background: var(--accent-light); border-radius: 0 var(--radius) var(--radius) 0; }
|
|
118
|
+
#doc-content blockquote p { margin: 0; }
|
|
119
|
+
#doc-content code { font-family: var(--font-mono); font-size: 13px; background: var(--code-bg); color: var(--code-text); padding: 2px 6px; border-radius: 4px; }
|
|
120
|
+
#doc-content pre { background: var(--code-bg); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; overflow-x: auto; margin: 16px 0; }
|
|
121
|
+
#doc-content pre code { background: none; padding: 0; font-size: 13px; line-height: 1.6; }
|
|
122
|
+
#doc-content table { width: 100%; border-collapse: collapse; margin: 16px 0; font-size: 14px; }
|
|
123
|
+
#doc-content th { background: var(--bg-sidebar); font-weight: 600; text-align: left; padding: 10px 14px; border: 1px solid var(--border); }
|
|
124
|
+
#doc-content td { padding: 9px 14px; border: 1px solid var(--border); }
|
|
125
|
+
#doc-content tr:nth-child(even) td { background: var(--bg-sidebar); }
|
|
126
|
+
#doc-content hr { border: none; border-top: 1px solid var(--border); margin: 28px 0; }
|
|
127
|
+
.mermaid-wrap { background: var(--bg-sidebar); border: 1px solid var(--border); border-radius: var(--radius); padding: 24px; margin: 20px 0; overflow-x: auto; text-align: center; }
|
|
128
|
+
|
|
129
|
+
/* ── API Explorer ── */
|
|
130
|
+
.api-explorer { width: 100%; }
|
|
131
|
+
.api-explorer-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; margin-bottom: 28px; padding-bottom: 20px; border-bottom: 1px solid var(--border); flex-wrap: wrap; }
|
|
132
|
+
.base-url-wrap { display: flex; flex-direction: column; gap: 4px; min-width: 260px; }
|
|
133
|
+
.base-url-label { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); }
|
|
134
|
+
.base-url-row { display: flex; gap: 6px; align-items: center; }
|
|
135
|
+
.base-url-input { padding: 6px 10px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); color: var(--text); font-size: 13px; font-family: var(--font-mono); width: 260px; outline: none; }
|
|
136
|
+
.base-url-input:focus { border-color: var(--accent); }
|
|
137
|
+
.auth-toggle { padding: 6px 12px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); color: var(--text-muted); font-size: 12px; cursor: pointer; white-space: nowrap; }
|
|
138
|
+
.auth-toggle:hover { background: var(--bg-hover); }
|
|
139
|
+
.auth-row { margin-top: 8px; display: none; gap: 6px; align-items: center; }
|
|
140
|
+
.auth-row.show { display: flex; }
|
|
141
|
+
.auth-input { flex: 1; padding: 6px 10px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); color: var(--text); font-size: 13px; font-family: var(--font-mono); outline: none; }
|
|
142
|
+
.auth-input:focus { border-color: var(--accent); }
|
|
143
|
+
.auth-label { font-size: 12px; color: var(--text-muted); white-space: nowrap; }
|
|
144
|
+
|
|
145
|
+
.api-group { margin-bottom: 24px; }
|
|
146
|
+
.api-group-title { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; color: var(--text-muted); margin-bottom: 8px; padding: 0 2px; }
|
|
147
|
+
|
|
148
|
+
.endpoint-card { border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 6px; overflow: hidden; transition: box-shadow .15s; }
|
|
149
|
+
.endpoint-card.open { box-shadow: 0 2px 8px rgba(0,0,0,.06); }
|
|
150
|
+
.endpoint-header { display: flex; align-items: center; gap: 12px; padding: 11px 16px; cursor: pointer; background: var(--bg); user-select: none; }
|
|
151
|
+
.endpoint-header:hover { background: var(--bg-hover); }
|
|
152
|
+
.endpoint-path { font-family: var(--font-mono); font-size: 13px; font-weight: 500; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
153
|
+
.endpoint-summary { font-size: 13px; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 280px; }
|
|
154
|
+
.endpoint-chevron { color: var(--text-muted); font-size: 16px; transition: transform .2s; flex-shrink: 0; }
|
|
155
|
+
.endpoint-card.open .endpoint-chevron { transform: rotate(90deg); }
|
|
156
|
+
.endpoint-body { display: none; border-top: 1px solid var(--border); background: var(--bg); }
|
|
157
|
+
.endpoint-card.open .endpoint-body { display: block; }
|
|
158
|
+
.endpoint-section { padding: 16px 20px; border-bottom: 1px solid var(--border); }
|
|
159
|
+
.endpoint-section:last-child { border-bottom: none; }
|
|
160
|
+
.section-title { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); margin-bottom: 12px; }
|
|
161
|
+
|
|
162
|
+
/* Method badges */
|
|
163
|
+
.method-badge { display: inline-flex; align-items: center; padding: 3px 8px; border-radius: 4px; font-size: 11px; font-weight: 700; letter-spacing: .05em; font-family: var(--font-mono); flex-shrink: 0; min-width: 54px; justify-content: center; }
|
|
164
|
+
.badge-get { background: var(--green-bg); color: var(--green-text); }
|
|
165
|
+
.badge-post { background: var(--blue-bg); color: var(--blue-text); }
|
|
166
|
+
.badge-put { background: var(--orange-bg); color: var(--orange-text); }
|
|
167
|
+
.badge-patch { background: var(--purple-bg); color: var(--purple-text); }
|
|
168
|
+
.badge-delete { background: var(--red-bg); color: var(--red-text); }
|
|
169
|
+
.badge-options,.badge-head { background: var(--code-bg); color: var(--text-muted); }
|
|
170
|
+
|
|
171
|
+
/* Params table */
|
|
172
|
+
.params-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
173
|
+
.params-table th { font-size: 11px; font-weight: 600; text-align: left; padding: 6px 10px; background: var(--bg-sidebar); border: 1px solid var(--border); color: var(--text-muted); }
|
|
174
|
+
.params-table td { padding: 7px 10px; border: 1px solid var(--border); font-family: var(--font-mono); font-size: 12px; }
|
|
175
|
+
.param-required { background: var(--red-bg); color: var(--red-text); padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 700; font-family: var(--font); }
|
|
176
|
+
.param-optional { background: var(--code-bg); color: var(--text-muted); padding: 1px 6px; border-radius: 3px; font-size: 10px; font-family: var(--font); }
|
|
177
|
+
|
|
178
|
+
/* Try it out */
|
|
179
|
+
.try-section { padding: 16px 20px; background: var(--bg-sidebar); }
|
|
180
|
+
.try-btn { display: inline-flex; align-items: center; gap: 6px; padding: 7px 16px; background: var(--accent); color: white; border: none; border-radius: var(--radius); font-size: 13px; font-weight: 600; cursor: pointer; }
|
|
181
|
+
.try-btn:hover { background: var(--accent-hover); }
|
|
182
|
+
.try-btn:disabled { opacity: .5; cursor: not-allowed; }
|
|
183
|
+
.try-form { margin-top: 14px; display: none; }
|
|
184
|
+
.try-form.show { display: block; }
|
|
185
|
+
.try-field { margin-bottom: 10px; }
|
|
186
|
+
.try-field label { display: block; font-size: 12px; font-weight: 600; margin-bottom: 4px; color: var(--text-muted); font-family: var(--font-mono); }
|
|
187
|
+
.try-field input, .try-field textarea { width: 100%; padding: 7px 10px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); color: var(--text); font-size: 13px; font-family: var(--font-mono); outline: none; }
|
|
188
|
+
.try-field input:focus, .try-field textarea:focus { border-color: var(--accent); }
|
|
189
|
+
.try-field textarea { min-height: 90px; resize: vertical; }
|
|
190
|
+
.try-actions { display: flex; gap: 8px; margin-top: 12px; }
|
|
191
|
+
.execute-btn { display: inline-flex; align-items: center; gap: 6px; padding: 8px 20px; background: var(--accent); color: white; border: none; border-radius: var(--radius); font-size: 13px; font-weight: 600; cursor: pointer; }
|
|
192
|
+
.execute-btn:hover { background: var(--accent-hover); }
|
|
193
|
+
.execute-btn:disabled { opacity: .5; cursor: not-allowed; }
|
|
194
|
+
.cancel-try-btn { padding: 8px 14px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); color: var(--text-muted); font-size: 13px; cursor: pointer; }
|
|
195
|
+
.cancel-try-btn:hover { background: var(--bg-hover); }
|
|
196
|
+
|
|
197
|
+
/* Response panel */
|
|
198
|
+
.response-panel { margin-top: 14px; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
|
|
199
|
+
.response-bar { display: flex; align-items: center; gap: 10px; padding: 8px 14px; background: var(--bg-sidebar); border-bottom: 1px solid var(--border); font-size: 12px; }
|
|
200
|
+
.status-chip { font-size: 12px; font-weight: 700; font-family: var(--font-mono); padding: 2px 8px; border-radius: 4px; }
|
|
201
|
+
.status-2xx { background: var(--green-bg); color: var(--green-text); }
|
|
202
|
+
.status-3xx { background: var(--blue-bg); color: var(--blue-text); }
|
|
203
|
+
.status-4xx { background: var(--yellow-bg); color: var(--yellow-text); }
|
|
204
|
+
.status-5xx { background: var(--red-bg); color: var(--red-text); }
|
|
205
|
+
.response-time { color: var(--text-muted); margin-left: auto; }
|
|
206
|
+
.response-body { padding: 14px; font-family: var(--font-mono); font-size: 12px; line-height: 1.6; overflow-x: auto; max-height: 360px; overflow-y: auto; white-space: pre; background: var(--bg); }
|
|
207
|
+
.response-headers { padding: 10px 14px; border-bottom: 1px solid var(--border); font-size: 12px; color: var(--text-muted); font-family: var(--font-mono); }
|
|
208
|
+
.cors-warn { margin-top: 10px; padding: 8px 12px; background: var(--yellow-bg); border-radius: var(--radius); font-size: 12px; color: var(--yellow-text); }
|
|
209
|
+
|
|
210
|
+
/* Welcome / empty states */
|
|
211
|
+
.empty-state { text-align: center; padding: 60px 40px; }
|
|
212
|
+
.empty-icon { font-size: 48px; margin-bottom: 16px; }
|
|
213
|
+
.empty-state h2 { font-size: 22px; font-weight: 700; margin-bottom: 8px; }
|
|
214
|
+
.empty-state p { color: var(--text-muted); max-width: 380px; margin: 0 auto 16px; }
|
|
215
|
+
.empty-state code { background: var(--code-bg); padding: 4px 10px; border-radius: var(--radius); font-family: var(--font-mono); font-size: 13px; }
|
|
216
|
+
|
|
217
|
+
/* Bootstrap panel */
|
|
218
|
+
.bootstrap-wrap { max-width: 580px; margin: 0 auto; padding: 48px 0; }
|
|
219
|
+
.bootstrap-hero { text-align: center; margin-bottom: 40px; }
|
|
220
|
+
.bootstrap-hero-icon { font-size: 52px; margin-bottom: 16px; }
|
|
221
|
+
.bootstrap-hero h1 { font-size: 26px; font-weight: 800; margin-bottom: 8px; }
|
|
222
|
+
.bootstrap-hero p { color: var(--text-muted); font-size: 15px; max-width: 420px; margin: 0 auto; }
|
|
223
|
+
.bootstrap-steps { display: flex; flex-direction: column; gap: 12px; margin-bottom: 32px; }
|
|
224
|
+
.bootstrap-step { display: flex; align-items: flex-start; gap: 14px; padding: 16px 18px; background: var(--bg-sidebar); border: 1px solid var(--border); border-radius: var(--radius); }
|
|
225
|
+
.bootstrap-step.done { border-color: var(--green); background: var(--green-bg); }
|
|
226
|
+
.bootstrap-step.active { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-light); }
|
|
227
|
+
.step-num { width: 28px; height: 28px; border-radius: 50%; background: var(--accent); color: white; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 700; flex-shrink: 0; margin-top: 1px; }
|
|
228
|
+
.bootstrap-step.done .step-num { background: var(--green); }
|
|
229
|
+
.step-content { flex: 1; min-width: 0; }
|
|
230
|
+
.step-title { font-size: 14px; font-weight: 600; margin-bottom: 3px; }
|
|
231
|
+
.step-desc { font-size: 13px; color: var(--text-muted); }
|
|
232
|
+
.step-desc code { font-family: var(--font-mono); background: var(--code-bg); padding: 1px 5px; border-radius: 3px; font-size: 12px; }
|
|
233
|
+
.step-check { font-size: 18px; flex-shrink: 0; margin-top: 4px; }
|
|
234
|
+
.bootstrap-btn { width: 100%; padding: 14px 24px; background: var(--accent); color: white; border: none; border-radius: var(--radius); font-size: 15px; font-weight: 700; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 10px; transition: background .15s; }
|
|
235
|
+
.bootstrap-btn:hover { background: var(--accent-hover); }
|
|
236
|
+
.bootstrap-btn:disabled { opacity: .6; cursor: not-allowed; }
|
|
237
|
+
.bootstrap-result { margin-top: 20px; padding: 16px 18px; background: var(--green-bg); border: 1px solid var(--green); border-radius: var(--radius); }
|
|
238
|
+
.bootstrap-result-title { font-weight: 700; color: var(--green-text); margin-bottom: 8px; font-size: 14px; }
|
|
239
|
+
.bootstrap-result-files { font-family: var(--font-mono); font-size: 12px; color: var(--green-text); line-height: 1.8; }
|
|
240
|
+
.bootstrap-claude { margin-top: 12px; padding: 14px 16px; background: var(--accent-light); border: 1px solid var(--accent); border-radius: var(--radius); }
|
|
241
|
+
.bootstrap-claude-label { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--accent); margin-bottom: 8px; }
|
|
242
|
+
.bootstrap-claude-cmd { font-family: var(--font-mono); font-size: 13px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 14px; cursor: pointer; user-select: all; color: var(--text); }
|
|
243
|
+
.bootstrap-claude-cmd:hover { border-color: var(--accent); }
|
|
244
|
+
.copy-hint { font-size: 11px; color: var(--text-muted); margin-top: 6px; }
|
|
245
|
+
|
|
246
|
+
/* ── Health Check ── */
|
|
247
|
+
.health-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 32px; padding-bottom: 20px; border-bottom: 1px solid var(--border); }
|
|
248
|
+
.health-title { font-size: 26px; font-weight: 800; margin-bottom: 4px; }
|
|
249
|
+
.health-subtitle { font-size: 13px; color: var(--text-muted); }
|
|
250
|
+
.health-overall { display: flex; align-items: center; gap: 8px; padding: 8px 16px; border-radius: 20px; font-size: 13px; font-weight: 700; }
|
|
251
|
+
.health-ok { background: var(--green-bg); color: var(--green-text); }
|
|
252
|
+
.health-warn { background: var(--yellow-bg); color: var(--yellow-text); }
|
|
253
|
+
.health-crit { background: var(--red-bg); color: var(--red-text); }
|
|
254
|
+
.health-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin-bottom: 32px; }
|
|
255
|
+
.health-card { background: var(--bg-sidebar); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px 18px; }
|
|
256
|
+
.health-card-val { font-size: 32px; font-weight: 800; line-height: 1; margin-bottom: 4px; }
|
|
257
|
+
.health-card-label { font-size: 12px; color: var(--text-muted); font-weight: 500; }
|
|
258
|
+
.health-card.issue .health-card-val { color: var(--red); }
|
|
259
|
+
.health-card.warn-card .health-card-val { color: var(--yellow); }
|
|
260
|
+
.health-card.ok .health-card-val { color: var(--green); }
|
|
261
|
+
.health-section-title { font-size: 14px; font-weight: 700; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
|
|
262
|
+
.health-section { margin-bottom: 28px; }
|
|
263
|
+
.drift-row { display: flex; align-items: flex-start; gap: 12px; padding: 12px 14px; border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 6px; background: var(--bg); }
|
|
264
|
+
.drift-sev { font-size: 10px; font-weight: 800; padding: 2px 7px; border-radius: 3px; flex-shrink: 0; margin-top: 2px; }
|
|
265
|
+
.drift-sev.high { background: var(--red-bg); color: var(--red-text); }
|
|
266
|
+
.drift-sev.medium { background: var(--yellow-bg); color: var(--yellow-text); }
|
|
267
|
+
.drift-sev.low { background: var(--code-bg); color: var(--text-muted); }
|
|
268
|
+
.drift-module { font-weight: 600; font-size: 13px; }
|
|
269
|
+
.drift-detail { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
|
|
270
|
+
.drift-detail span { font-family: var(--font-mono); background: var(--code-bg); padding: 0 4px; border-radius: 3px; font-size: 11px; margin-right: 3px; }
|
|
271
|
+
.complexity-row { display: flex; align-items: center; gap: 10px; padding: 10px 14px; border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 6px; background: var(--bg); }
|
|
272
|
+
.complexity-score { width: 38px; height: 38px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 800; flex-shrink: 0; }
|
|
273
|
+
.score-bad { background: var(--red-bg); color: var(--red-text); }
|
|
274
|
+
.score-warn { background: var(--yellow-bg); color: var(--yellow-text); }
|
|
275
|
+
.score-ok { background: var(--green-bg); color: var(--green-text); }
|
|
276
|
+
.complexity-file { font-family: var(--font-mono); font-size: 12px; font-weight: 500; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
277
|
+
.complexity-tags { display: flex; gap: 6px; flex-shrink: 0; flex-wrap: wrap; }
|
|
278
|
+
.complexity-tag { font-size: 11px; background: var(--code-bg); color: var(--text-muted); padding: 1px 7px; border-radius: 3px; }
|
|
279
|
+
.complexity-tag.crit { background: var(--red-bg); color: var(--red-text); }
|
|
280
|
+
.health-empty { text-align: center; padding: 20px; color: var(--text-muted); font-size: 13px; }
|
|
281
|
+
.health-refresh { padding: 7px 14px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); color: var(--text-muted); font-size: 12px; cursor: pointer; }
|
|
282
|
+
.health-refresh:hover { background: var(--bg-hover); }
|
|
283
|
+
|
|
284
|
+
/* Misc */
|
|
285
|
+
.spinner { display: inline-block; width: 16px; height: 16px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin .6s linear infinite; }
|
|
286
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
287
|
+
#sidebar::-webkit-scrollbar, #main::-webkit-scrollbar { width: 5px; }
|
|
288
|
+
#sidebar::-webkit-scrollbar-thumb, #main::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
289
|
+
@media (max-width: 768px) { #app { grid-template-columns: 1fr; } #sidebar { display: none; } #doc-wrap { padding: 24px 20px 60px; } }
|
|
290
|
+
|
|
291
|
+
/* ── Command Palette ── */
|
|
292
|
+
.palette-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.48); z-index: 500; display: none; align-items: flex-start; justify-content: center; padding-top: 14vh; }
|
|
293
|
+
.palette-overlay.open { display: flex; }
|
|
294
|
+
.palette-box { width: 100%; max-width: 560px; background: var(--bg); border: 1px solid var(--border); border-radius: 10px; box-shadow: 0 24px 64px rgba(0,0,0,.28); overflow: hidden; }
|
|
295
|
+
.palette-input-wrap { display: flex; align-items: center; gap: 10px; padding: 14px 16px; border-bottom: 1px solid var(--border); }
|
|
296
|
+
.palette-search-icon { font-size: 16px; color: var(--text-muted); flex-shrink: 0; }
|
|
297
|
+
#palette-input { flex: 1; border: none; outline: none; font-size: 15px; background: transparent; color: var(--text); }
|
|
298
|
+
#palette-input::placeholder { color: var(--text-muted); }
|
|
299
|
+
.palette-esc { font-size: 11px; background: var(--border); color: var(--text-muted); padding: 2px 7px; border-radius: 4px; flex-shrink: 0; cursor: pointer; }
|
|
300
|
+
.palette-results { max-height: 380px; overflow-y: auto; }
|
|
301
|
+
.palette-group-header { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted); padding: 8px 16px 4px; background: var(--bg-sidebar); }
|
|
302
|
+
.palette-item { display: flex; align-items: center; gap: 10px; padding: 10px 16px; cursor: pointer; font-size: 13px; border-left: 2px solid transparent; }
|
|
303
|
+
.palette-item.selected { background: var(--bg-active); color: var(--accent); border-left-color: var(--accent); }
|
|
304
|
+
.palette-item-icon { width: 20px; text-align: center; flex-shrink: 0; }
|
|
305
|
+
.palette-item-name { flex: 1; }
|
|
306
|
+
.palette-item-section { font-size: 11px; color: var(--text-muted); flex-shrink: 0; }
|
|
307
|
+
.palette-item-snippet { font-size: 11px; color: var(--text-muted); flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 220px; }
|
|
308
|
+
.palette-empty { padding: 32px; text-align: center; color: var(--text-muted); font-size: 13px; }
|
|
309
|
+
.palette-footer { padding: 8px 16px; border-top: 1px solid var(--border); font-size: 11px; color: var(--text-muted); display: flex; gap: 14px; background: var(--bg-sidebar); }
|
|
310
|
+
.palette-footer kbd { background: var(--border); padding: 1px 5px; border-radius: 3px; font-family: var(--font-mono); }
|
|
311
|
+
|
|
312
|
+
/* ── Copy button ── */
|
|
313
|
+
.code-block-wrap { position: relative; }
|
|
314
|
+
.copy-btn { position: absolute; top: 8px; right: 8px; padding: 3px 9px; border: 1px solid var(--border); border-radius: 4px; background: var(--bg); color: var(--text-muted); font-size: 11px; cursor: pointer; opacity: 0; transition: opacity .15s; z-index: 1; font-family: var(--font); }
|
|
315
|
+
.code-block-wrap:hover .copy-btn { opacity: 1; }
|
|
316
|
+
.copy-btn.copied { color: var(--green-text); border-color: var(--green); background: var(--green-bg); }
|
|
317
|
+
|
|
318
|
+
/* ── Heading anchors ── */
|
|
319
|
+
.heading-anchor { opacity: 0; margin-left: 6px; font-size: .72em; color: var(--text-muted); text-decoration: none; transition: opacity .15s; vertical-align: middle; }
|
|
320
|
+
h2:hover .heading-anchor, h3:hover .heading-anchor { opacity: 1; }
|
|
321
|
+
</style>
|
|
322
|
+
</head>
|
|
323
|
+
<body>
|
|
324
|
+
<div id="app">
|
|
325
|
+
|
|
326
|
+
<header id="header">
|
|
327
|
+
<div class="logo" onclick="goHome()">
|
|
328
|
+
<div class="logo-icon">📋</div>DocuTrack
|
|
329
|
+
</div>
|
|
330
|
+
<div class="header-sep"></div>
|
|
331
|
+
<span id="project-name">loading…</span>
|
|
332
|
+
<div id="search-wrap">
|
|
333
|
+
<span class="search-icon">🔍</span>
|
|
334
|
+
<input type="text" id="search" placeholder="Search docs…" autocomplete="off">
|
|
335
|
+
<span class="search-kbd">⌘K</span>
|
|
336
|
+
</div>
|
|
337
|
+
<div class="header-right">
|
|
338
|
+
<div id="coverage-badge">
|
|
339
|
+
<span class="coverage-dot" id="cov-dot"></span>
|
|
340
|
+
<span id="cov-pct">–%</span>
|
|
341
|
+
<span style="color:var(--text-muted);font-weight:400">coverage</span>
|
|
342
|
+
</div>
|
|
343
|
+
<button id="theme-toggle" title="Toggle dark mode">🌙</button>
|
|
344
|
+
<span class="live-pill"><span class="live-dot"></span>live</span>
|
|
345
|
+
</div>
|
|
346
|
+
</header>
|
|
347
|
+
|
|
348
|
+
<aside id="sidebar">
|
|
349
|
+
<div class="nav-section">
|
|
350
|
+
<div class="nav-section-title">Overview</div>
|
|
351
|
+
<div id="nav-arch"></div>
|
|
352
|
+
</div>
|
|
353
|
+
<div class="nav-section">
|
|
354
|
+
<div class="nav-section-title">Modules</div>
|
|
355
|
+
<div id="nav-modules"></div>
|
|
356
|
+
</div>
|
|
357
|
+
<div class="nav-section">
|
|
358
|
+
<div class="nav-section-title">Decisions</div>
|
|
359
|
+
<div id="nav-decisions"></div>
|
|
360
|
+
</div>
|
|
361
|
+
<div class="nav-section">
|
|
362
|
+
<div class="nav-section-title">API</div>
|
|
363
|
+
<div id="nav-api-explorer"></div>
|
|
364
|
+
<div id="nav-api-docs"></div>
|
|
365
|
+
</div>
|
|
366
|
+
<div class="nav-section">
|
|
367
|
+
<div class="nav-section-title">Health</div>
|
|
368
|
+
<div id="nav-health"></div>
|
|
369
|
+
</div>
|
|
370
|
+
<div id="coverage-section">
|
|
371
|
+
<div class="coverage-label">
|
|
372
|
+
<span>Doc coverage</span>
|
|
373
|
+
<span id="cov-label">–%</span>
|
|
374
|
+
</div>
|
|
375
|
+
<div class="coverage-bar"><div class="coverage-fill" id="cov-bar" style="width:0%"></div></div>
|
|
376
|
+
</div>
|
|
377
|
+
<div style="padding:12px 16px 8px;border-top:1px solid var(--border);margin-top:8px">
|
|
378
|
+
<button onclick="showRegenPanel()" style="width:100%;padding:7px 12px;border:1px solid var(--border);background:var(--bg);color:var(--text-muted);border-radius:var(--radius);font-size:12px;cursor:pointer;text-align:left;display:flex;align-items:center;gap:6px">
|
|
379
|
+
<span>✨</span><span>Regenerate docs</span>
|
|
380
|
+
</button>
|
|
381
|
+
</div>
|
|
382
|
+
</aside>
|
|
383
|
+
|
|
384
|
+
<main id="main">
|
|
385
|
+
<div id="doc-wrap">
|
|
386
|
+
<div id="pending-banner">
|
|
387
|
+
⚠ <strong id="pending-count">0</strong> file(s) modified without documentation.
|
|
388
|
+
Run <code style="margin-left:4px">docutrack status</code> for details.
|
|
389
|
+
</div>
|
|
390
|
+
<div id="doc-content">
|
|
391
|
+
<div class="empty-state">
|
|
392
|
+
<div class="empty-icon">📋</div>
|
|
393
|
+
<h2>Welcome to DocuTrack</h2>
|
|
394
|
+
<p>Living documentation for your project. Select a page from the sidebar.</p>
|
|
395
|
+
<span class="spinner"></span>
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
</main>
|
|
400
|
+
|
|
401
|
+
</div>
|
|
402
|
+
|
|
403
|
+
<!-- Command Palette -->
|
|
404
|
+
<div id="palette-overlay" class="palette-overlay" onclick="onPaletteOverlayClick(event)">
|
|
405
|
+
<div class="palette-box">
|
|
406
|
+
<div class="palette-input-wrap">
|
|
407
|
+
<span class="palette-search-icon">🔍</span>
|
|
408
|
+
<input id="palette-input" placeholder="Search docs, jump to…" autocomplete="off" spellcheck="false">
|
|
409
|
+
<span class="palette-esc" onclick="closePalette()">ESC</span>
|
|
410
|
+
</div>
|
|
411
|
+
<div id="palette-results" class="palette-results"></div>
|
|
412
|
+
<div class="palette-footer">
|
|
413
|
+
<span><kbd>↑↓</kbd> navigate</span>
|
|
414
|
+
<span><kbd>↵</kbd> open</span>
|
|
415
|
+
<span><kbd>ESC</kbd> close</span>
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
|
|
420
|
+
<script src="https://cdn.jsdelivr.net/npm/marked@9/marked.min.js"></script>
|
|
421
|
+
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
|
422
|
+
<script>
|
|
423
|
+
'use strict'
|
|
424
|
+
|
|
425
|
+
// ── State ──────────────────────────────────────────────────────────────────
|
|
426
|
+
const state = {
|
|
427
|
+
tree: null,
|
|
428
|
+
active: null, // file path or 'api-explorer' or 'health-check'
|
|
429
|
+
query: '',
|
|
430
|
+
view: 'docs', // 'docs' | 'api-explorer' | 'health-check'
|
|
431
|
+
staleSet: new Set(),
|
|
432
|
+
healthBadge: 0, // number of critical health issues
|
|
433
|
+
lang: 'es', // 'es' | 'en' — language for docs generation
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const api = {
|
|
437
|
+
spec: null,
|
|
438
|
+
baseUrl: localStorage.getItem('dt-baseUrl') || 'http://localhost:3000',
|
|
439
|
+
token: localStorage.getItem('dt-token') || '',
|
|
440
|
+
showAuth: false,
|
|
441
|
+
open: {}, // endpointId -> boolean
|
|
442
|
+
tryOpen: {}, // endpointId -> boolean
|
|
443
|
+
responses: {}, // endpointId -> response object
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ── Marked ────────────────────────────────────────────────────────────────
|
|
447
|
+
marked.use({
|
|
448
|
+
gfm: true,
|
|
449
|
+
renderer: {
|
|
450
|
+
code({ text, lang }) {
|
|
451
|
+
const t = text || ''
|
|
452
|
+
if (lang === 'mermaid') {
|
|
453
|
+
return `<div class="mermaid-wrap"><div class="mermaid">${esc(t)}</div></div>`
|
|
454
|
+
}
|
|
455
|
+
if (!t.trim()) return `<pre><code class="language-${esc(lang||'')}"></code></pre>`
|
|
456
|
+
return `<div class="code-block-wrap"><pre><code class="language-${esc(lang||'')}">${esc(t)}</code></pre><button class="copy-btn" onclick="copyCode(this)">Copy</button></div>`
|
|
457
|
+
},
|
|
458
|
+
heading({ text, depth }) {
|
|
459
|
+
const t = text || ''
|
|
460
|
+
if (depth === 1) return `<h1>${t}</h1>\n`
|
|
461
|
+
const anchor = t.replace(/<[^>]+>/g, '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || `h${depth}`
|
|
462
|
+
return `<h${depth} id="${anchor}">${t}<a class="heading-anchor" href="#${anchor}" onclick="copyAnchor(this,event)">#</a></h${depth}>\n`
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
})
|
|
466
|
+
mermaid.initialize({ startOnLoad: false, theme: 'neutral', securityLevel: 'loose' })
|
|
467
|
+
|
|
468
|
+
// ── Boot ──────────────────────────────────────────────────────────────────
|
|
469
|
+
async function boot() {
|
|
470
|
+
setTheme(localStorage.getItem('dt-theme') || 'light')
|
|
471
|
+
|
|
472
|
+
// Pre-set state.active so refreshTree() skips its default auto-navigation
|
|
473
|
+
const _initHash = location.hash.slice(1)
|
|
474
|
+
if (_initHash) state.active = '__hash__'
|
|
475
|
+
|
|
476
|
+
await refreshTree()
|
|
477
|
+
await refreshStatus()
|
|
478
|
+
connectSSE()
|
|
479
|
+
|
|
480
|
+
// Restore the view the user was on before reloading
|
|
481
|
+
if (_initHash) restoreFromHash(_initHash)
|
|
482
|
+
document.getElementById('search').addEventListener('input', e => {
|
|
483
|
+
state.query = e.target.value.trim()
|
|
484
|
+
renderNav()
|
|
485
|
+
})
|
|
486
|
+
document.getElementById('palette-input').addEventListener('input', e => {
|
|
487
|
+
renderPaletteResults(e.target.value.trim())
|
|
488
|
+
})
|
|
489
|
+
document.addEventListener('keydown', e => {
|
|
490
|
+
const palOpen = document.getElementById('palette-overlay').classList.contains('open')
|
|
491
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
492
|
+
e.preventDefault()
|
|
493
|
+
palOpen ? closePalette() : openPalette()
|
|
494
|
+
return
|
|
495
|
+
}
|
|
496
|
+
if (e.key === 'Escape') {
|
|
497
|
+
if (palOpen) { closePalette(); return }
|
|
498
|
+
document.getElementById('search').blur()
|
|
499
|
+
}
|
|
500
|
+
if (palOpen) {
|
|
501
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); paletteIdx = Math.min(paletteIdx + 1, paletteItems.length - 1); updatePaletteSelection(); scrollPaletteItem() }
|
|
502
|
+
else if (e.key === 'ArrowUp') { e.preventDefault(); paletteIdx = Math.max(paletteIdx - 1, 0); updatePaletteSelection(); scrollPaletteItem() }
|
|
503
|
+
else if (e.key === 'Enter') { e.preventDefault(); selectPaletteItem(paletteIdx) }
|
|
504
|
+
}
|
|
505
|
+
})
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ── Tree / nav ─────────────────────────────────────────────────────────────
|
|
509
|
+
async function refreshTree() {
|
|
510
|
+
const tree = await get('/api/tree')
|
|
511
|
+
if (!tree) return
|
|
512
|
+
state.tree = tree
|
|
513
|
+
|
|
514
|
+
// Project name from package.json
|
|
515
|
+
try {
|
|
516
|
+
const r = await fetch('/api/content?path=package.json')
|
|
517
|
+
if (r.ok) { const j = JSON.parse(await r.text()); if (j.name) { document.getElementById('project-name').textContent = j.name; document.title = `DocuTrack — ${j.name}` } }
|
|
518
|
+
} catch { /* ok */ }
|
|
519
|
+
|
|
520
|
+
renderNav()
|
|
521
|
+
const hasModules = tree.modules?.length > 0
|
|
522
|
+
if (!state.active) {
|
|
523
|
+
if (hasModules && tree.architecture) loadFile(tree.architecture)
|
|
524
|
+
else showWelcome()
|
|
525
|
+
} else if (state.active === 'bootstrap' && hasModules) {
|
|
526
|
+
// Modules just appeared — navigate to architecture
|
|
527
|
+
if (tree.architecture) loadFile(tree.architecture)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function renderNav() {
|
|
532
|
+
const { tree, query, active } = state
|
|
533
|
+
if (!tree) return
|
|
534
|
+
const q = query.toLowerCase()
|
|
535
|
+
|
|
536
|
+
// Architecture
|
|
537
|
+
const archEl = document.getElementById('nav-arch')
|
|
538
|
+
if (tree.architecture) {
|
|
539
|
+
archEl.innerHTML = matches('architecture', q)
|
|
540
|
+
? navItem(tree.architecture, '🏗', 'Architecture', active === tree.architecture)
|
|
541
|
+
: ''
|
|
542
|
+
} else {
|
|
543
|
+
archEl.innerHTML = '<div class="nav-empty">Not created yet</div>'
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
renderSection('nav-modules', tree.modules, '📦', q, active)
|
|
547
|
+
renderSection('nav-decisions', tree.decisions, '📝', q, active)
|
|
548
|
+
|
|
549
|
+
// API Explorer special entry
|
|
550
|
+
const explorerEl = document.getElementById('nav-api-explorer')
|
|
551
|
+
const epCount = api.spec ? Object.values(api.spec.paths || {}).reduce((n, p) => n + Object.keys(p).length, 0) : 0
|
|
552
|
+
const explorerMatch = !q || 'api explorer'.includes(q)
|
|
553
|
+
explorerEl.innerHTML = explorerMatch
|
|
554
|
+
? `<div class="nav-item${active === 'api-explorer' ? ' active' : ''}" onclick="showApiExplorer()">
|
|
555
|
+
<span class="nav-icon">⚡</span>API Explorer
|
|
556
|
+
${epCount > 0 ? `<span class="nav-badge">${epCount}</span>` : ''}
|
|
557
|
+
</div>`
|
|
558
|
+
: ''
|
|
559
|
+
|
|
560
|
+
// API docs (markdown files)
|
|
561
|
+
renderSection('nav-api-docs', tree.api, '📄', q, active)
|
|
562
|
+
|
|
563
|
+
// Health Check nav entry
|
|
564
|
+
const healthMatch = !q || 'health check'.includes(q)
|
|
565
|
+
const healthHasIssue = state.healthBadge
|
|
566
|
+
document.getElementById('nav-health').innerHTML = healthMatch
|
|
567
|
+
? `<div class="nav-item${active === 'health-check' ? ' active' : ''}" onclick="showHealthCheck()">
|
|
568
|
+
<span class="nav-icon">🩺</span>Health Check
|
|
569
|
+
${healthHasIssue ? `<span class="nav-badge" style="background:var(--red-bg);color:var(--red-text)">${healthHasIssue}</span>` : ''}
|
|
570
|
+
</div>` : ''
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function renderSection(id, items, icon, q, active) {
|
|
574
|
+
const el = document.getElementById(id)
|
|
575
|
+
if (!items?.length) { el.innerHTML = '<div class="nav-empty">No docs yet</div>'; return }
|
|
576
|
+
const filtered = q ? items.filter(i => i.name.toLowerCase().includes(q)) : items
|
|
577
|
+
el.innerHTML = filtered.length
|
|
578
|
+
? filtered.map(i => navItem(i.path, icon, fmtName(i.name), active === i.path)).join('')
|
|
579
|
+
: ''
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function navItem(p, icon, label, isActive) {
|
|
583
|
+
const isStale = state.staleSet.has(p)
|
|
584
|
+
const staleTag = isStale ? '<span class="nav-stale">stale</span>' : ''
|
|
585
|
+
return `<div class="nav-item${isActive ? ' active' : ''}" onclick="loadFile('${ea(p)}')">`
|
|
586
|
+
+ `<span class="nav-icon">${icon}</span>${esc(label)}${staleTag}</div>`
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function fmtName(n) {
|
|
590
|
+
return n.replace(/^ADR-\d+-/, 'ADR: ').replace(/[-_]/g, ' ')
|
|
591
|
+
.replace(/\b\w/g, c => c.toUpperCase())
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function matches(str, q) { return !q || str.toLowerCase().includes(q) }
|
|
595
|
+
|
|
596
|
+
// ── Doc viewer ─────────────────────────────────────────────────────────────
|
|
597
|
+
async function loadFile(filePath) {
|
|
598
|
+
if (!filePath) return
|
|
599
|
+
state.active = filePath
|
|
600
|
+
state.view = 'docs'
|
|
601
|
+
setHash('doc/' + filePath)
|
|
602
|
+
renderNav()
|
|
603
|
+
|
|
604
|
+
const el = document.getElementById('doc-content')
|
|
605
|
+
el.innerHTML = '<div style="padding:40px;text-align:center"><span class="spinner"></span></div>'
|
|
606
|
+
|
|
607
|
+
const r = await fetch(`/api/content?path=${encodeURIComponent(filePath)}`)
|
|
608
|
+
if (!r.ok) { el.innerHTML = `<p style="color:var(--red);padding:40px">Could not load: ${esc(filePath)}</p>`; return }
|
|
609
|
+
|
|
610
|
+
const text = await r.text()
|
|
611
|
+
el.innerHTML = marked.parse(text)
|
|
612
|
+
document.getElementById('main').scrollTo({ top: 0 })
|
|
613
|
+
await mermaid.run({ nodes: document.querySelectorAll('#doc-content .mermaid') })
|
|
614
|
+
|
|
615
|
+
if (filePath === 'ARCHITECTURE.md' && isArchTemplate(text)) {
|
|
616
|
+
const banner = document.createElement('div')
|
|
617
|
+
banner.id = 'arch-gen-banner'
|
|
618
|
+
banner.style.cssText = 'position:sticky;bottom:0;background:var(--bg);border-top:1px solid var(--border);padding:12px 24px;display:flex;align-items:center;gap:14px;flex-wrap:wrap'
|
|
619
|
+
banner.innerHTML = `
|
|
620
|
+
<span style="font-size:13px;color:var(--text-muted);flex:1">La arquitectura tiene contenido de plantilla sin completar.</span>
|
|
621
|
+
<button id="arch-gen-btn" onclick="generateArch()" style="padding:8px 18px;background:var(--accent);color:white;border:none;border-radius:var(--radius);font-size:13px;font-weight:600;cursor:pointer;white-space:nowrap">✨ Fill in with AI</button>`
|
|
622
|
+
document.getElementById('doc-wrap').appendChild(banner)
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function showWelcome() {
|
|
627
|
+
const hasModules = state.tree?.modules?.length > 0
|
|
628
|
+
if (!hasModules) {
|
|
629
|
+
showBootstrap()
|
|
630
|
+
} else {
|
|
631
|
+
document.getElementById('doc-content').innerHTML = `
|
|
632
|
+
<div class="empty-state">
|
|
633
|
+
<div class="empty-icon">📋</div>
|
|
634
|
+
<h2>Welcome to DocuTrack</h2>
|
|
635
|
+
<p>Living documentation for your project. Select a page from the sidebar to get started.</p>
|
|
636
|
+
</div>`
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function showBootstrap() {
|
|
641
|
+
state.active = 'bootstrap'
|
|
642
|
+
const el = document.getElementById('doc-content')
|
|
643
|
+
el.innerHTML = `
|
|
644
|
+
<div class="bootstrap-wrap">
|
|
645
|
+
<div class="bootstrap-hero">
|
|
646
|
+
<div class="bootstrap-hero-icon">🚀</div>
|
|
647
|
+
<h1>Set up your docs</h1>
|
|
648
|
+
<p>This project doesn't have documentation yet. Let's generate it from your existing code in two steps.</p>
|
|
649
|
+
</div>
|
|
650
|
+
|
|
651
|
+
<div class="bootstrap-steps">
|
|
652
|
+
<div class="bootstrap-step done" id="step-init">
|
|
653
|
+
<span class="step-num">1</span>
|
|
654
|
+
<div class="step-content">
|
|
655
|
+
<div class="step-title">DocuTrack initialized</div>
|
|
656
|
+
<div class="step-desc">Hooks, templates, and slash commands are installed.</div>
|
|
657
|
+
</div>
|
|
658
|
+
<span class="step-check">✅</span>
|
|
659
|
+
</div>
|
|
660
|
+
|
|
661
|
+
<div class="bootstrap-step active" id="step-scan">
|
|
662
|
+
<span class="step-num">2</span>
|
|
663
|
+
<div class="step-content">
|
|
664
|
+
<div class="step-title">Scan your source files</div>
|
|
665
|
+
<div class="step-desc">Queues all existing files so the AI agent knows what to document.</div>
|
|
666
|
+
</div>
|
|
667
|
+
</div>
|
|
668
|
+
|
|
669
|
+
<div class="bootstrap-step" id="step-agent">
|
|
670
|
+
<span class="step-num">3</span>
|
|
671
|
+
<div class="step-content">
|
|
672
|
+
<div class="step-title">Run the documentalista agent</div>
|
|
673
|
+
<div class="step-desc">Tell Claude Code to document everything — docs appear here in real-time.</div>
|
|
674
|
+
</div>
|
|
675
|
+
</div>
|
|
676
|
+
</div>
|
|
677
|
+
|
|
678
|
+
<button class="bootstrap-btn" id="scan-btn" onclick="runScan()">
|
|
679
|
+
<span id="scan-btn-icon">⚡</span>
|
|
680
|
+
<span id="scan-btn-label">Scan project files</span>
|
|
681
|
+
</button>
|
|
682
|
+
|
|
683
|
+
<div id="scan-result" style="display:none"></div>
|
|
684
|
+
</div>`
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
async function runScan() {
|
|
688
|
+
const btn = document.getElementById('scan-btn')
|
|
689
|
+
const icon = document.getElementById('scan-btn-icon')
|
|
690
|
+
const label = document.getElementById('scan-btn-label')
|
|
691
|
+
btn.disabled = true
|
|
692
|
+
icon.innerHTML = '<span class="spinner" style="width:16px;height:16px;border-width:2px;border-color:rgba(255,255,255,.3);border-top-color:white"></span>'
|
|
693
|
+
label.textContent = 'Scanning…'
|
|
694
|
+
|
|
695
|
+
let data
|
|
696
|
+
try {
|
|
697
|
+
const r = await fetch('/api/scan', { method: 'POST' })
|
|
698
|
+
data = await r.json()
|
|
699
|
+
} catch {
|
|
700
|
+
btn.disabled = false
|
|
701
|
+
icon.textContent = '⚡'
|
|
702
|
+
label.textContent = 'Scan project files'
|
|
703
|
+
return
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Mark step 2 done, activate step 3
|
|
707
|
+
document.getElementById('step-scan').classList.remove('active')
|
|
708
|
+
document.getElementById('step-scan').classList.add('done')
|
|
709
|
+
document.getElementById('step-scan').innerHTML += '<span class="step-check">✅</span>'
|
|
710
|
+
document.getElementById('step-agent').classList.add('active')
|
|
711
|
+
|
|
712
|
+
// Update button to disabled with success state
|
|
713
|
+
btn.style.background = 'var(--green)'
|
|
714
|
+
btn.innerHTML = `✓ ${data.queued} file${data.queued !== 1 ? 's' : ''} queued`
|
|
715
|
+
|
|
716
|
+
// Show result + Claude command
|
|
717
|
+
const preview = data.files?.slice(0, 4).map(f => ` ${f}`).join('\n')
|
|
718
|
+
+ (data.hasMore ? `\n …and more` : '')
|
|
719
|
+
|
|
720
|
+
document.getElementById('scan-result').style.display = 'block'
|
|
721
|
+
document.getElementById('scan-result').innerHTML = `
|
|
722
|
+
<div class="bootstrap-result">
|
|
723
|
+
<div class="bootstrap-result-title">✓ ${data.queued} files queued${data.skipped > 0 ? ` (${data.skipped} already queued)` : ''}</div>
|
|
724
|
+
${preview ? `<div class="bootstrap-result-files">${esc(preview)}</div>` : ''}
|
|
725
|
+
</div>
|
|
726
|
+
<div id="step3-generate" style="margin-top:16px">
|
|
727
|
+
<div style="display:flex;justify-content:center;gap:0;margin-bottom:12px;border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;width:fit-content;margin-left:auto;margin-right:auto">
|
|
728
|
+
<button id="lang-es" onclick="setLang('es')" style="padding:6px 18px;border:none;background:${state.lang==='es'?'var(--accent)':'var(--bg-sidebar)'};color:${state.lang==='es'?'white':'var(--text-muted)'};font-size:13px;font-weight:600;cursor:pointer;transition:background .15s">🇪🇸 Español</button>
|
|
729
|
+
<button id="lang-en" onclick="setLang('en')" style="padding:6px 18px;border:none;background:${state.lang==='en'?'var(--accent)':'var(--bg-sidebar)'};color:${state.lang==='en'?'white':'var(--text-muted)'};font-size:13px;cursor:pointer;transition:background .15s">🇺🇸 English</button>
|
|
730
|
+
</div>
|
|
731
|
+
<button class="bootstrap-btn" id="gen-btn" onclick="runGenerate()">
|
|
732
|
+
<span id="gen-btn-icon">✨</span>
|
|
733
|
+
<span id="gen-btn-label">Generate docs</span>
|
|
734
|
+
</button>
|
|
735
|
+
</div>`
|
|
736
|
+
|
|
737
|
+
// Refresh status badge
|
|
738
|
+
await refreshStatus()
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
async function runGenerate(force = false) {
|
|
742
|
+
const btn = document.getElementById('gen-btn')
|
|
743
|
+
const icon = document.getElementById('gen-btn-icon')
|
|
744
|
+
const label = document.getElementById('gen-btn-label')
|
|
745
|
+
if (btn) btn.disabled = true
|
|
746
|
+
if (icon) icon.innerHTML = '<span class="spinner" style="width:16px;height:16px;border-width:2px;border-color:rgba(255,255,255,.3);border-top-color:white"></span>'
|
|
747
|
+
if (label) label.textContent = 'Preparing…'
|
|
748
|
+
|
|
749
|
+
try {
|
|
750
|
+
await fetch('/api/generate', {
|
|
751
|
+
method: 'POST',
|
|
752
|
+
headers: { 'Content-Type': 'application/json' },
|
|
753
|
+
body: JSON.stringify({ lang: state.lang || 'es', force }),
|
|
754
|
+
})
|
|
755
|
+
} catch { /* ok — trigger file not critical */ }
|
|
756
|
+
|
|
757
|
+
const langStr = state.lang === 'es' ? 'en español' : 'in English'
|
|
758
|
+
const cmd = force
|
|
759
|
+
? `Delete all existing docs and run /documentalista to regenerate everything ${langStr}`
|
|
760
|
+
: '/documentalista'
|
|
761
|
+
|
|
762
|
+
let copied = false
|
|
763
|
+
try { await navigator.clipboard.writeText(cmd); copied = true } catch { /* blocked */ }
|
|
764
|
+
|
|
765
|
+
const wrap = document.getElementById('step3-generate')
|
|
766
|
+
if (wrap) wrap.innerHTML = `
|
|
767
|
+
<div style="text-align:center;padding:24px 0 8px">
|
|
768
|
+
<div style="font-size:36px;margin-bottom:10px">${copied ? '✓' : '📋'}</div>
|
|
769
|
+
<div style="font-size:15px;font-weight:700;margin-bottom:6px;color:var(--green-text)">${copied ? 'Copied to clipboard!' : 'Ready — copy this command'}</div>
|
|
770
|
+
<div style="font-size:13px;color:var(--text-muted);margin-bottom:20px">Switch to Claude Code and press
|
|
771
|
+
<code style="background:var(--border);padding:1px 6px;border-radius:3px;font-size:12px">Ctrl+V</code> then
|
|
772
|
+
<code style="background:var(--border);padding:1px 6px;border-radius:3px;font-size:12px">Enter</code>
|
|
773
|
+
</div>
|
|
774
|
+
<div class="bootstrap-claude">
|
|
775
|
+
<div class="bootstrap-claude-label">${copied ? 'In your clipboard' : 'Command'}</div>
|
|
776
|
+
<div class="bootstrap-claude-cmd" onclick="copyGenCmd(this)">${esc(cmd)}</div>
|
|
777
|
+
<p class="copy-hint">Click to copy again if needed</p>
|
|
778
|
+
</div>
|
|
779
|
+
</div>`
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function setLang(lang) {
|
|
783
|
+
state.lang = lang
|
|
784
|
+
const es = document.getElementById('lang-es')
|
|
785
|
+
const en = document.getElementById('lang-en')
|
|
786
|
+
if (es && en) {
|
|
787
|
+
es.style.background = lang === 'es' ? 'var(--accent)' : 'var(--bg-sidebar)'
|
|
788
|
+
es.style.color = lang === 'es' ? 'white' : 'var(--text-muted)'
|
|
789
|
+
en.style.background = lang === 'en' ? 'var(--accent)' : 'var(--bg-sidebar)'
|
|
790
|
+
en.style.color = lang === 'en' ? 'white' : 'var(--text-muted)'
|
|
791
|
+
}
|
|
792
|
+
document.querySelectorAll('[data-cmd-type]').forEach(el => {
|
|
793
|
+
el.textContent = genCmdText(el.dataset.cmdType)
|
|
794
|
+
})
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
function showRegenPanel() {
|
|
801
|
+
state.active = 'regen'
|
|
802
|
+
setHash('regen')
|
|
803
|
+
renderNav()
|
|
804
|
+
const lang = state.lang || 'es'
|
|
805
|
+
const el = document.getElementById('doc-content')
|
|
806
|
+
el.innerHTML = `
|
|
807
|
+
<div style="max-width:540px;margin:60px auto;padding:0 24px">
|
|
808
|
+
<h2 style="font-size:20px;font-weight:700;margin-bottom:8px">Regenerar documentación</h2>
|
|
809
|
+
<p style="color:var(--text-muted);font-size:14px;margin-bottom:28px">Dile a Claude Code que regenere todos los docs desde los archivos fuente.</p>
|
|
810
|
+
|
|
811
|
+
<div style="margin-bottom:20px">
|
|
812
|
+
<div style="font-size:13px;font-weight:600;margin-bottom:8px">Idioma</div>
|
|
813
|
+
<div style="display:flex;gap:0;border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;width:fit-content">
|
|
814
|
+
<button id="lang-es" onclick="setLang('es')" style="padding:8px 20px;border:none;background:${lang==='es'?'var(--accent)':'var(--bg-sidebar)'};color:${lang==='es'?'white':'var(--text-muted)'};font-size:13px;font-weight:600;cursor:pointer">🇪🇸 Español</button>
|
|
815
|
+
<button id="lang-en" onclick="setLang('en')" style="padding:8px 20px;border:none;background:${lang==='en'?'var(--accent)':'var(--bg-sidebar)'};color:${lang==='en'?'white':'var(--text-muted)'};font-size:13px;cursor:pointer">🇺🇸 English</button>
|
|
816
|
+
</div>
|
|
817
|
+
</div>
|
|
818
|
+
|
|
819
|
+
<button class="bootstrap-btn" id="gen-btn" onclick="runGenerate(true)">
|
|
820
|
+
<span id="gen-btn-icon">↻</span>
|
|
821
|
+
<span id="gen-btn-label">Regenerate all docs</span>
|
|
822
|
+
</button>
|
|
823
|
+
<div id="step3-generate" style="margin-top:16px"></div>
|
|
824
|
+
</div>`
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function isArchTemplate(text) {
|
|
828
|
+
// Solo es plantilla si tiene comentarios HTML placeholder O filas de tabla completamente vacías
|
|
829
|
+
const hasComments = /<!--\s*(Describe|describe|TODO|todo)/.test(text)
|
|
830
|
+
const emptyRows = (text.match(/^\|(\s*\|){3,}\s*$/mg) || []).length
|
|
831
|
+
return hasComments || emptyRows >= 3
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
async function generateArch() {
|
|
835
|
+
const btn = document.getElementById('arch-gen-btn')
|
|
836
|
+
if (btn) { btn.disabled = true; btn.textContent = '⏳ Copying…' }
|
|
837
|
+
|
|
838
|
+
try {
|
|
839
|
+
await fetch('/api/generate', {
|
|
840
|
+
method: 'POST',
|
|
841
|
+
headers: { 'Content-Type': 'application/json' },
|
|
842
|
+
body: JSON.stringify({ lang: state.lang || 'es', type: 'arch' }),
|
|
843
|
+
})
|
|
844
|
+
} catch { /* ok */ }
|
|
845
|
+
|
|
846
|
+
const langStr = state.lang === 'es' ? 'en español' : 'in English'
|
|
847
|
+
const cmd = `Fill in ARCHITECTURE.md for this project based on the actual source code and package.json. Write it ${langStr}.`
|
|
848
|
+
|
|
849
|
+
let copied = false
|
|
850
|
+
try { await navigator.clipboard.writeText(cmd); copied = true } catch { /* ok */ }
|
|
851
|
+
|
|
852
|
+
const banner = document.getElementById('arch-gen-banner')
|
|
853
|
+
if (banner) banner.innerHTML = `
|
|
854
|
+
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;width:100%">
|
|
855
|
+
<span style="font-size:22px">${copied ? '✓' : '📋'}</span>
|
|
856
|
+
<div style="flex:1;min-width:180px">
|
|
857
|
+
<div style="font-size:13px;font-weight:600;color:var(--green-text);margin-bottom:6px">${copied ? 'Copied! Switch to Claude Code and press Ctrl+V → Enter' : 'Run this in Claude Code:'}</div>
|
|
858
|
+
<div class="bootstrap-claude-cmd" onclick="copyGenCmd(this)" style="font-size:12px">${esc(cmd)}</div>
|
|
859
|
+
</div>
|
|
860
|
+
</div>`
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function goHome() {
|
|
864
|
+
if (state.tree?.architecture) loadFile(state.tree.architecture)
|
|
865
|
+
else showWelcome()
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// ── API Explorer ───────────────────────────────────────────────────────────
|
|
869
|
+
async function showApiExplorer() {
|
|
870
|
+
state.active = 'api-explorer'
|
|
871
|
+
state.view = 'api-explorer'
|
|
872
|
+
setHash('api-explorer')
|
|
873
|
+
renderNav()
|
|
874
|
+
|
|
875
|
+
const el = document.getElementById('doc-content')
|
|
876
|
+
el.innerHTML = '<div style="padding:40px;text-align:center"><span class="spinner"></span></div>'
|
|
877
|
+
document.getElementById('main').scrollTo({ top: 0 })
|
|
878
|
+
|
|
879
|
+
if (!api.spec) {
|
|
880
|
+
const spec = await get('/api/openapi')
|
|
881
|
+
api.spec = spec
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
renderApiExplorer()
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function renderApiExplorer() {
|
|
888
|
+
const el = document.getElementById('doc-content')
|
|
889
|
+
|
|
890
|
+
if (!api.spec || api.spec.error) {
|
|
891
|
+
el.innerHTML = `
|
|
892
|
+
<div class="empty-state">
|
|
893
|
+
<div class="empty-icon">⚡</div>
|
|
894
|
+
<h2>No API spec found</h2>
|
|
895
|
+
<p>Run the analyzer to auto-detect routes from your source code.</p>
|
|
896
|
+
<code>npx docutrack analyze</code>
|
|
897
|
+
<p style="margin-top:12px;font-size:13px">Or create <code>docs/api/openapi.json</code> manually (OpenAPI 3.0).</p>
|
|
898
|
+
</div>`
|
|
899
|
+
return
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const { info, paths } = api.spec
|
|
903
|
+
const groups = groupByTag(paths)
|
|
904
|
+
|
|
905
|
+
const totalEp = Object.values(paths).reduce((n, p) => n + Object.keys(p).length, 0)
|
|
906
|
+
|
|
907
|
+
el.innerHTML = `
|
|
908
|
+
<div class="api-explorer">
|
|
909
|
+
<div class="api-explorer-header">
|
|
910
|
+
<div>
|
|
911
|
+
<h1 style="margin-bottom:4px">
|
|
912
|
+
${esc(info.title)}
|
|
913
|
+
<span style="font-size:14px;color:var(--text-muted);font-weight:400">v${esc(info.version)}</span>
|
|
914
|
+
</h1>
|
|
915
|
+
<p style="color:var(--text-muted);font-size:13px;margin:4px 0 0">
|
|
916
|
+
${totalEp} endpoint${totalEp !== 1 ? 's' : ''} detected
|
|
917
|
+
</p>
|
|
918
|
+
</div>
|
|
919
|
+
<div class="base-url-wrap">
|
|
920
|
+
<span class="base-url-label">Base URL</span>
|
|
921
|
+
<div class="base-url-row">
|
|
922
|
+
<input class="base-url-input" id="base-url-input" type="text"
|
|
923
|
+
value="${ea(api.baseUrl)}" placeholder="http://localhost:3000"
|
|
924
|
+
oninput="api.baseUrl=this.value;localStorage.setItem('dt-baseUrl',this.value)">
|
|
925
|
+
<button class="auth-toggle" onclick="toggleAuthRow()">🔑 Auth</button>
|
|
926
|
+
</div>
|
|
927
|
+
<div class="auth-row${api.showAuth ? ' show' : ''}" id="auth-row">
|
|
928
|
+
<span class="auth-label">Bearer</span>
|
|
929
|
+
<input class="auth-input" id="auth-input" type="text" placeholder="your-token-here"
|
|
930
|
+
value="${ea(api.token)}"
|
|
931
|
+
oninput="api.token=this.value;localStorage.setItem('dt-token',this.value)">
|
|
932
|
+
</div>
|
|
933
|
+
</div>
|
|
934
|
+
</div>
|
|
935
|
+
${Object.entries(groups).map(([tag, eps]) => renderApiGroup(tag, eps)).join('')}
|
|
936
|
+
</div>`
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function groupByTag(paths) {
|
|
940
|
+
const groups = {}
|
|
941
|
+
if (!paths) return groups
|
|
942
|
+
for (const [path, pathItem] of Object.entries(paths)) {
|
|
943
|
+
for (const method of ['get','post','put','patch','delete','options','head']) {
|
|
944
|
+
if (!pathItem[method]) continue
|
|
945
|
+
const op = pathItem[method]
|
|
946
|
+
const tag = op.tags?.[0] || 'default'
|
|
947
|
+
if (!groups[tag]) groups[tag] = []
|
|
948
|
+
groups[tag].push({ path, method: method.toUpperCase(), ...op })
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
return groups
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function renderApiGroup(tag, endpoints) {
|
|
955
|
+
return `
|
|
956
|
+
<div class="api-group">
|
|
957
|
+
<div class="api-group-title">${esc(tag)}</div>
|
|
958
|
+
${endpoints.map(ep => renderEndpointCard(ep)).join('')}
|
|
959
|
+
</div>`
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function renderEndpointCard(ep) {
|
|
963
|
+
const id = epId(ep.method, ep.path)
|
|
964
|
+
const isOpen = api.open[id]
|
|
965
|
+
const params = ep.parameters || []
|
|
966
|
+
const hasBody = !!ep.requestBody
|
|
967
|
+
const pathParams = params.filter(p => p.in === 'path')
|
|
968
|
+
const queryParams = params.filter(p => p.in === 'query')
|
|
969
|
+
const headerParams = params.filter(p => p.in === 'header')
|
|
970
|
+
|
|
971
|
+
return `
|
|
972
|
+
<div class="endpoint-card${isOpen ? ' open' : ''}" id="card-${id}">
|
|
973
|
+
<div class="endpoint-header" onclick="toggleEndpoint('${id}')">
|
|
974
|
+
<span class="method-badge badge-${ep.method.toLowerCase()}">${ep.method}</span>
|
|
975
|
+
<span class="endpoint-path">${esc(ep.path)}</span>
|
|
976
|
+
${ep.summary ? `<span class="endpoint-summary">${esc(ep.summary)}</span>` : ''}
|
|
977
|
+
<span class="endpoint-chevron">›</span>
|
|
978
|
+
</div>
|
|
979
|
+
<div class="endpoint-body">
|
|
980
|
+
${ep.description ? `<div class="endpoint-section"><p style="color:var(--text-muted);font-size:14px;margin:0">${esc(ep.description)}</p></div>` : ''}
|
|
981
|
+
${params.length ? `
|
|
982
|
+
<div class="endpoint-section">
|
|
983
|
+
<div class="section-title">Parameters</div>
|
|
984
|
+
<table class="params-table">
|
|
985
|
+
<thead><tr><th>Name</th><th>In</th><th>Type</th><th>Required</th></tr></thead>
|
|
986
|
+
<tbody>${params.map(p => `
|
|
987
|
+
<tr>
|
|
988
|
+
<td>${esc(p.name)}</td>
|
|
989
|
+
<td>${esc(p.in)}</td>
|
|
990
|
+
<td>${esc(p.schema?.type || 'string')}</td>
|
|
991
|
+
<td>${p.required ? '<span class="param-required">required</span>' : '<span class="param-optional">optional</span>'}</td>
|
|
992
|
+
</tr>`).join('')}
|
|
993
|
+
</tbody>
|
|
994
|
+
</table>
|
|
995
|
+
</div>` : ''}
|
|
996
|
+
${hasBody ? `
|
|
997
|
+
<div class="endpoint-section">
|
|
998
|
+
<div class="section-title">Request Body</div>
|
|
999
|
+
<pre style="background:var(--code-bg);padding:12px;border-radius:var(--radius);font-size:12px;overflow-x:auto">${esc(JSON.stringify(ep.requestBody?.content?.['application/json']?.schema || {}, null, 2))}</pre>
|
|
1000
|
+
</div>` : ''}
|
|
1001
|
+
${ep.responses ? `
|
|
1002
|
+
<div class="endpoint-section">
|
|
1003
|
+
<div class="section-title">Responses</div>
|
|
1004
|
+
${Object.entries(ep.responses).map(([code, r]) => `
|
|
1005
|
+
<div style="display:flex;align-items:center;gap:10px;margin-bottom:6px;font-size:13px">
|
|
1006
|
+
<span class="status-chip ${statusClass(code)}">${code}</span>
|
|
1007
|
+
<span style="color:var(--text-muted)">${esc(r.description || '')}</span>
|
|
1008
|
+
</div>`).join('')}
|
|
1009
|
+
</div>` : ''}
|
|
1010
|
+
<div class="try-section">
|
|
1011
|
+
${renderTrySection(id, ep, pathParams, queryParams, headerParams, hasBody)}
|
|
1012
|
+
</div>
|
|
1013
|
+
</div>
|
|
1014
|
+
</div>`
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function renderTrySection(id, ep, pathParams, queryParams, headerParams, hasBody) {
|
|
1018
|
+
const tryOpen = api.tryOpen[id]
|
|
1019
|
+
const resp = api.responses[id]
|
|
1020
|
+
|
|
1021
|
+
return `
|
|
1022
|
+
<button class="try-btn" onclick="toggleTry('${id}',event)">
|
|
1023
|
+
${tryOpen ? '✕ Close' : '▶ Try it out'}
|
|
1024
|
+
</button>
|
|
1025
|
+
<div class="try-form${tryOpen ? ' show' : ''}" id="try-${id}">
|
|
1026
|
+
${pathParams.map(p => `
|
|
1027
|
+
<div class="try-field">
|
|
1028
|
+
<label>${esc(p.name)} <span style="color:var(--red-text);font-size:10px">path</span></label>
|
|
1029
|
+
<input type="text" id="${id}-path-${esc(p.name)}" placeholder="${esc(p.name)}">
|
|
1030
|
+
</div>`).join('')}
|
|
1031
|
+
${queryParams.map(p => `
|
|
1032
|
+
<div class="try-field">
|
|
1033
|
+
<label>${esc(p.name)} <span style="color:var(--text-muted);font-size:10px">query</span></label>
|
|
1034
|
+
<input type="text" id="${id}-query-${esc(p.name)}" placeholder="${esc(p.schema?.type || 'string')}">
|
|
1035
|
+
</div>`).join('')}
|
|
1036
|
+
${hasBody ? `
|
|
1037
|
+
<div class="try-field">
|
|
1038
|
+
<label>Request Body <span style="color:var(--text-muted);font-size:10px">application/json</span></label>
|
|
1039
|
+
<textarea id="${id}-body" placeholder="{}"></textarea>
|
|
1040
|
+
</div>` : ''}
|
|
1041
|
+
<div class="try-actions">
|
|
1042
|
+
<button class="execute-btn" id="exec-${id}" onclick="executeRequest('${id}','${esc(ep.method)}','${ea(ep.path)}',${hasBody})">
|
|
1043
|
+
▶ Execute
|
|
1044
|
+
</button>
|
|
1045
|
+
<button class="cancel-try-btn" onclick="toggleTry('${id}',event)">Cancel</button>
|
|
1046
|
+
</div>
|
|
1047
|
+
${resp ? renderResponse(resp) : ''}
|
|
1048
|
+
</div>`
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function renderResponse(resp) {
|
|
1052
|
+
const cls = resp.status >= 500 ? 'status-5xx'
|
|
1053
|
+
: resp.status >= 400 ? 'status-4xx'
|
|
1054
|
+
: resp.status >= 300 ? 'status-3xx'
|
|
1055
|
+
: 'status-2xx'
|
|
1056
|
+
|
|
1057
|
+
let body = resp.body
|
|
1058
|
+
try { body = JSON.stringify(JSON.parse(body), null, 2) } catch { /* raw */ }
|
|
1059
|
+
|
|
1060
|
+
return `
|
|
1061
|
+
<div class="response-panel">
|
|
1062
|
+
<div class="response-bar">
|
|
1063
|
+
<span class="status-chip ${cls}">${resp.status} ${resp.statusText}</span>
|
|
1064
|
+
<span style="color:var(--text-muted);font-size:12px">${resp.contentType || ''}</span>
|
|
1065
|
+
<span class="response-time">${resp.ms}ms</span>
|
|
1066
|
+
</div>
|
|
1067
|
+
<div class="response-body">${esc(body)}</div>
|
|
1068
|
+
</div>
|
|
1069
|
+
${resp.corsError ? `<div class="cors-warn">⚠ Could not reach <code>${esc(api.baseUrl)}</code> — either the server is not running, or it is blocking cross-origin requests. Start your app server and make sure it allows <code>Access-Control-Allow-Origin: *</code>.</div>` : ''}`
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// ── Execute request ────────────────────────────────────────────────────────
|
|
1073
|
+
async function executeRequest(id, method, pathTemplate, hasBody) {
|
|
1074
|
+
const btn = document.getElementById(`exec-${id}`)
|
|
1075
|
+
btn.disabled = true
|
|
1076
|
+
btn.textContent = '…'
|
|
1077
|
+
|
|
1078
|
+
// Build URL
|
|
1079
|
+
let url = api.baseUrl.replace(/\/$/, '') + pathTemplate
|
|
1080
|
+
// Substitute path params
|
|
1081
|
+
url = url.replace(/\{([^}]+)\}/g, (_, name) => {
|
|
1082
|
+
const input = document.getElementById(`${id}-path-${name}`)
|
|
1083
|
+
return input ? encodeURIComponent(input.value || `:${name}`) : `:${name}`
|
|
1084
|
+
})
|
|
1085
|
+
|
|
1086
|
+
// Collect query params from spec
|
|
1087
|
+
const queryEl = document.querySelectorAll(`[id^="${id}-query-"]`)
|
|
1088
|
+
const qs = []
|
|
1089
|
+
queryEl.forEach(el => {
|
|
1090
|
+
const key = el.id.replace(`${id}-query-`, '')
|
|
1091
|
+
if (el.value) qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(el.value)}`)
|
|
1092
|
+
})
|
|
1093
|
+
if (qs.length) url += '?' + qs.join('&')
|
|
1094
|
+
|
|
1095
|
+
const headers = { 'Content-Type': 'application/json' }
|
|
1096
|
+
if (api.token) headers['Authorization'] = `Bearer ${api.token}`
|
|
1097
|
+
|
|
1098
|
+
let body = undefined
|
|
1099
|
+
if (hasBody) {
|
|
1100
|
+
const bodyEl = document.getElementById(`${id}-body`)
|
|
1101
|
+
body = bodyEl?.value?.trim() || '{}'
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
const t0 = Date.now()
|
|
1105
|
+
let resp
|
|
1106
|
+
|
|
1107
|
+
try {
|
|
1108
|
+
const fetchResp = await fetch(url, {
|
|
1109
|
+
method,
|
|
1110
|
+
headers,
|
|
1111
|
+
body: body && method !== 'GET' ? body : undefined,
|
|
1112
|
+
})
|
|
1113
|
+
const text = await fetchResp.text()
|
|
1114
|
+
resp = {
|
|
1115
|
+
status: fetchResp.status,
|
|
1116
|
+
statusText: fetchResp.statusText,
|
|
1117
|
+
contentType: fetchResp.headers.get('content-type') || '',
|
|
1118
|
+
body: text,
|
|
1119
|
+
ms: Date.now() - t0,
|
|
1120
|
+
corsError: false,
|
|
1121
|
+
}
|
|
1122
|
+
} catch (err) {
|
|
1123
|
+
const isCors = err.message?.toLowerCase().includes('fetch') || err.name === 'TypeError'
|
|
1124
|
+
resp = {
|
|
1125
|
+
status: 0,
|
|
1126
|
+
statusText: 'Network Error',
|
|
1127
|
+
contentType: '',
|
|
1128
|
+
body: err.message,
|
|
1129
|
+
ms: Date.now() - t0,
|
|
1130
|
+
corsError: isCors,
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
api.responses[id] = resp
|
|
1135
|
+
btn.disabled = false
|
|
1136
|
+
btn.textContent = '▶ Execute'
|
|
1137
|
+
|
|
1138
|
+
// Re-render just the try section by refreshing the response part
|
|
1139
|
+
const tryDiv = document.getElementById(`try-${id}`)
|
|
1140
|
+
// Remove old response panel if present
|
|
1141
|
+
tryDiv.querySelectorAll('.response-panel, .cors-warn').forEach(e => e.remove())
|
|
1142
|
+
tryDiv.insertAdjacentHTML('beforeend', renderResponse(resp))
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// ── Interaction helpers ────────────────────────────────────────────────────
|
|
1146
|
+
function toggleEndpoint(id) {
|
|
1147
|
+
api.open[id] = !api.open[id]
|
|
1148
|
+
const card = document.getElementById(`card-${id}`)
|
|
1149
|
+
if (api.open[id]) card.classList.add('open')
|
|
1150
|
+
else card.classList.remove('open')
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function toggleTry(id, e) {
|
|
1154
|
+
e.stopPropagation()
|
|
1155
|
+
api.tryOpen[id] = !api.tryOpen[id]
|
|
1156
|
+
const form = document.getElementById(`try-${id}`)
|
|
1157
|
+
if (api.tryOpen[id]) form.classList.add('show')
|
|
1158
|
+
else form.classList.remove('show')
|
|
1159
|
+
// Update button label
|
|
1160
|
+
const btn = form.previousElementSibling
|
|
1161
|
+
if (btn?.classList.contains('try-btn')) {
|
|
1162
|
+
btn.textContent = api.tryOpen[id] ? '✕ Close' : '▶ Try it out'
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
function toggleAuthRow() {
|
|
1167
|
+
api.showAuth = !api.showAuth
|
|
1168
|
+
document.getElementById('auth-row').classList.toggle('show', api.showAuth)
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// ── Status ─────────────────────────────────────────────────────────────────
|
|
1172
|
+
async function refreshStatus() {
|
|
1173
|
+
const s = await get('/api/status')
|
|
1174
|
+
if (!s) return
|
|
1175
|
+
const pct = s.coverage
|
|
1176
|
+
document.getElementById('cov-pct').textContent = `${pct}%`
|
|
1177
|
+
document.getElementById('cov-label').textContent = `${pct}%`
|
|
1178
|
+
const bar = document.getElementById('cov-bar')
|
|
1179
|
+
bar.style.width = `${pct}%`
|
|
1180
|
+
bar.className = 'coverage-fill' + (pct < 50 ? ' bad' : pct < 80 ? ' warn' : '')
|
|
1181
|
+
document.getElementById('cov-dot').style.background = pct >= 80 ? 'var(--green)' : pct >= 50 ? 'var(--yellow)' : 'var(--red)'
|
|
1182
|
+
const banner = document.getElementById('pending-banner')
|
|
1183
|
+
if (s.pending > 0) { document.getElementById('pending-count').textContent = s.pending; banner.classList.add('show') }
|
|
1184
|
+
else banner.classList.remove('show')
|
|
1185
|
+
|
|
1186
|
+
// Store stale map for nav rendering
|
|
1187
|
+
state.staleSet = new Set((s.stale || []).map(x => x.doc))
|
|
1188
|
+
renderNav()
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// ── Health Check ───────────────────────────────────────────────────────────
|
|
1192
|
+
async function showHealthCheck() {
|
|
1193
|
+
state.active = 'health-check'
|
|
1194
|
+
state.view = 'health-check'
|
|
1195
|
+
setHash('health-check')
|
|
1196
|
+
renderNav()
|
|
1197
|
+
|
|
1198
|
+
const el = document.getElementById('doc-content')
|
|
1199
|
+
el.innerHTML = '<div style="padding:40px;text-align:center"><span class="spinner"></span></div>'
|
|
1200
|
+
document.getElementById('main').scrollTo({ top: 0 })
|
|
1201
|
+
|
|
1202
|
+
const data = await get('/api/check')
|
|
1203
|
+
if (!data) {
|
|
1204
|
+
el.innerHTML = `<div class="empty-state"><div class="empty-icon">🩺</div><h2>Health check unavailable</h2><p>Make sure DocuTrack is initialized.</p></div>`
|
|
1205
|
+
return
|
|
1206
|
+
}
|
|
1207
|
+
renderHealthCheck(data)
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function renderHealthCheck(data) {
|
|
1211
|
+
const el = document.getElementById('doc-content')
|
|
1212
|
+
const { pending, stale, drift, complexity, ok } = data
|
|
1213
|
+
|
|
1214
|
+
const criticalCount = complexity.critical?.length || 0
|
|
1215
|
+
const highDrift = (drift || []).filter(d => d.severity === 'high').length
|
|
1216
|
+
const totalIssues = pending + stale + highDrift + criticalCount
|
|
1217
|
+
state.healthBadge = totalIssues
|
|
1218
|
+
renderNav()
|
|
1219
|
+
|
|
1220
|
+
const overallClass = ok ? 'health-ok' : totalIssues > 3 ? 'health-crit' : 'health-warn'
|
|
1221
|
+
const overallText = ok ? '✓ Healthy' : `⚠ ${totalIssues} issue${totalIssues !== 1 ? 's' : ''}`
|
|
1222
|
+
|
|
1223
|
+
const scoreCard = (val, label, cls) => `
|
|
1224
|
+
<div class="health-card ${cls}">
|
|
1225
|
+
<div class="health-card-val">${val}</div>
|
|
1226
|
+
<div class="health-card-label">${label}</div>
|
|
1227
|
+
</div>`
|
|
1228
|
+
|
|
1229
|
+
const cards = [
|
|
1230
|
+
scoreCard(pending, 'Pending files', pending > 0 ? 'warn-card' : 'ok'),
|
|
1231
|
+
scoreCard(stale, 'Stale docs', stale > 0 ? 'warn-card' : 'ok'),
|
|
1232
|
+
scoreCard(drift.length, 'Drifted modules', highDrift > 0 ? 'issue' : drift.length > 0 ? 'warn-card' : 'ok'),
|
|
1233
|
+
scoreCard(complexity.summary?.critical || 0, 'Complex files', criticalCount > 0 ? 'issue' : 'ok'),
|
|
1234
|
+
].join('')
|
|
1235
|
+
|
|
1236
|
+
const driftSection = drift.length > 0 ? `
|
|
1237
|
+
<div class="health-section">
|
|
1238
|
+
<div class="health-section-title">📐 Documentation Drift</div>
|
|
1239
|
+
${drift.map(d => `
|
|
1240
|
+
<div class="drift-row">
|
|
1241
|
+
<span class="drift-sev ${d.severity}">${d.severity.toUpperCase()}</span>
|
|
1242
|
+
<div>
|
|
1243
|
+
<div class="drift-module">${esc(d.module)}</div>
|
|
1244
|
+
${d.undocumented?.length ? `<div class="drift-detail">+undocumented: ${d.undocumented.map(n => `<span>${esc(n)}</span>`).join('')}</div>` : ''}
|
|
1245
|
+
${d.orphaned?.length ? `<div class="drift-detail" style="margin-top:3px">−orphaned: ${d.orphaned.map(n => `<span>${esc(n)}</span>`).join('')}</div>` : ''}
|
|
1246
|
+
</div>
|
|
1247
|
+
</div>`).join('')}
|
|
1248
|
+
</div>` : ''
|
|
1249
|
+
|
|
1250
|
+
const complexFiles = complexity.critical || []
|
|
1251
|
+
const complexSection = complexFiles.length > 0 ? `
|
|
1252
|
+
<div class="health-section">
|
|
1253
|
+
<div class="health-section-title">📊 Complexity Hotspots</div>
|
|
1254
|
+
${complexFiles.map(f => {
|
|
1255
|
+
const scoreClass = f.score < 40 ? 'score-bad' : f.score < 70 ? 'score-warn' : 'score-ok'
|
|
1256
|
+
return `
|
|
1257
|
+
<div class="complexity-row">
|
|
1258
|
+
<div class="complexity-score ${scoreClass}">${f.score}</div>
|
|
1259
|
+
<div class="complexity-file" title="${esc(f.file)}">${esc(f.file)}</div>
|
|
1260
|
+
<div class="complexity-tags">
|
|
1261
|
+
${f.warnings.map(w => `<span class="complexity-tag${w.level === 'critical' ? ' crit' : ''}">${esc(w.message)}</span>`).join('')}
|
|
1262
|
+
</div>
|
|
1263
|
+
</div>`}).join('')}
|
|
1264
|
+
</div>` : ''
|
|
1265
|
+
|
|
1266
|
+
const allGood = drift.length === 0 && complexFiles.length === 0 && pending === 0 && stale === 0
|
|
1267
|
+
const emptyState = allGood ? `
|
|
1268
|
+
<div class="health-empty" style="padding:40px">
|
|
1269
|
+
<div style="font-size:48px;margin-bottom:12px">✅</div>
|
|
1270
|
+
<p style="font-size:16px;font-weight:700;color:var(--text)">Documentation is fully healthy.</p>
|
|
1271
|
+
<p>No pending files, no stale docs, no drift, no complexity issues.</p>
|
|
1272
|
+
</div>` : ''
|
|
1273
|
+
|
|
1274
|
+
el.innerHTML = `
|
|
1275
|
+
<div style="max-width:800px">
|
|
1276
|
+
<div class="health-header">
|
|
1277
|
+
<div>
|
|
1278
|
+
<div class="health-title">Health Check</div>
|
|
1279
|
+
<div class="health-subtitle">Documentation quality analysis</div>
|
|
1280
|
+
</div>
|
|
1281
|
+
<div style="display:flex;align-items:center;gap:10px">
|
|
1282
|
+
<span class="health-overall ${overallClass}">${overallText}</span>
|
|
1283
|
+
<button class="health-refresh" onclick="showHealthCheck()">↻ Refresh</button>
|
|
1284
|
+
</div>
|
|
1285
|
+
</div>
|
|
1286
|
+
<div class="health-grid">${cards}</div>
|
|
1287
|
+
${emptyState}
|
|
1288
|
+
${driftSection}
|
|
1289
|
+
${complexSection}
|
|
1290
|
+
${(pending > 0 || stale > 0) ? `
|
|
1291
|
+
<div class="health-section">
|
|
1292
|
+
<div class="health-section-title">💡 Suggested Actions</div>
|
|
1293
|
+
<div style="font-size:13px;color:var(--text-muted);line-height:2">
|
|
1294
|
+
${pending > 0 ? `<div>→ Run the <strong>documentalista</strong> subagent to clear ${pending} pending file${pending !== 1 ? 's' : ''}</div>` : ''}
|
|
1295
|
+
${stale > 0 ? `<div>→ Update ${stale} stale doc${stale !== 1 ? 's' : ''}, then run <code>docutrack clear</code></div>` : ''}
|
|
1296
|
+
${highDrift > 0 ? `<div>→ Add missing exports to docs for high-drift modules above</div>` : ''}
|
|
1297
|
+
${criticalCount > 0 ? `<div>→ Consider splitting the complex files listed above</div>` : ''}
|
|
1298
|
+
</div>
|
|
1299
|
+
</div>` : ''}
|
|
1300
|
+
</div>`
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// ── SSE ─────────────────────────────────────────────────────────────────────
|
|
1304
|
+
function connectSSE() {
|
|
1305
|
+
const es = new EventSource('/events')
|
|
1306
|
+
es.onmessage = async (e) => {
|
|
1307
|
+
if (e.data !== 'reload') return
|
|
1308
|
+
|
|
1309
|
+
await refreshTree()
|
|
1310
|
+
await refreshStatus()
|
|
1311
|
+
|
|
1312
|
+
if (state.view === 'api-explorer') {
|
|
1313
|
+
api.spec = await get('/api/openapi')
|
|
1314
|
+
renderApiExplorer()
|
|
1315
|
+
} else if (state.view === 'health-check') {
|
|
1316
|
+
const data = await get('/api/check')
|
|
1317
|
+
if (data) renderHealthCheck(data)
|
|
1318
|
+
} else if (state.active && state.active !== 'bootstrap' && state.active !== 'regen') {
|
|
1319
|
+
await loadFile(state.active)
|
|
1320
|
+
}
|
|
1321
|
+
// If we were on bootstrap and modules now exist → navigate to arch
|
|
1322
|
+
if (state.active === 'bootstrap' && state.tree?.modules?.length > 0) {
|
|
1323
|
+
if (state.tree.architecture) loadFile(state.tree.architecture)
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
es.onerror = () => setTimeout(connectSSE, 3000)
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// ── Theme ──────────────────────────────────────────────────────────────────
|
|
1330
|
+
document.getElementById('theme-toggle').addEventListener('click', () => {
|
|
1331
|
+
setTheme(document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark')
|
|
1332
|
+
})
|
|
1333
|
+
|
|
1334
|
+
function setTheme(t) {
|
|
1335
|
+
document.documentElement.setAttribute('data-theme', t)
|
|
1336
|
+
document.getElementById('theme-toggle').textContent = t === 'dark' ? '☀️' : '🌙'
|
|
1337
|
+
localStorage.setItem('dt-theme', t)
|
|
1338
|
+
mermaid.initialize({ startOnLoad: false, theme: t === 'dark' ? 'dark' : 'neutral', securityLevel: 'loose' })
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// ── Routing ───────────────────────────────────────────────────────────────
|
|
1342
|
+
function setHash(h) {
|
|
1343
|
+
history.replaceState(null, '', '#' + h)
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function restoreFromHash(h) {
|
|
1347
|
+
if (h === 'api-explorer') { showApiExplorer(); return }
|
|
1348
|
+
if (h === 'health-check') { showHealthCheck(); return }
|
|
1349
|
+
if (h === 'regen') { showRegenPanel(); return }
|
|
1350
|
+
if (h.startsWith('doc/')) { loadFile(h.slice(4)); return }
|
|
1351
|
+
loadFile(h)
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// ── Utils ──────────────────────────────────────────────────────────────────
|
|
1355
|
+
async function get(url) {
|
|
1356
|
+
try { const r = await fetch(url); return r.ok ? r.json() : null } catch { return null }
|
|
1357
|
+
}
|
|
1358
|
+
function esc(s) {
|
|
1359
|
+
return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''')
|
|
1360
|
+
}
|
|
1361
|
+
function ea(s) { return String(s||'').replace(/'/g,"\\'") }
|
|
1362
|
+
function epId(method, path) {
|
|
1363
|
+
return `${method}-${path}`.replace(/[^a-zA-Z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '')
|
|
1364
|
+
}
|
|
1365
|
+
function statusClass(code) {
|
|
1366
|
+
const n = parseInt(code)
|
|
1367
|
+
if (n >= 500) return 'status-5xx'
|
|
1368
|
+
if (n >= 400) return 'status-4xx'
|
|
1369
|
+
if (n >= 300) return 'status-3xx'
|
|
1370
|
+
return 'status-2xx'
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// ── Command Palette ───────────────────────────────────────────────────────────
|
|
1374
|
+
let paletteIdx = 0
|
|
1375
|
+
let paletteItems = []
|
|
1376
|
+
let paletteSearchTimer = null
|
|
1377
|
+
|
|
1378
|
+
function openPalette() {
|
|
1379
|
+
document.getElementById('palette-overlay').classList.add('open')
|
|
1380
|
+
const inp = document.getElementById('palette-input')
|
|
1381
|
+
inp.value = ''
|
|
1382
|
+
inp.focus()
|
|
1383
|
+
renderPaletteResults('')
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function closePalette() {
|
|
1387
|
+
document.getElementById('palette-overlay').classList.remove('open')
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
function onPaletteOverlayClick(e) {
|
|
1391
|
+
if (e.target === document.getElementById('palette-overlay')) closePalette()
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
function renderPaletteResults(q) {
|
|
1395
|
+
const { tree } = state
|
|
1396
|
+
paletteItems = []
|
|
1397
|
+
const lq = q.toLowerCase()
|
|
1398
|
+
|
|
1399
|
+
if (tree) {
|
|
1400
|
+
if (tree.architecture && (!q || 'architecture'.includes(lq))) {
|
|
1401
|
+
paletteItems.push({ icon: '🏗', name: 'Architecture', section: 'Overview', action: () => loadFile(tree.architecture) })
|
|
1402
|
+
}
|
|
1403
|
+
const addItems = (items, icon, section, fn) => {
|
|
1404
|
+
if (!items?.length) return
|
|
1405
|
+
for (const item of items) {
|
|
1406
|
+
const name = fmtName(item.name)
|
|
1407
|
+
if (!q || name.toLowerCase().includes(lq) || item.name.toLowerCase().includes(lq)) {
|
|
1408
|
+
paletteItems.push({ icon, name, section, action: () => fn(item) })
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
addItems(tree.modules, '📦', 'Modules', m => loadFile(m.path))
|
|
1413
|
+
addItems(tree.decisions, '📝', 'Decisions', d => loadFile(d.path))
|
|
1414
|
+
addItems(tree.api, '📄', 'API', a => loadFile(a.path))
|
|
1415
|
+
}
|
|
1416
|
+
if (!q || 'api explorer'.includes(lq)) paletteItems.push({ icon: '⚡', name: 'API Explorer', section: 'Views', action: showApiExplorer })
|
|
1417
|
+
if (!q || 'health check'.includes(lq)) paletteItems.push({ icon: '🩺', name: 'Health Check', section: 'Views', action: showHealthCheck })
|
|
1418
|
+
if (!q || 'regenerate docs'.includes(lq)) paletteItems.push({ icon: '✨', name: 'Regenerate docs', section: 'Actions', action: showRegenPanel })
|
|
1419
|
+
|
|
1420
|
+
paintPalette()
|
|
1421
|
+
|
|
1422
|
+
// Debounced content search
|
|
1423
|
+
clearTimeout(paletteSearchTimer)
|
|
1424
|
+
if (q.length >= 2) {
|
|
1425
|
+
paletteSearchTimer = setTimeout(() => fetchContentResults(q), 250)
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
async function fetchContentResults(q) {
|
|
1430
|
+
try {
|
|
1431
|
+
const r = await fetch(`/api/search?q=${encodeURIComponent(q)}`)
|
|
1432
|
+
if (!r.ok) return
|
|
1433
|
+
const hits = await r.json()
|
|
1434
|
+
if (!hits.length) return
|
|
1435
|
+
// Append content hits (avoid duplicates with name matches)
|
|
1436
|
+
const existing = new Set(paletteItems.map(i => i._path).filter(Boolean))
|
|
1437
|
+
for (const hit of hits) {
|
|
1438
|
+
if (existing.has(hit.path)) continue
|
|
1439
|
+
paletteItems.push({ icon: '🔎', name: hit.title, section: 'Content', snippet: hit.snippet, _path: hit.path, action: () => loadFile(hit.path) })
|
|
1440
|
+
}
|
|
1441
|
+
paintPalette()
|
|
1442
|
+
} catch { /* ok */ }
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
function paintPalette() {
|
|
1446
|
+
const container = document.getElementById('palette-results')
|
|
1447
|
+
if (!container) return
|
|
1448
|
+
|
|
1449
|
+
if (!paletteItems.length) {
|
|
1450
|
+
container.innerHTML = '<div class="palette-empty">No results</div>'
|
|
1451
|
+
return
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
const groups = {}
|
|
1455
|
+
for (const item of paletteItems) {
|
|
1456
|
+
if (!groups[item.section]) groups[item.section] = []
|
|
1457
|
+
groups[item.section].push(item)
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
let flatIdx = 0
|
|
1461
|
+
let html = ''
|
|
1462
|
+
for (const [section, items] of Object.entries(groups)) {
|
|
1463
|
+
html += `<div class="palette-group-header">${esc(section)}</div>`
|
|
1464
|
+
for (const item of items) {
|
|
1465
|
+
const i = flatIdx++
|
|
1466
|
+
const sel = i === paletteIdx ? ' selected' : ''
|
|
1467
|
+
html += `<div class="palette-item${sel}" onmouseenter="paletteIdx=${i};updatePaletteSelection()" onclick="selectPaletteItem(${i})">
|
|
1468
|
+
<span class="palette-item-icon">${item.icon}</span>
|
|
1469
|
+
<span class="palette-item-name">${esc(item.name)}</span>
|
|
1470
|
+
${item.snippet ? `<span class="palette-item-snippet">${esc(item.snippet)}</span>` : `<span class="palette-item-section">${esc(item.section)}</span>`}
|
|
1471
|
+
</div>`
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
container.innerHTML = html
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
function selectPaletteItem(i) {
|
|
1478
|
+
closePalette()
|
|
1479
|
+
paletteItems[i]?.action()
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
function updatePaletteSelection() {
|
|
1483
|
+
document.querySelectorAll('#palette-results .palette-item').forEach((el, i) => {
|
|
1484
|
+
el.classList.toggle('selected', i === paletteIdx)
|
|
1485
|
+
})
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function scrollPaletteItem() {
|
|
1489
|
+
const items = document.querySelectorAll('#palette-results .palette-item')
|
|
1490
|
+
items[paletteIdx]?.scrollIntoView({ block: 'nearest' })
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// ── Claude command helpers ───────────────────────────────────────────────────
|
|
1494
|
+
function genCmdText(type) {
|
|
1495
|
+
const lang = state.lang === 'es' ? 'en español' : 'in English'
|
|
1496
|
+
if (type === 'bootstrap') return `Run the documentalista subagent to document all pending files ${lang}`
|
|
1497
|
+
if (type === 'regen') return `Regenerate all documentation ${lang} from scratch — read each source file and overwrite existing docs`
|
|
1498
|
+
if (type === 'arch') return `Fill in ARCHITECTURE.md based on the actual source code and package.json. Write it ${lang}.`
|
|
1499
|
+
return ''
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
function copyGenCmd(el) {
|
|
1503
|
+
navigator.clipboard.writeText(el.textContent).then(() => {
|
|
1504
|
+
const orig = el.textContent
|
|
1505
|
+
const origBg = el.style.background
|
|
1506
|
+
const origColor = el.style.color
|
|
1507
|
+
el.style.background = 'var(--green-bg)'
|
|
1508
|
+
el.style.color = 'var(--green-text)'
|
|
1509
|
+
el.textContent = '✓ Copiado'
|
|
1510
|
+
setTimeout(() => {
|
|
1511
|
+
el.style.background = origBg
|
|
1512
|
+
el.style.color = origColor
|
|
1513
|
+
el.textContent = orig
|
|
1514
|
+
}, 1200)
|
|
1515
|
+
}).catch(() => {})
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// ── Copy code ────────────────────────────────────────────────────────────────
|
|
1519
|
+
function copyCode(btn) {
|
|
1520
|
+
const code = btn.previousElementSibling?.querySelector('code')
|
|
1521
|
+
if (!code) return
|
|
1522
|
+
navigator.clipboard.writeText(code.textContent).then(() => {
|
|
1523
|
+
btn.textContent = '✓ Copied'
|
|
1524
|
+
btn.classList.add('copied')
|
|
1525
|
+
setTimeout(() => { btn.textContent = 'Copy'; btn.classList.remove('copied') }, 1500)
|
|
1526
|
+
}).catch(() => {})
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// ── Heading anchors ──────────────────────────────────────────────────────────
|
|
1530
|
+
function copyAnchor(a, e) {
|
|
1531
|
+
e.preventDefault()
|
|
1532
|
+
const url = location.href.split('#')[0] + a.getAttribute('href')
|
|
1533
|
+
navigator.clipboard.writeText(url).then(() => {
|
|
1534
|
+
const orig = a.textContent
|
|
1535
|
+
a.textContent = '✓'
|
|
1536
|
+
setTimeout(() => { a.textContent = orig }, 1200)
|
|
1537
|
+
}).catch(() => {
|
|
1538
|
+
location.hash = a.getAttribute('href')
|
|
1539
|
+
})
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
boot()
|
|
1543
|
+
</script>
|
|
1544
|
+
</body>
|
|
1545
|
+
</html>
|