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 <h1>KnotView</h1>
26 <div class="connection-panel">
27 <input
28 type="text"
29 x-model="serverUrl"
30 placeholder="https://knot.example.com"
31 @keyup.enter="connectToServer"
32 />
33 <button
34 @click="connectToServer"
35 :disabled="isConnected"
36 x-text="isConnected ? 'Connected ✓' : 'Connect'"
37 ></button>
38 </div>
39 <div
40 x-show="status.message"
41 class="status"
42 :class="status.type"
43 x-text="status.message"
44 ></div>
45 </header>
46
47 <div class="main-content">
48 <!-- Sidebar -->
49 <aside class="sidebar" x-show="state.currentRepo">
50 <div>
51 <h2>
52 Repository
53 <button
54 @click="showUsersList"
55 class="secondary"
56 style="padding: 6px 12px; font-size: 12px"
57 >
58 ← Back
59 </button>
60 </h2>
61 <div class="repo-info">
62 <template x-if="state.currentRepo">
63 <div>
64 <div class="label">Repository</div>
65 <div
66 class="value"
67 x-text="state.currentRepo?.name"
68 ></div>
69
70 <div class="label">Owner</div>
71 <div
72 class="value"
73 x-text="state.resolvedHandle || state.currentRepo?.did"
74 ></div>
75
76 <div class="label">Clone URL</div>
77 <div class="clone-url">
78 <code
79 x-text="`${API.getBaseUrl()}/repo/${state.currentRepo?.fullPath}`"
80 ></code>
81 <button
82 class="copy-btn"
83 @click="copyToClipboard(`${API.getBaseUrl()}/repo/${state.currentRepo?.fullPath}`)"
84 >
85 Copy
86 </button>
87 </div>
88
89 <button
90 @click="window.location.href = `${API.getBaseUrl()}/xrpc/sh.tangled.repo.archive?repo=${encodeURIComponent(state.currentRepo?.fullPath)}&branch=${encodeURIComponent(state.currentBranch)}`"
91 style="width: 100%; margin-top: 8px"
92 >
93 Download Archive
94 </button>
95 </div>
96 </template>
97 </div>
98 </div>
99
100 <div class="branches-section">
101 <h2>Branches</h2>
102 <div class="branch-list">
103 <template x-for="branch in branches" :key="branch">
104 <div
105 class="branch-item"
106 :class="{ active: branch.reference.name === state.currentBranch }"
107 @click="switchBranch(branch.reference.name)"
108 >
109 <span x-text="branch.reference.name"></span>
110 </div>
111 </template>
112 </div>
113 </div>
114 </aside>
115
116 <!-- Main Viewer -->
117 <main class="viewer">
118 <div x-show="loading" class="loading">
119 <div class="spinner"></div>
120 <p x-text="loadingMessage"></p>
121 </div>
122
123 <div
124 x-show="error && !loading"
125 class="error-message"
126 x-html="error"
127 ></div>
128
129 <!-- Empty State -->
130 <div
131 x-show="!loading && !error && !state.currentRepo && view === 'empty'"
132 class="empty-state"
133 >
134 <svg
135 xmlns="http://www.w3.org/2000/svg"
136 fill="none"
137 viewBox="0 0 24 24"
138 stroke="currentColor"
139 >
140 <path
141 stroke-linecap="round"
142 stroke-linejoin="round"
143 stroke-width="2"
144 d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"
145 />
146 </svg>
147 <h3>No Repository Selected</h3>
148 <p>
149 Connect to a server and select a repository to
150 browse its contents.
151 </p>
152 </div>
153
154 <!-- Users/Repos List -->
155 <div
156 x-show="!loading && !error && view === 'repoList'"
157 style="padding: 20px"
158 >
159 <template x-for="user in users" :key="user.did">
160 <div class="user-item">
161 <div
162 class="user-header"
163 x-text="user.handle || user.did"
164 ></div>
165 <template
166 x-for="repo in user.repos"
167 :key="repo.fullPath"
168 >
169 <div
170 class="repo-item"
171 @click="selectRepository(repo)"
172 >
173 <strong x-text="repo.name"></strong>
174 <small x-text="repo.fullPath"></small>
175 </div>
176 </template>
177 </div>
178 </template>
179
180 <!-- Manual Entry Fallback -->
181 <div
182 x-show="users.length === 0 && !loading"
183 class="empty-state"
184 >
185 <svg
186 xmlns="http://www.w3.org/2000/svg"
187 fill="none"
188 viewBox="0 0 24 24"
189 stroke="currentColor"
190 >
191 <path
192 stroke-linecap="round"
193 stroke-linejoin="round"
194 stroke-width="2"
195 d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
196 />
197 </svg>
198 <h3>Repository List Not Available</h3>
199 <p>
200 This server doesn't support automatic repository
201 listing.
202 </p>
203 <p style="margin-top: 20px">
204 Please enter repository path manually:
205 </p>
206 <div
207 style="
208 margin-top: 20px;
209 max-width: 400px;
210 margin-left: auto;
211 margin-right: auto;
212 "
213 >
214 <input
215 type="text"
216 x-model="manualRepoPath"
217 placeholder="did:plc:xxx.../repo-name"
218 style="
219 width: 100%;
220 margin-bottom: 10px;
221 padding: 10px;
222 border: 1px solid #cbd5e1;
223 border-radius: 6px;
224 "
225 @keyup.enter="loadManualRepo"
226 />
227 <button
228 @click="loadManualRepo"
229 style="width: 100%"
230 >
231 Load Repository
232 </button>
233 </div>
234 </div>
235 </div>
236
237 <!-- File Browser -->
238 <div x-show="!loading && !error && view === 'tree'">
239 <div class="breadcrumb" x-html="breadcrumbHtml"></div>
240 <div class="file-list" x-html="fileListHtml"></div>
241 <div
242 x-show="readmeHtml"
243 style="
244 margin-top: 20px;
245 border: 1px solid #e2e8f0;
246 border-radius: 6px;
247 overflow: hidden;
248 "
249 >
250 <div
251 style="
252 padding: 12px 20px;
253 background: #f8fafc;
254 border-bottom: 1px solid #e2e8f0;
255 font-weight: 600;
256 "
257 >
258 📖 README.md
259 </div>
260 <div
261 class="markdown-content"
262 x-html="readmeHtml"
263 ></div>
264 </div>
265 </div>
266
267 <!-- File Viewer -->
268 <div x-show="!loading && !error && view === 'file'">
269 <div class="breadcrumb" x-html="breadcrumbHtml"></div>
270 <div class="file-header">
271 <h3 x-text="currentFile.name"></h3>
272 <div class="file-actions">
273 <button @click="downloadFile">Download</button>
274 </div>
275 </div>
276 <div
277 x-show="currentFile.isBinary"
278 style="
279 padding: 40px;
280 text-align: center;
281 color: #64748b;
282 "
283 >
284 <p>Binary file (cannot be displayed)</p>
285 <button
286 @click="downloadFile"
287 style="margin-top: 16px"
288 >
289 Download File
290 </button>
291 </div>
292 <div
293 x-show="currentFile.isMarkdown && !currentFile.isBinary"
294 class="markdown-content"
295 x-html="currentFile.content"
296 ></div>
297 <div
298 x-show="!currentFile.isMarkdown && !currentFile.isBinary"
299 class="file-content"
300 x-html="currentFile.content"
301 ></div>
302 </div>
303 </main>
304 </div>
305 </div>
306 </body>
307</html>