worldwideweb 0.0.20 → 0.0.21
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/.claude/settings.local.json +16 -0
- package/README.md +40 -3
- package/bin/worldwideweb.js +18 -0
- package/config.json +6 -0
- package/jsonos/browser.html +997 -0
- package/jsonos/main.cjs +291 -0
- package/jsonos/preload.js +14 -0
- package/jsonos/screenshot.png +0 -0
- package/jsonos/screenshot2.png +0 -0
- package/main.js +168 -0
- package/package.json +11 -17
|
@@ -0,0 +1,997 @@
|
|
|
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>WorldWideWeb - jsonos</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
16
|
+
background: #1a1a2e;
|
|
17
|
+
color: #eee;
|
|
18
|
+
height: 100vh;
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* Navigation Bar */
|
|
24
|
+
#navbar {
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
gap: 8px;
|
|
28
|
+
padding: 8px 12px;
|
|
29
|
+
background: #16213e;
|
|
30
|
+
border-bottom: 1px solid #0f3460;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#navbar button {
|
|
34
|
+
background: #0f3460;
|
|
35
|
+
border: none;
|
|
36
|
+
color: #eee;
|
|
37
|
+
padding: 8px 12px;
|
|
38
|
+
border-radius: 4px;
|
|
39
|
+
cursor: pointer;
|
|
40
|
+
font-size: 14px;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#navbar button:hover {
|
|
44
|
+
background: #1a4a7a;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#navbar button:disabled {
|
|
48
|
+
opacity: 0.5;
|
|
49
|
+
cursor: not-allowed;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#url-bar {
|
|
53
|
+
flex: 1;
|
|
54
|
+
padding: 8px 12px;
|
|
55
|
+
border: 1px solid #0f3460;
|
|
56
|
+
border-radius: 4px;
|
|
57
|
+
background: #0a0a1a;
|
|
58
|
+
color: #eee;
|
|
59
|
+
font-size: 14px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#url-bar:focus {
|
|
63
|
+
outline: none;
|
|
64
|
+
border-color: #e94560;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* Auth Status */
|
|
68
|
+
#auth-status {
|
|
69
|
+
padding: 4px 12px;
|
|
70
|
+
border-radius: 4px;
|
|
71
|
+
font-size: 12px;
|
|
72
|
+
cursor: pointer;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#auth-status.logged-out {
|
|
76
|
+
background: #e94560;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#auth-status.logged-in {
|
|
80
|
+
background: #4ecca3;
|
|
81
|
+
color: #1a1a2e;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* Main Content */
|
|
85
|
+
#content {
|
|
86
|
+
flex: 1;
|
|
87
|
+
overflow: auto;
|
|
88
|
+
padding: 20px;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* Loading State */
|
|
92
|
+
#loading {
|
|
93
|
+
display: none;
|
|
94
|
+
text-align: center;
|
|
95
|
+
padding: 40px;
|
|
96
|
+
color: #888;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#loading.active {
|
|
100
|
+
display: block;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* JSON Viewer */
|
|
104
|
+
#viewer {
|
|
105
|
+
background: #0a0a1a;
|
|
106
|
+
border-radius: 8px;
|
|
107
|
+
padding: 20px;
|
|
108
|
+
min-height: 200px;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* Linked Object Styles */
|
|
112
|
+
.lion-object {
|
|
113
|
+
border: 1px solid #0f3460;
|
|
114
|
+
border-radius: 8px;
|
|
115
|
+
padding: 16px;
|
|
116
|
+
margin-bottom: 16px;
|
|
117
|
+
background: #16213e;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.lion-object .lion-id {
|
|
121
|
+
color: #e94560;
|
|
122
|
+
font-size: 12px;
|
|
123
|
+
margin-bottom: 8px;
|
|
124
|
+
word-break: break-all;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.lion-object .lion-type {
|
|
128
|
+
color: #4ecca3;
|
|
129
|
+
font-size: 14px;
|
|
130
|
+
font-weight: bold;
|
|
131
|
+
margin-bottom: 12px;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.lion-property {
|
|
135
|
+
display: flex;
|
|
136
|
+
margin-bottom: 8px;
|
|
137
|
+
padding: 4px 0;
|
|
138
|
+
border-bottom: 1px solid #0f3460;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.lion-property:last-child {
|
|
142
|
+
border-bottom: none;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.lion-key {
|
|
146
|
+
color: #7b8cde;
|
|
147
|
+
min-width: 150px;
|
|
148
|
+
font-weight: 500;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.lion-value {
|
|
152
|
+
color: #eee;
|
|
153
|
+
flex: 1;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.lion-link {
|
|
157
|
+
color: #e94560;
|
|
158
|
+
text-decoration: none;
|
|
159
|
+
cursor: pointer;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.lion-link:hover {
|
|
163
|
+
text-decoration: underline;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* Edit Mode */
|
|
167
|
+
.lion-value[contenteditable="true"] {
|
|
168
|
+
background: #0a0a1a;
|
|
169
|
+
padding: 4px 8px;
|
|
170
|
+
border-radius: 4px;
|
|
171
|
+
outline: 1px solid #e94560;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* View Tabs */
|
|
175
|
+
#view-tabs {
|
|
176
|
+
display: flex;
|
|
177
|
+
gap: 8px;
|
|
178
|
+
margin-bottom: 16px;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
#view-tabs button {
|
|
182
|
+
background: #0f3460;
|
|
183
|
+
border: none;
|
|
184
|
+
color: #eee;
|
|
185
|
+
padding: 6px 16px;
|
|
186
|
+
border-radius: 4px;
|
|
187
|
+
cursor: pointer;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#view-tabs button.active {
|
|
191
|
+
background: #e94560;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* Raw JSON View */
|
|
195
|
+
#raw-json {
|
|
196
|
+
display: none;
|
|
197
|
+
background: #0a0a1a;
|
|
198
|
+
padding: 16px;
|
|
199
|
+
border-radius: 8px;
|
|
200
|
+
font-family: 'Monaco', 'Menlo', monospace;
|
|
201
|
+
font-size: 13px;
|
|
202
|
+
white-space: pre-wrap;
|
|
203
|
+
word-break: break-all;
|
|
204
|
+
color: #4ecca3;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* Status Bar */
|
|
208
|
+
#status-bar {
|
|
209
|
+
padding: 4px 12px;
|
|
210
|
+
background: #0f3460;
|
|
211
|
+
font-size: 12px;
|
|
212
|
+
color: #888;
|
|
213
|
+
display: flex;
|
|
214
|
+
justify-content: space-between;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* Custom View Container */
|
|
218
|
+
#custom-view {
|
|
219
|
+
display: none;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* Login Modal */
|
|
223
|
+
#login-modal {
|
|
224
|
+
display: none;
|
|
225
|
+
position: fixed;
|
|
226
|
+
top: 0;
|
|
227
|
+
left: 0;
|
|
228
|
+
width: 100%;
|
|
229
|
+
height: 100%;
|
|
230
|
+
background: rgba(0, 0, 0, 0.8);
|
|
231
|
+
z-index: 1000;
|
|
232
|
+
justify-content: center;
|
|
233
|
+
align-items: center;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
#login-modal.active {
|
|
237
|
+
display: flex;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
#login-modal .modal-content {
|
|
241
|
+
background: #16213e;
|
|
242
|
+
padding: 32px;
|
|
243
|
+
border-radius: 12px;
|
|
244
|
+
width: 400px;
|
|
245
|
+
max-width: 90%;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
#login-modal h2 {
|
|
249
|
+
margin-bottom: 20px;
|
|
250
|
+
color: #4ecca3;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
#login-modal label {
|
|
254
|
+
display: block;
|
|
255
|
+
margin-bottom: 8px;
|
|
256
|
+
color: #888;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
#login-modal input {
|
|
260
|
+
width: 100%;
|
|
261
|
+
padding: 12px;
|
|
262
|
+
border: 1px solid #0f3460;
|
|
263
|
+
border-radius: 4px;
|
|
264
|
+
background: #0a0a1a;
|
|
265
|
+
color: #eee;
|
|
266
|
+
font-size: 14px;
|
|
267
|
+
margin-bottom: 16px;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
#login-modal .providers {
|
|
271
|
+
display: flex;
|
|
272
|
+
flex-wrap: wrap;
|
|
273
|
+
gap: 8px;
|
|
274
|
+
margin-bottom: 20px;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
#login-modal .provider-btn {
|
|
278
|
+
padding: 8px 16px;
|
|
279
|
+
background: #0f3460;
|
|
280
|
+
border: none;
|
|
281
|
+
color: #eee;
|
|
282
|
+
border-radius: 4px;
|
|
283
|
+
cursor: pointer;
|
|
284
|
+
font-size: 12px;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
#login-modal .provider-btn:hover {
|
|
288
|
+
background: #1a4a7a;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
#login-modal .modal-actions {
|
|
292
|
+
display: flex;
|
|
293
|
+
gap: 12px;
|
|
294
|
+
justify-content: flex-end;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
#login-modal .modal-actions button {
|
|
298
|
+
padding: 10px 24px;
|
|
299
|
+
border: none;
|
|
300
|
+
border-radius: 4px;
|
|
301
|
+
cursor: pointer;
|
|
302
|
+
font-size: 14px;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
#login-modal .btn-cancel {
|
|
306
|
+
background: #333;
|
|
307
|
+
color: #eee;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
#login-modal .btn-login {
|
|
311
|
+
background: #4ecca3;
|
|
312
|
+
color: #1a1a2e;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/* Welcome Screen */
|
|
316
|
+
#welcome-screen {
|
|
317
|
+
text-align: center;
|
|
318
|
+
padding: 60px 20px;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
#welcome-screen h1 {
|
|
322
|
+
font-size: 48px;
|
|
323
|
+
margin-bottom: 16px;
|
|
324
|
+
background: linear-gradient(135deg, #e94560, #4ecca3);
|
|
325
|
+
-webkit-background-clip: text;
|
|
326
|
+
-webkit-text-fill-color: transparent;
|
|
327
|
+
background-clip: text;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
#welcome-screen p {
|
|
331
|
+
color: #888;
|
|
332
|
+
margin-bottom: 32px;
|
|
333
|
+
font-size: 18px;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
#welcome-screen .quick-links {
|
|
337
|
+
display: flex;
|
|
338
|
+
flex-wrap: wrap;
|
|
339
|
+
gap: 12px;
|
|
340
|
+
justify-content: center;
|
|
341
|
+
margin-top: 32px;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
#welcome-screen .quick-link {
|
|
345
|
+
padding: 12px 24px;
|
|
346
|
+
background: #0f3460;
|
|
347
|
+
border: none;
|
|
348
|
+
color: #eee;
|
|
349
|
+
border-radius: 8px;
|
|
350
|
+
cursor: pointer;
|
|
351
|
+
font-size: 14px;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
#welcome-screen .quick-link:hover {
|
|
355
|
+
background: #1a4a7a;
|
|
356
|
+
}
|
|
357
|
+
</style>
|
|
358
|
+
</head>
|
|
359
|
+
<body>
|
|
360
|
+
<!-- Navigation Bar -->
|
|
361
|
+
<nav id="navbar">
|
|
362
|
+
<button id="btn-back" title="Back">←</button>
|
|
363
|
+
<button id="btn-forward" title="Forward">→</button>
|
|
364
|
+
<button id="btn-reload" title="Reload">↻</button>
|
|
365
|
+
<input type="text" id="url-bar" placeholder="Enter URL or @id..." />
|
|
366
|
+
<button id="btn-go">Go</button>
|
|
367
|
+
<button id="btn-edit" title="Toggle Edit Mode">✎</button>
|
|
368
|
+
<span id="auth-status" class="logged-out">Login</span>
|
|
369
|
+
</nav>
|
|
370
|
+
|
|
371
|
+
<!-- Main Content -->
|
|
372
|
+
<main id="content">
|
|
373
|
+
<div id="loading">Loading...</div>
|
|
374
|
+
|
|
375
|
+
<div id="view-tabs">
|
|
376
|
+
<button class="active" data-view="rendered">Rendered</button>
|
|
377
|
+
<button data-view="raw">Raw JSON</button>
|
|
378
|
+
<button data-view="custom">Custom View</button>
|
|
379
|
+
</div>
|
|
380
|
+
|
|
381
|
+
<div id="viewer"></div>
|
|
382
|
+
<pre id="raw-json"></pre>
|
|
383
|
+
<div id="custom-view"></div>
|
|
384
|
+
</main>
|
|
385
|
+
|
|
386
|
+
<!-- Status Bar -->
|
|
387
|
+
<footer id="status-bar">
|
|
388
|
+
<span id="status-text">Ready</span>
|
|
389
|
+
<span id="status-size"></span>
|
|
390
|
+
</footer>
|
|
391
|
+
|
|
392
|
+
<!-- Login Modal -->
|
|
393
|
+
<div id="login-modal">
|
|
394
|
+
<div class="modal-content">
|
|
395
|
+
<h2>Login with Solid</h2>
|
|
396
|
+
<label>Identity Provider (IdP)</label>
|
|
397
|
+
<input type="text" id="idp-input" placeholder="https://solidcommunity.net" value="https://solidcommunity.net" />
|
|
398
|
+
|
|
399
|
+
<label>Quick Providers</label>
|
|
400
|
+
<div class="providers">
|
|
401
|
+
<button class="provider-btn" data-idp="https://solidcommunity.net">solidcommunity.net</button>
|
|
402
|
+
<button class="provider-btn" data-idp="https://login.inrupt.com">inrupt.com</button>
|
|
403
|
+
<button class="provider-btn" data-idp="https://solidweb.org">solidweb.org</button>
|
|
404
|
+
<button class="provider-btn" data-idp="https://teamid.live">teamid.live</button>
|
|
405
|
+
</div>
|
|
406
|
+
|
|
407
|
+
<div class="modal-actions">
|
|
408
|
+
<button class="btn-cancel" id="btn-cancel-login">Cancel</button>
|
|
409
|
+
<button class="btn-login" id="btn-do-login">Login</button>
|
|
410
|
+
</div>
|
|
411
|
+
</div>
|
|
412
|
+
</div>
|
|
413
|
+
|
|
414
|
+
<!-- LION Library (Linked Objects) -->
|
|
415
|
+
<script>
|
|
416
|
+
// Minimal LION implementation (~2KB)
|
|
417
|
+
const LION = {
|
|
418
|
+
cache: new Map(),
|
|
419
|
+
|
|
420
|
+
async fetch(url, options = {}) {
|
|
421
|
+
const headers = {
|
|
422
|
+
'Accept': 'application/ld+json, application/json, text/turtle',
|
|
423
|
+
...options.headers
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
const response = await fetch(url, { ...options, headers });
|
|
428
|
+
const contentType = response.headers.get('content-type') || '';
|
|
429
|
+
|
|
430
|
+
let data;
|
|
431
|
+
if (contentType.includes('json')) {
|
|
432
|
+
data = await response.json();
|
|
433
|
+
} else if (contentType.includes('turtle')) {
|
|
434
|
+
// Basic turtle to JSON-LD conversion for simple cases
|
|
435
|
+
const text = await response.text();
|
|
436
|
+
data = this.turtleToJsonLd(text, url);
|
|
437
|
+
} else {
|
|
438
|
+
data = await response.json();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Normalize to always have @id
|
|
442
|
+
if (!data['@id']) {
|
|
443
|
+
data['@id'] = url;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
this.cache.set(url, data);
|
|
447
|
+
return data;
|
|
448
|
+
} catch (error) {
|
|
449
|
+
console.error('LION fetch error:', error);
|
|
450
|
+
throw error;
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
|
|
454
|
+
// Very basic turtle parser for common patterns
|
|
455
|
+
turtleToJsonLd(turtle, baseUrl) {
|
|
456
|
+
const obj = { '@id': baseUrl };
|
|
457
|
+
const lines = turtle.split('\n');
|
|
458
|
+
|
|
459
|
+
for (const line of lines) {
|
|
460
|
+
const trimmed = line.trim();
|
|
461
|
+
if (trimmed.startsWith('@prefix') || trimmed.startsWith('#') || !trimmed) {
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
// Basic triple pattern: <subject> <predicate> <object> .
|
|
465
|
+
const match = trimmed.match(/<([^>]+)>\s+<([^>]+)>\s+(?:<([^>]+)>|"([^"]*)")/);
|
|
466
|
+
if (match) {
|
|
467
|
+
const predicate = match[2].split(/[#\/]/).pop();
|
|
468
|
+
const value = match[3] || match[4];
|
|
469
|
+
obj[predicate] = value;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return obj;
|
|
473
|
+
},
|
|
474
|
+
|
|
475
|
+
async create(url, data) {
|
|
476
|
+
const response = await fetch(url, {
|
|
477
|
+
method: 'PUT',
|
|
478
|
+
headers: {
|
|
479
|
+
'Content-Type': 'application/ld+json'
|
|
480
|
+
},
|
|
481
|
+
body: JSON.stringify({ '@id': url, ...data })
|
|
482
|
+
});
|
|
483
|
+
return response.ok;
|
|
484
|
+
},
|
|
485
|
+
|
|
486
|
+
async update(url, data) {
|
|
487
|
+
const existing = this.cache.get(url) || {};
|
|
488
|
+
const merged = { ...existing, ...data, '@id': url };
|
|
489
|
+
return this.create(url, merged);
|
|
490
|
+
},
|
|
491
|
+
|
|
492
|
+
async deleteObject(url) {
|
|
493
|
+
const response = await fetch(url, { method: 'DELETE' });
|
|
494
|
+
this.cache.delete(url);
|
|
495
|
+
return response.ok;
|
|
496
|
+
},
|
|
497
|
+
|
|
498
|
+
isUrl(value) {
|
|
499
|
+
if (typeof value !== 'string') return false;
|
|
500
|
+
return value.startsWith('http://') || value.startsWith('https://');
|
|
501
|
+
},
|
|
502
|
+
|
|
503
|
+
getType(data) {
|
|
504
|
+
return data['@type'] || data.type || 'Unknown';
|
|
505
|
+
},
|
|
506
|
+
|
|
507
|
+
getView(data) {
|
|
508
|
+
return data['@view'] || data.view;
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
window.LION = LION;
|
|
513
|
+
</script>
|
|
514
|
+
|
|
515
|
+
<!-- jsonos Runtime -->
|
|
516
|
+
<script>
|
|
517
|
+
const jsonos = {
|
|
518
|
+
panes: new Map(),
|
|
519
|
+
currentData: null,
|
|
520
|
+
editMode: false,
|
|
521
|
+
history: [],
|
|
522
|
+
historyIndex: -1,
|
|
523
|
+
|
|
524
|
+
// Register a pane/view for a type
|
|
525
|
+
registerPane(type, renderFn) {
|
|
526
|
+
this.panes.set(type, renderFn);
|
|
527
|
+
},
|
|
528
|
+
|
|
529
|
+
// Render data using appropriate pane
|
|
530
|
+
async render(data, container) {
|
|
531
|
+
this.currentData = data;
|
|
532
|
+
const type = LION.getType(data);
|
|
533
|
+
const viewUrl = LION.getView(data);
|
|
534
|
+
|
|
535
|
+
// Try custom @view first
|
|
536
|
+
if (viewUrl) {
|
|
537
|
+
try {
|
|
538
|
+
await this.loadCustomView(viewUrl, data, container);
|
|
539
|
+
return;
|
|
540
|
+
} catch (e) {
|
|
541
|
+
console.warn('Custom view failed, using default:', e);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Try registered pane
|
|
546
|
+
if (this.panes.has(type)) {
|
|
547
|
+
const html = this.panes.get(type)(data);
|
|
548
|
+
container.innerHTML = html;
|
|
549
|
+
this.bindLinks(container);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Default: render as linked object tree
|
|
554
|
+
this.renderDefault(data, container);
|
|
555
|
+
},
|
|
556
|
+
|
|
557
|
+
renderDefault(data, container) {
|
|
558
|
+
const html = this.objectToHtml(data);
|
|
559
|
+
container.innerHTML = html;
|
|
560
|
+
this.bindLinks(container);
|
|
561
|
+
},
|
|
562
|
+
|
|
563
|
+
objectToHtml(obj, depth = 0) {
|
|
564
|
+
if (depth > 5) return '<span class="lion-value">...</span>';
|
|
565
|
+
|
|
566
|
+
let html = '<div class="lion-object">';
|
|
567
|
+
|
|
568
|
+
// @id
|
|
569
|
+
if (obj['@id']) {
|
|
570
|
+
html += `<div class="lion-id">@id: <a class="lion-link" href="${obj['@id']}">${obj['@id']}</a></div>`;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// @type
|
|
574
|
+
if (obj['@type']) {
|
|
575
|
+
const type = Array.isArray(obj['@type']) ? obj['@type'].join(', ') : obj['@type'];
|
|
576
|
+
html += `<div class="lion-type">${type}</div>`;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Properties
|
|
580
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
581
|
+
if (key.startsWith('@')) continue;
|
|
582
|
+
|
|
583
|
+
html += '<div class="lion-property">';
|
|
584
|
+
html += `<span class="lion-key">${this.formatKey(key)}</span>`;
|
|
585
|
+
html += `<span class="lion-value" data-key="${key}">${this.valueToHtml(value, depth)}</span>`;
|
|
586
|
+
html += '</div>';
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
html += '</div>';
|
|
590
|
+
return html;
|
|
591
|
+
},
|
|
592
|
+
|
|
593
|
+
valueToHtml(value, depth) {
|
|
594
|
+
if (value === null || value === undefined) {
|
|
595
|
+
return '<em>null</em>';
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (Array.isArray(value)) {
|
|
599
|
+
return value.map(v => this.valueToHtml(v, depth)).join('<br>');
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (typeof value === 'object') {
|
|
603
|
+
return this.objectToHtml(value, depth + 1);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (LION.isUrl(value)) {
|
|
607
|
+
return `<a class="lion-link" href="${value}">${value}</a>`;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return String(value);
|
|
611
|
+
},
|
|
612
|
+
|
|
613
|
+
formatKey(key) {
|
|
614
|
+
// Remove namespace prefixes for display
|
|
615
|
+
const parts = key.split(/[:#\/]/);
|
|
616
|
+
return parts[parts.length - 1];
|
|
617
|
+
},
|
|
618
|
+
|
|
619
|
+
bindLinks(container) {
|
|
620
|
+
container.querySelectorAll('.lion-link').forEach(link => {
|
|
621
|
+
link.addEventListener('click', (e) => {
|
|
622
|
+
e.preventDefault();
|
|
623
|
+
const url = link.getAttribute('href');
|
|
624
|
+
this.navigate(url);
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
},
|
|
628
|
+
|
|
629
|
+
async loadCustomView(viewUrl, data, container) {
|
|
630
|
+
const customViewEl = document.getElementById('custom-view');
|
|
631
|
+
const module = await import(viewUrl);
|
|
632
|
+
if (module.render) {
|
|
633
|
+
const result = module.render(data);
|
|
634
|
+
if (typeof result === 'string') {
|
|
635
|
+
customViewEl.innerHTML = result;
|
|
636
|
+
} else {
|
|
637
|
+
customViewEl.innerHTML = '';
|
|
638
|
+
customViewEl.appendChild(result);
|
|
639
|
+
}
|
|
640
|
+
// Switch to custom view tab
|
|
641
|
+
document.querySelector('[data-view="custom"]').click();
|
|
642
|
+
}
|
|
643
|
+
},
|
|
644
|
+
|
|
645
|
+
async navigate(url) {
|
|
646
|
+
if (!url) return;
|
|
647
|
+
|
|
648
|
+
// Update history
|
|
649
|
+
if (this.historyIndex < this.history.length - 1) {
|
|
650
|
+
this.history = this.history.slice(0, this.historyIndex + 1);
|
|
651
|
+
}
|
|
652
|
+
this.history.push(url);
|
|
653
|
+
this.historyIndex = this.history.length - 1;
|
|
654
|
+
|
|
655
|
+
// Update UI
|
|
656
|
+
document.getElementById('url-bar').value = url;
|
|
657
|
+
document.getElementById('loading').classList.add('active');
|
|
658
|
+
document.getElementById('viewer').innerHTML = '';
|
|
659
|
+
document.getElementById('status-text').textContent = 'Loading...';
|
|
660
|
+
|
|
661
|
+
try {
|
|
662
|
+
const data = await LION.fetch(url);
|
|
663
|
+
|
|
664
|
+
// Update raw view
|
|
665
|
+
document.getElementById('raw-json').textContent = JSON.stringify(data, null, 2);
|
|
666
|
+
|
|
667
|
+
// Render
|
|
668
|
+
await this.render(data, document.getElementById('viewer'));
|
|
669
|
+
|
|
670
|
+
document.getElementById('status-text').textContent = `Loaded: ${url}`;
|
|
671
|
+
document.getElementById('status-size').textContent =
|
|
672
|
+
`${JSON.stringify(data).length} bytes`;
|
|
673
|
+
} catch (error) {
|
|
674
|
+
document.getElementById('viewer').innerHTML =
|
|
675
|
+
`<div class="lion-object"><div class="lion-type">Error</div>
|
|
676
|
+
<div class="lion-property"><span class="lion-key">message</span>
|
|
677
|
+
<span class="lion-value">${error.message}</span></div></div>`;
|
|
678
|
+
document.getElementById('status-text').textContent = `Error: ${error.message}`;
|
|
679
|
+
} finally {
|
|
680
|
+
document.getElementById('loading').classList.remove('active');
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
this.updateNavButtons();
|
|
684
|
+
},
|
|
685
|
+
|
|
686
|
+
goBack() {
|
|
687
|
+
if (this.historyIndex > 0) {
|
|
688
|
+
this.historyIndex--;
|
|
689
|
+
const url = this.history[this.historyIndex];
|
|
690
|
+
document.getElementById('url-bar').value = url;
|
|
691
|
+
this.navigateWithoutHistory(url);
|
|
692
|
+
}
|
|
693
|
+
},
|
|
694
|
+
|
|
695
|
+
goForward() {
|
|
696
|
+
if (this.historyIndex < this.history.length - 1) {
|
|
697
|
+
this.historyIndex++;
|
|
698
|
+
const url = this.history[this.historyIndex];
|
|
699
|
+
document.getElementById('url-bar').value = url;
|
|
700
|
+
this.navigateWithoutHistory(url);
|
|
701
|
+
}
|
|
702
|
+
},
|
|
703
|
+
|
|
704
|
+
async navigateWithoutHistory(url) {
|
|
705
|
+
document.getElementById('loading').classList.add('active');
|
|
706
|
+
try {
|
|
707
|
+
const data = await LION.fetch(url);
|
|
708
|
+
document.getElementById('raw-json').textContent = JSON.stringify(data, null, 2);
|
|
709
|
+
await this.render(data, document.getElementById('viewer'));
|
|
710
|
+
document.getElementById('status-text').textContent = `Loaded: ${url}`;
|
|
711
|
+
} catch (error) {
|
|
712
|
+
document.getElementById('viewer').innerHTML =
|
|
713
|
+
`<div class="lion-object"><div class="lion-type">Error</div>
|
|
714
|
+
<div class="lion-property"><span class="lion-key">message</span>
|
|
715
|
+
<span class="lion-value">${error.message}</span></div></div>`;
|
|
716
|
+
} finally {
|
|
717
|
+
document.getElementById('loading').classList.remove('active');
|
|
718
|
+
}
|
|
719
|
+
this.updateNavButtons();
|
|
720
|
+
},
|
|
721
|
+
|
|
722
|
+
updateNavButtons() {
|
|
723
|
+
document.getElementById('btn-back').disabled = this.historyIndex <= 0;
|
|
724
|
+
document.getElementById('btn-forward').disabled =
|
|
725
|
+
this.historyIndex >= this.history.length - 1;
|
|
726
|
+
},
|
|
727
|
+
|
|
728
|
+
toggleEditMode() {
|
|
729
|
+
this.editMode = !this.editMode;
|
|
730
|
+
const btn = document.getElementById('btn-edit');
|
|
731
|
+
btn.style.background = this.editMode ? '#e94560' : '';
|
|
732
|
+
|
|
733
|
+
document.querySelectorAll('.lion-value[data-key]').forEach(el => {
|
|
734
|
+
el.contentEditable = this.editMode;
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
if (!this.editMode && this.currentData) {
|
|
738
|
+
// Save changes
|
|
739
|
+
this.saveChanges();
|
|
740
|
+
}
|
|
741
|
+
},
|
|
742
|
+
|
|
743
|
+
async saveChanges() {
|
|
744
|
+
const url = this.currentData['@id'];
|
|
745
|
+
if (!url) return;
|
|
746
|
+
|
|
747
|
+
const updates = {};
|
|
748
|
+
document.querySelectorAll('.lion-value[data-key]').forEach(el => {
|
|
749
|
+
const key = el.dataset.key;
|
|
750
|
+
const value = el.textContent.trim();
|
|
751
|
+
updates[key] = value;
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
try {
|
|
755
|
+
await LION.update(url, updates);
|
|
756
|
+
document.getElementById('status-text').textContent = 'Saved!';
|
|
757
|
+
} catch (error) {
|
|
758
|
+
document.getElementById('status-text').textContent = `Save failed: ${error.message}`;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
window.jsonos = jsonos;
|
|
764
|
+
|
|
765
|
+
// Solid Authentication Module
|
|
766
|
+
const solidAuth = {
|
|
767
|
+
session: null,
|
|
768
|
+
webId: null,
|
|
769
|
+
|
|
770
|
+
async login(idp) {
|
|
771
|
+
try {
|
|
772
|
+
document.getElementById('status-text').textContent = 'Logging in...';
|
|
773
|
+
|
|
774
|
+
// For Electron, we use a popup-based flow
|
|
775
|
+
// This is a simplified version - production would use @inrupt/solid-client-authn-browser
|
|
776
|
+
const authEndpoint = `${idp}/authorize`;
|
|
777
|
+
const clientId = window.location.origin;
|
|
778
|
+
|
|
779
|
+
// Store the IdP for session restoration
|
|
780
|
+
localStorage.setItem('solid-idp', idp);
|
|
781
|
+
|
|
782
|
+
// Open login popup
|
|
783
|
+
const popup = window.open(
|
|
784
|
+
`${idp}/.well-known/openid-configuration`,
|
|
785
|
+
'solid-login',
|
|
786
|
+
'width=500,height=600'
|
|
787
|
+
);
|
|
788
|
+
|
|
789
|
+
// For now, show manual WebID input as fallback
|
|
790
|
+
const webId = prompt('Enter your WebID (e.g., https://you.solidcommunity.net/profile/card#me):');
|
|
791
|
+
|
|
792
|
+
if (webId) {
|
|
793
|
+
this.webId = webId;
|
|
794
|
+
this.session = { webId };
|
|
795
|
+
localStorage.setItem('solid-webid', webId);
|
|
796
|
+
this.updateUI(true);
|
|
797
|
+
|
|
798
|
+
// Fetch and display profile
|
|
799
|
+
jsonos.navigate(webId);
|
|
800
|
+
}
|
|
801
|
+
} catch (error) {
|
|
802
|
+
console.error('Login failed:', error);
|
|
803
|
+
document.getElementById('status-text').textContent = `Login failed: ${error.message}`;
|
|
804
|
+
}
|
|
805
|
+
},
|
|
806
|
+
|
|
807
|
+
async logout() {
|
|
808
|
+
this.session = null;
|
|
809
|
+
this.webId = null;
|
|
810
|
+
localStorage.removeItem('solid-webid');
|
|
811
|
+
localStorage.removeItem('solid-idp');
|
|
812
|
+
this.updateUI(false);
|
|
813
|
+
document.getElementById('status-text').textContent = 'Logged out';
|
|
814
|
+
},
|
|
815
|
+
|
|
816
|
+
async restoreSession() {
|
|
817
|
+
const webId = localStorage.getItem('solid-webid');
|
|
818
|
+
if (webId) {
|
|
819
|
+
this.webId = webId;
|
|
820
|
+
this.session = { webId };
|
|
821
|
+
this.updateUI(true);
|
|
822
|
+
return true;
|
|
823
|
+
}
|
|
824
|
+
return false;
|
|
825
|
+
},
|
|
826
|
+
|
|
827
|
+
updateUI(isLoggedIn) {
|
|
828
|
+
const authStatus = document.getElementById('auth-status');
|
|
829
|
+
if (isLoggedIn) {
|
|
830
|
+
authStatus.classList.remove('logged-out');
|
|
831
|
+
authStatus.classList.add('logged-in');
|
|
832
|
+
authStatus.textContent = this.webId.split('/').pop().replace('#me', '') || 'Logged In';
|
|
833
|
+
authStatus.title = this.webId;
|
|
834
|
+
} else {
|
|
835
|
+
authStatus.classList.remove('logged-in');
|
|
836
|
+
authStatus.classList.add('logged-out');
|
|
837
|
+
authStatus.textContent = 'Login';
|
|
838
|
+
authStatus.title = '';
|
|
839
|
+
}
|
|
840
|
+
},
|
|
841
|
+
|
|
842
|
+
// Get fetch with auth headers
|
|
843
|
+
async authFetch(url, options = {}) {
|
|
844
|
+
// In a full implementation, this would add Bearer token
|
|
845
|
+
// For now, just pass through with credentials
|
|
846
|
+
return fetch(url, {
|
|
847
|
+
...options,
|
|
848
|
+
credentials: 'include'
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
window.solidAuth = solidAuth;
|
|
854
|
+
|
|
855
|
+
// Register some default panes
|
|
856
|
+
jsonos.registerPane('schema:Person', (data) => `
|
|
857
|
+
<div class="lion-object">
|
|
858
|
+
<div class="lion-type">👤 Person</div>
|
|
859
|
+
<div class="lion-property">
|
|
860
|
+
<span class="lion-key">Name</span>
|
|
861
|
+
<span class="lion-value" data-key="schema:name">${data['schema:name'] || data.name || 'Unknown'}</span>
|
|
862
|
+
</div>
|
|
863
|
+
${data['schema:email'] || data.email ? `
|
|
864
|
+
<div class="lion-property">
|
|
865
|
+
<span class="lion-key">Email</span>
|
|
866
|
+
<span class="lion-value" data-key="schema:email">${data['schema:email'] || data.email}</span>
|
|
867
|
+
</div>` : ''}
|
|
868
|
+
${data['schema:knows'] || data.knows ? `
|
|
869
|
+
<div class="lion-property">
|
|
870
|
+
<span class="lion-key">Knows</span>
|
|
871
|
+
<span class="lion-value">${jsonos.valueToHtml(data['schema:knows'] || data.knows, 0)}</span>
|
|
872
|
+
</div>` : ''}
|
|
873
|
+
</div>
|
|
874
|
+
`);
|
|
875
|
+
|
|
876
|
+
jsonos.registerPane('schema:WebPage', (data) => `
|
|
877
|
+
<div class="lion-object">
|
|
878
|
+
<div class="lion-type">🌐 Web Page</div>
|
|
879
|
+
<div class="lion-property">
|
|
880
|
+
<span class="lion-key">Title</span>
|
|
881
|
+
<span class="lion-value">${data['schema:name'] || data.name || 'Untitled'}</span>
|
|
882
|
+
</div>
|
|
883
|
+
${data['schema:description'] ? `
|
|
884
|
+
<div class="lion-property">
|
|
885
|
+
<span class="lion-key">Description</span>
|
|
886
|
+
<span class="lion-value">${data['schema:description']}</span>
|
|
887
|
+
</div>` : ''}
|
|
888
|
+
</div>
|
|
889
|
+
`);
|
|
890
|
+
|
|
891
|
+
// Initialize UI
|
|
892
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
893
|
+
const urlBar = document.getElementById('url-bar');
|
|
894
|
+
const btnGo = document.getElementById('btn-go');
|
|
895
|
+
const btnBack = document.getElementById('btn-back');
|
|
896
|
+
const btnForward = document.getElementById('btn-forward');
|
|
897
|
+
const btnReload = document.getElementById('btn-reload');
|
|
898
|
+
const btnEdit = document.getElementById('btn-edit');
|
|
899
|
+
const authStatus = document.getElementById('auth-status');
|
|
900
|
+
|
|
901
|
+
// Navigation
|
|
902
|
+
btnGo.addEventListener('click', () => jsonos.navigate(urlBar.value));
|
|
903
|
+
urlBar.addEventListener('keypress', (e) => {
|
|
904
|
+
if (e.key === 'Enter') jsonos.navigate(urlBar.value);
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
btnBack.addEventListener('click', () => jsonos.goBack());
|
|
908
|
+
btnForward.addEventListener('click', () => jsonos.goForward());
|
|
909
|
+
btnReload.addEventListener('click', () => {
|
|
910
|
+
if (jsonos.currentData && jsonos.currentData['@id']) {
|
|
911
|
+
LION.cache.delete(jsonos.currentData['@id']);
|
|
912
|
+
jsonos.navigateWithoutHistory(jsonos.currentData['@id']);
|
|
913
|
+
}
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
// Edit mode
|
|
917
|
+
btnEdit.addEventListener('click', () => jsonos.toggleEditMode());
|
|
918
|
+
|
|
919
|
+
// View tabs
|
|
920
|
+
document.querySelectorAll('#view-tabs button').forEach(btn => {
|
|
921
|
+
btn.addEventListener('click', () => {
|
|
922
|
+
document.querySelectorAll('#view-tabs button').forEach(b => b.classList.remove('active'));
|
|
923
|
+
btn.classList.add('active');
|
|
924
|
+
|
|
925
|
+
const view = btn.dataset.view;
|
|
926
|
+
document.getElementById('viewer').style.display = view === 'rendered' ? 'block' : 'none';
|
|
927
|
+
document.getElementById('raw-json').style.display = view === 'raw' ? 'block' : 'none';
|
|
928
|
+
document.getElementById('custom-view').style.display = view === 'custom' ? 'block' : 'none';
|
|
929
|
+
});
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
// Solid Authentication
|
|
933
|
+
const loginModal = document.getElementById('login-modal');
|
|
934
|
+
const idpInput = document.getElementById('idp-input');
|
|
935
|
+
|
|
936
|
+
authStatus.addEventListener('click', () => {
|
|
937
|
+
if (authStatus.classList.contains('logged-out')) {
|
|
938
|
+
loginModal.classList.add('active');
|
|
939
|
+
} else {
|
|
940
|
+
// Logout
|
|
941
|
+
solidAuth.logout();
|
|
942
|
+
}
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
// Provider buttons
|
|
946
|
+
document.querySelectorAll('.provider-btn').forEach(btn => {
|
|
947
|
+
btn.addEventListener('click', () => {
|
|
948
|
+
idpInput.value = btn.dataset.idp;
|
|
949
|
+
});
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
// Cancel login
|
|
953
|
+
document.getElementById('btn-cancel-login').addEventListener('click', () => {
|
|
954
|
+
loginModal.classList.remove('active');
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
// Do login
|
|
958
|
+
document.getElementById('btn-do-login').addEventListener('click', async () => {
|
|
959
|
+
const idp = idpInput.value.trim();
|
|
960
|
+
if (idp) {
|
|
961
|
+
await solidAuth.login(idp);
|
|
962
|
+
loginModal.classList.remove('active');
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
// Close modal on outside click
|
|
967
|
+
loginModal.addEventListener('click', (e) => {
|
|
968
|
+
if (e.target === loginModal) {
|
|
969
|
+
loginModal.classList.remove('active');
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
// Initial navigation buttons state
|
|
974
|
+
jsonos.updateNavButtons();
|
|
975
|
+
|
|
976
|
+
// Restore session and load initial URL
|
|
977
|
+
solidAuth.restoreSession().then(restored => {
|
|
978
|
+
const params = new URLSearchParams(window.location.search);
|
|
979
|
+
let initialUrl = params.get('uri');
|
|
980
|
+
|
|
981
|
+
if (!initialUrl) {
|
|
982
|
+
if (restored && solidAuth.webId) {
|
|
983
|
+
// Load user's profile
|
|
984
|
+
initialUrl = solidAuth.webId;
|
|
985
|
+
} else {
|
|
986
|
+
// Show welcome/demo content
|
|
987
|
+
initialUrl = 'https://melvincarvalho.com/.well-known/did.json';
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
urlBar.value = initialUrl;
|
|
992
|
+
jsonos.navigate(initialUrl);
|
|
993
|
+
});
|
|
994
|
+
});
|
|
995
|
+
</script>
|
|
996
|
+
</body>
|
|
997
|
+
</html>
|