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 Tangled Knots.
157 Browse, explore, and download content from Knot
158 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 even if Tangled AppView is down!
202 </li>
203 </ol>
204 </div>
205 </div>
206
207 <!-- Users/Repos List -->
208 <div
209 x-show="!loading && !error && view === 'repoList'"
210 style="padding: 20px"
211 >
212 <template x-for="user in users" :key="user.did">
213 <div class="user-item">
214 <div
215 class="user-header"
216 x-text="user.handle || user.did"
217 ></div>
218 <template
219 x-for="repo in user.repos"
220 :key="repo.fullPath"
221 >
222 <div
223 class="repo-item"
224 @click="selectRepository(repo)"
225 >
226 <strong x-text="repo.name"></strong>
227 <small x-text="repo.fullPath"></small>
228 </div>
229 </template>
230 </div>
231 </template>
232
233 <!-- Manual Entry Fallback -->
234 <div
235 x-show="users.length === 0 && !loading"
236 class="empty-state"
237 >
238 <svg
239 xmlns="http://www.w3.org/2000/svg"
240 fill="none"
241 viewBox="0 0 24 24"
242 stroke="currentColor"
243 >
244 <path
245 stroke-linecap="round"
246 stroke-linejoin="round"
247 stroke-width="2"
248 d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
249 />
250 </svg>
251 <h3>Repository List Not Available</h3>
252 <p>
253 This server doesn't support automatic repository
254 listing.
255 </p>
256 <p style="margin-top: 20px">
257 Please enter repository path manually:
258 </p>
259 <div
260 style="
261 margin-top: 20px;
262 max-width: 400px;
263 margin-left: auto;
264 margin-right: auto;
265 "
266 >
267 <input
268 type="text"
269 x-model="manualRepoPath"
270 placeholder="did:plc:xxx.../repo-name"
271 style="
272 width: 100%;
273 margin-bottom: 10px;
274 padding: 10px;
275 border: 1px solid #cbd5e1;
276 border-radius: 6px;
277 "
278 @keyup.enter="loadManualRepo"
279 />
280 <button
281 @click="loadManualRepo"
282 style="width: 100%"
283 >
284 Load Repository
285 </button>
286 </div>
287 </div>
288 </div>
289
290 <!-- File Browser -->
291 <div x-show="!loading && !error && view === 'tree'">
292 <div class="breadcrumb" x-html="breadcrumbHtml"></div>
293 <div class="file-list" x-html="fileListHtml"></div>
294 <div
295 x-show="readmeHtml"
296 style="
297 margin-top: 20px;
298 border: 1px solid #e2e8f0;
299 border-radius: 6px;
300 overflow: hidden;
301 "
302 >
303 <div
304 style="
305 padding: 12px 20px;
306 background: #f8fafc;
307 border-bottom: 1px solid #e2e8f0;
308 font-weight: 600;
309 "
310 >
311 📖 README.md
312 </div>
313 <div
314 class="markdown-content"
315 x-html="readmeHtml"
316 ></div>
317 </div>
318 </div>
319
320 <!-- File Viewer -->
321 <div x-show="!loading && !error && view === 'file'">
322 <div class="breadcrumb" x-html="breadcrumbHtml"></div>
323 <div class="file-header">
324 <h3 x-text="currentFile.name"></h3>
325 <div class="file-actions">
326 <button @click="downloadFile">Download</button>
327 </div>
328 </div>
329 <div
330 x-show="currentFile.isBinary"
331 style="
332 padding: 40px;
333 text-align: center;
334 color: #64748b;
335 "
336 >
337 <p>Binary file (cannot be displayed)</p>
338 <button
339 @click="downloadFile"
340 style="margin-top: 16px"
341 >
342 Download File
343 </button>
344 </div>
345 <div
346 x-show="currentFile.isMarkdown && !currentFile.isBinary"
347 class="markdown-content"
348 x-html="currentFile.content"
349 ></div>
350 <div
351 x-show="!currentFile.isMarkdown && !currentFile.isBinary"
352 class="file-content"
353 x-html="currentFile.content"
354 ></div>
355 </div>
356 </main>
357 </div>
358 <footer>
359 <p>
360 <a
361 href="https://tangled.org/julien.rbrt.fr/knotview"
362 target="_blank"
363 rel="noopener noreferrer"
364 >
365 Fork me on Tangled.
366 </a>
367 </p>
368 </footer>
369 </div>
370 </body>
371</html>