Knot server viewer.
knotview.srv.rbrt.fr
knot
tangled
1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 <title>KnotView</title>
7 <link rel="icon" type="image/svg+xml" href="favicon.svg" />
8 <link rel="stylesheet" href="styles.css" />
9 <link
10 rel="stylesheet"
11 href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css"
12 />
13 <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
14 <script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js"></script>
15 <script
16 defer
17 src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"
18 ></script>
19 <script src="api.js"></script>
20 <script src="app.js"></script>
21 </head>
22 <body>
23 <div class="container" x-data="app" x-init="init()">
24 <header>
25 <div class="header-top">
26 <h1>KnotView</h1>
27 <button class="theme-toggle" @click="toggleTheme">
28 <span x-show="!darkTheme">🌙</span>
29 <span x-show="darkTheme">☀️</span>
30 <span x-text="darkTheme ? 'Light' : 'Dark'"></span>
31 </button>
32 </div>
33 <div class="connection-panel">
34 <input
35 type="text"
36 x-model="serverUrl"
37 placeholder="https://knot.example.com"
38 @keyup.enter="connectToServer"
39 />
40 <button
41 @click="connectToServer"
42 :disabled="isConnected"
43 x-text="isConnected ? 'Connected ✓' : 'Connect'"
44 ></button>
45 </div>
46 <div
47 x-show="status.message"
48 class="status"
49 :class="status.type"
50 x-text="status.message"
51 ></div>
52 </header>
53
54 <div class="main-content">
55 <!-- Sidebar -->
56 <aside class="sidebar" x-show="state.currentRepo">
57 <div>
58 <h2>
59 Repository
60 <button
61 @click="showUsersList"
62 class="secondary"
63 style="padding: 6px 12px; font-size: 12px"
64 >
65 ← Back
66 </button>
67 </h2>
68 <div class="repo-info">
69 <template x-if="state.currentRepo">
70 <div>
71 <div class="label">Repository</div>
72 <div
73 class="value"
74 x-text="state.currentRepo?.name"
75 ></div>
76
77 <div class="label">Owner</div>
78 <div
79 class="value"
80 x-text="state.resolvedHandle || state.currentRepo?.did"
81 ></div>
82
83 <div class="label">Clone URL</div>
84 <div class="clone-url">
85 <code
86 x-text="`${API.getBaseUrl()}/repo/${state.currentRepo?.fullPath}`"
87 ></code>
88 <button
89 class="copy-btn"
90 @click="copyToClipboard(`${API.getBaseUrl()}/repo/${state.currentRepo?.fullPath}`)"
91 >
92 Copy
93 </button>
94 </div>
95
96 <button
97 @click="window.location.href = `${API.getBaseUrl()}/xrpc/sh.tangled.repo.archive?repo=${encodeURIComponent(state.currentRepo?.fullPath)}&branch=${encodeURIComponent(state.currentBranch)}`"
98 style="width: 100%; margin-top: 8px"
99 >
100 Download Archive
101 </button>
102 </div>
103 </template>
104 </div>
105 </div>
106
107 <div class="branches-section">
108 <h2>Branches</h2>
109 <div class="branch-list">
110 <template x-for="branch in branches" :key="branch">
111 <div
112 class="branch-item"
113 :class="{ active: branch.reference.name === state.currentBranch }"
114 @click="switchBranch(branch.reference.name)"
115 >
116 <span x-text="branch.reference.name"></span>
117 </div>
118 </template>
119 </div>
120 </div>
121 </aside>
122
123 <!-- Main Viewer -->
124 <main class="viewer">
125 <div x-show="loading" class="loading">
126 <div class="spinner"></div>
127 <p x-text="loadingMessage"></p>
128 </div>
129
130 <div
131 x-show="error && !loading"
132 class="error-message"
133 x-html="error"
134 ></div>
135
136 <!-- Empty State -->
137 <div
138 x-show="!loading && !error && !state.currentRepo && view === 'empty'"
139 class="welcome-hero"
140 >
141 <svg
142 xmlns="http://www.w3.org/2000/svg"
143 fill="none"
144 viewBox="0 0 24 24"
145 stroke="currentColor"
146 >
147 <path
148 stroke-linecap="round"
149 stroke-linejoin="round"
150 stroke-width="2"
151 d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"
152 />
153 </svg>
154 <h2>Welcome to KnotView</h2>
155 <p class="subtitle">
156 A web-based repository browser for AT Protocol
157 repositories. Browse, explore, and download content
158 from Knot servers.
159 </p>
160
161 <div class="feature-list">
162 <div class="feature-item">
163 <h4>🗂️ Browse Repositories</h4>
164 <p>
165 Navigate through files and folders with an
166 intuitive interface
167 </p>
168 </div>
169 <div class="feature-item">
170 <h4>🌿 Branch Support</h4>
171 <p>
172 Switch between different branches seamlessly
173 </p>
174 </div>
175 <div class="feature-item">
176 <h4>📄 File Viewer</h4>
177 <p>
178 View files with syntax highlighting and
179 markdown rendering
180 </p>
181 </div>
182 <div class="feature-item">
183 <h4>📦 Download Archives</h4>
184 <p>Download entire repositories as archives</p>
185 </div>
186 </div>
187
188 <div class="getting-started">
189 <h3>Getting Started</h3>
190 <ol>
191 <li>
192 Enter your Knot server URL in the input
193 above
194 </li>
195 <li>
196 Click "Connect" to connect to the server
197 </li>
198 <li>Select a repository from the list</li>
199 <li>
200 Browse files, switch branches, and explore!
201 </li>
202 </ol>
203 </div>
204 </div>
205
206 <!-- Users/Repos List -->
207 <div
208 x-show="!loading && !error && view === 'repoList'"
209 style="padding: 20px"
210 >
211 <template x-for="user in users" :key="user.did">
212 <div class="user-item">
213 <div
214 class="user-header"
215 x-text="user.handle || user.did"
216 ></div>
217 <template
218 x-for="repo in user.repos"
219 :key="repo.fullPath"
220 >
221 <div
222 class="repo-item"
223 @click="selectRepository(repo)"
224 >
225 <strong x-text="repo.name"></strong>
226 <small x-text="repo.fullPath"></small>
227 </div>
228 </template>
229 </div>
230 </template>
231
232 <!-- Manual Entry Fallback -->
233 <div
234 x-show="users.length === 0 && !loading"
235 class="empty-state"
236 >
237 <svg
238 xmlns="http://www.w3.org/2000/svg"
239 fill="none"
240 viewBox="0 0 24 24"
241 stroke="currentColor"
242 >
243 <path
244 stroke-linecap="round"
245 stroke-linejoin="round"
246 stroke-width="2"
247 d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
248 />
249 </svg>
250 <h3>Repository List Not Available</h3>
251 <p>
252 This server doesn't support automatic repository
253 listing.
254 </p>
255 <p style="margin-top: 20px">
256 Please enter repository path manually:
257 </p>
258 <div
259 style="
260 margin-top: 20px;
261 max-width: 400px;
262 margin-left: auto;
263 margin-right: auto;
264 "
265 >
266 <input
267 type="text"
268 x-model="manualRepoPath"
269 placeholder="did:plc:xxx.../repo-name"
270 style="
271 width: 100%;
272 margin-bottom: 10px;
273 padding: 10px;
274 border: 1px solid #cbd5e1;
275 border-radius: 6px;
276 "
277 @keyup.enter="loadManualRepo"
278 />
279 <button
280 @click="loadManualRepo"
281 style="width: 100%"
282 >
283 Load Repository
284 </button>
285 </div>
286 </div>
287 </div>
288
289 <!-- File Browser -->
290 <div x-show="!loading && !error && view === 'tree'">
291 <div class="breadcrumb" x-html="breadcrumbHtml"></div>
292 <div class="file-list" x-html="fileListHtml"></div>
293 <div
294 x-show="readmeHtml"
295 style="
296 margin-top: 20px;
297 border: 1px solid #e2e8f0;
298 border-radius: 6px;
299 overflow: hidden;
300 "
301 >
302 <div
303 style="
304 padding: 12px 20px;
305 background: #f8fafc;
306 border-bottom: 1px solid #e2e8f0;
307 font-weight: 600;
308 "
309 >
310 📖 README.md
311 </div>
312 <div
313 class="markdown-content"
314 x-html="readmeHtml"
315 ></div>
316 </div>
317 </div>
318
319 <!-- File Viewer -->
320 <div x-show="!loading && !error && view === 'file'">
321 <div class="breadcrumb" x-html="breadcrumbHtml"></div>
322 <div class="file-header">
323 <h3 x-text="currentFile.name"></h3>
324 <div class="file-actions">
325 <button @click="downloadFile">Download</button>
326 </div>
327 </div>
328 <div
329 x-show="currentFile.isBinary"
330 style="
331 padding: 40px;
332 text-align: center;
333 color: #64748b;
334 "
335 >
336 <p>Binary file (cannot be displayed)</p>
337 <button
338 @click="downloadFile"
339 style="margin-top: 16px"
340 >
341 Download File
342 </button>
343 </div>
344 <div
345 x-show="currentFile.isMarkdown && !currentFile.isBinary"
346 class="markdown-content"
347 x-html="currentFile.content"
348 ></div>
349 <div
350 x-show="!currentFile.isMarkdown && !currentFile.isBinary"
351 class="file-content"
352 x-html="currentFile.content"
353 ></div>
354 </div>
355 </main>
356 </div>
357 </div>
358 </body>
359</html>