@dtudury/streamo 4.0.2 → 4.0.4
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/README.md +4 -0
- package/package.json +1 -1
- package/public/apps/chat/index.html +6 -3
- package/public/apps/explorer/index.html +94 -3
- package/public/apps/explorer/main.js +203 -48
- package/public/index.html +220 -15
- package/public/streamo.svg +24 -0
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dtudury/streamo",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.4",
|
|
4
4
|
"description": "peer-to-peer sync where your data and identity belong to you, not the server",
|
|
5
5
|
"keywords": ["p2p", "peer-to-peer", "sync", "reactive", "content-addressed", "websocket", "signed", "append-only", "offline-first", "cryptographic", "identity"],
|
|
6
6
|
"repository": "git@github.com:dtudury/streamo.git",
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>streamo chat</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="/streamo.svg">
|
|
7
8
|
<style>
|
|
8
9
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0 }
|
|
9
10
|
:root { font-family: system-ui, sans-serif; font-size: 15px; --bg: #f5f5f5; --surface: #fff; --accent: #0070f3; --border: #ddd }
|
|
@@ -11,7 +12,9 @@
|
|
|
11
12
|
|
|
12
13
|
/* Login */
|
|
13
14
|
#login { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 2rem; width: min(360px, 90vw); display: flex; flex-direction: column; gap: .75rem }
|
|
14
|
-
#login h1 { font-size: 1.2rem; font-weight: 600 }
|
|
15
|
+
#login h1 { font-size: 1.2rem; font-weight: 600; display: flex; align-items: center; gap: .5rem }
|
|
16
|
+
#login h1 img { width: 1.4rem; height: 1.4rem }
|
|
17
|
+
#chat-header img { width: 1.1rem; height: 1.1rem }
|
|
15
18
|
#login input { border: 1px solid var(--border); border-radius: 6px; padding: .5rem .75rem; font-size: 1rem; width: 100% }
|
|
16
19
|
#login button { background: var(--accent); color: #fff; border: none; border-radius: 6px; padding: .6rem; font-size: 1rem; cursor: pointer }
|
|
17
20
|
#login button:hover { opacity: .85 }
|
|
@@ -38,7 +41,7 @@
|
|
|
38
41
|
<body>
|
|
39
42
|
|
|
40
43
|
<div id="login">
|
|
41
|
-
<h1>streamo chat</h1>
|
|
44
|
+
<h1><img src="/streamo.svg" alt="">streamo chat</h1>
|
|
42
45
|
<input id="username" placeholder="username" autocomplete="username">
|
|
43
46
|
<input id="password" type="password" placeholder="password" autocomplete="current-password">
|
|
44
47
|
<button id="join-btn">join</button>
|
|
@@ -47,7 +50,7 @@
|
|
|
47
50
|
|
|
48
51
|
<div id="chat">
|
|
49
52
|
<div id="chat-header">
|
|
50
|
-
streamo chat <span id="my-name"></span>
|
|
53
|
+
<img src="/streamo.svg" alt="">streamo chat <span id="my-name"></span>
|
|
51
54
|
</div>
|
|
52
55
|
<div id="messages"></div>
|
|
53
56
|
<div id="input-row">
|
|
@@ -4,13 +4,20 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>streamo explorer</title>
|
|
7
|
-
<link rel="icon"
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="/streamo.svg">
|
|
8
8
|
<link rel="stylesheet" href="/apps/styles/proto.css">
|
|
9
9
|
<style>
|
|
10
10
|
body { max-width: 60rem; margin: 0 auto; padding: 2rem 1.25rem; }
|
|
11
11
|
|
|
12
12
|
.header { display: flex; align-items: baseline; gap: 0.75rem; margin-bottom: 0.25rem; }
|
|
13
|
-
.wordmark {
|
|
13
|
+
.wordmark {
|
|
14
|
+
display: flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
gap: 0.5rem;
|
|
17
|
+
font-size: 1.6rem;
|
|
18
|
+
letter-spacing: -0.02em;
|
|
19
|
+
}
|
|
20
|
+
.wordmark img { width: 1.8rem; height: 1.8rem; }
|
|
14
21
|
.crumbs { font-size: 0.85rem; color: var(--ink-dim); }
|
|
15
22
|
.back { cursor: pointer; color: var(--ink-dim); font-size: 0.85rem; display: inline-block; margin-bottom: 1rem; }
|
|
16
23
|
.back:hover { color: var(--ink); }
|
|
@@ -221,6 +228,19 @@
|
|
|
221
228
|
}
|
|
222
229
|
.repo-link:hover { background: var(--flash); text-decoration-style: solid; }
|
|
223
230
|
|
|
231
|
+
/* Sticky at-view header: selector + strip + tabs travel with you as
|
|
232
|
+
you scroll long value trees or storage detail. Background-cover so
|
|
233
|
+
content scrolling underneath doesn't bleed through. */
|
|
234
|
+
.atview-header {
|
|
235
|
+
position: sticky;
|
|
236
|
+
top: 0;
|
|
237
|
+
z-index: 10;
|
|
238
|
+
background: var(--bg, #fefdf8);
|
|
239
|
+
padding-top: 0.25rem;
|
|
240
|
+
border-bottom: 1px solid var(--rule);
|
|
241
|
+
margin-bottom: 0.75rem;
|
|
242
|
+
}
|
|
243
|
+
|
|
224
244
|
/* Byte stream — zoomed strip in a horizontally-scrollable container,
|
|
225
245
|
click-drag-to-pan inside for "look around" navigation. */
|
|
226
246
|
.byte-strip-container {
|
|
@@ -236,6 +256,45 @@
|
|
|
236
256
|
.byte-strip-container.dragging .chunk { cursor: grabbing; }
|
|
237
257
|
.byte-strip { display: block; }
|
|
238
258
|
|
|
259
|
+
/* Sig-coverage overlay: when hovering a sig anywhere on the page,
|
|
260
|
+
this rect is positioned over its [signedFrom, signedTo] byte range
|
|
261
|
+
on the strip. Subtle dashed band — doesn't fight the chunk colors. */
|
|
262
|
+
.byte-strip .sig-coverage {
|
|
263
|
+
fill: rgba(239, 68, 68, 0.12);
|
|
264
|
+
stroke: rgba(239, 68, 68, 0.6);
|
|
265
|
+
stroke-width: 1.5;
|
|
266
|
+
stroke-dasharray: 4 3;
|
|
267
|
+
opacity: 0;
|
|
268
|
+
transition: opacity 0.08s;
|
|
269
|
+
}
|
|
270
|
+
.byte-strip .sig-coverage.active { opacity: 1; }
|
|
271
|
+
|
|
272
|
+
/* "← older newer →" labels under the strip — no UI action, just
|
|
273
|
+
Tour-Guide orientation so the append direction is obvious. */
|
|
274
|
+
.strip-direction {
|
|
275
|
+
display: flex;
|
|
276
|
+
justify-content: space-between;
|
|
277
|
+
font-size: 0.7rem;
|
|
278
|
+
color: var(--ink-dim);
|
|
279
|
+
padding: 0 0.25rem;
|
|
280
|
+
margin-top: 0.15rem;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/* Live readout of the hovered chunk — codec, address, length. Quiet
|
|
284
|
+
by default; lights up when something's hovered. */
|
|
285
|
+
.chunk-inspector {
|
|
286
|
+
font-family: monospace;
|
|
287
|
+
font-size: 0.8rem;
|
|
288
|
+
padding: 0.25rem 0.5rem;
|
|
289
|
+
margin: 0.25rem 0 0.75rem;
|
|
290
|
+
border-radius: var(--radius);
|
|
291
|
+
transition: color 0.08s, background 0.08s;
|
|
292
|
+
}
|
|
293
|
+
.chunk-inspector.active {
|
|
294
|
+
color: var(--ink);
|
|
295
|
+
background: var(--flash);
|
|
296
|
+
}
|
|
297
|
+
|
|
239
298
|
.byte-map {
|
|
240
299
|
display: block;
|
|
241
300
|
}
|
|
@@ -276,6 +335,38 @@
|
|
|
276
335
|
.tv-array, .tv-object { color: #1e40af; background: rgba(59, 130, 246, 0.10); }
|
|
277
336
|
.tv-duple { color: #6b21a8; background: rgba(168, 85, 247, 0.10); }
|
|
278
337
|
|
|
338
|
+
/* Recursive typed-value tree — used for rehydrated views. Outer levels
|
|
339
|
+
expand inline; un-expanded composites become drillable chips. */
|
|
340
|
+
.tv-tree {
|
|
341
|
+
font-size: 0.85rem;
|
|
342
|
+
line-height: 1.7;
|
|
343
|
+
font-family: monospace;
|
|
344
|
+
margin: 0.4rem 0;
|
|
345
|
+
}
|
|
346
|
+
.tv-tree-row {
|
|
347
|
+
padding-left: 1.5rem;
|
|
348
|
+
}
|
|
349
|
+
.tv-tree .tv-bracket {
|
|
350
|
+
color: var(--ink-dim);
|
|
351
|
+
font-weight: 600;
|
|
352
|
+
}
|
|
353
|
+
.tv-tree .tv-bracket.clickable {
|
|
354
|
+
cursor: pointer;
|
|
355
|
+
}
|
|
356
|
+
.tv-tree .tv-bracket.clickable:hover {
|
|
357
|
+
background: var(--flash);
|
|
358
|
+
color: var(--ink);
|
|
359
|
+
}
|
|
360
|
+
.tv-tree .tv-key {
|
|
361
|
+
color: var(--ink-dim);
|
|
362
|
+
margin-right: 0.4rem;
|
|
363
|
+
}
|
|
364
|
+
.tv-drill {
|
|
365
|
+
cursor: pointer;
|
|
366
|
+
text-decoration: underline dotted var(--ink-dim);
|
|
367
|
+
}
|
|
368
|
+
.tv-drill:hover { background: var(--flash); text-decoration-style: solid; }
|
|
369
|
+
|
|
279
370
|
/* codec category palette — used in both the legend and the SVG fills */
|
|
280
371
|
.cat-commit { fill: #f59e0b; background: #f59e0b; }
|
|
281
372
|
.cat-sig { fill: #ef4444; background: #ef4444; }
|
|
@@ -344,7 +435,7 @@
|
|
|
344
435
|
</head>
|
|
345
436
|
<body>
|
|
346
437
|
<div class="header">
|
|
347
|
-
<div class="wordmark">streamo</div>
|
|
438
|
+
<div class="wordmark"><img src="/streamo.svg" alt="streamo">streamo</div>
|
|
348
439
|
<div class="crumbs">explorer</div>
|
|
349
440
|
</div>
|
|
350
441
|
<div id="conn" class="conn">connecting…</div>
|
|
@@ -443,11 +443,13 @@ function AtView ({ keyHex, address }) {
|
|
|
443
443
|
const isCommit = isCommitShape(decoded)
|
|
444
444
|
const isSig = codecType === 'SIGNATURE'
|
|
445
445
|
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
//
|
|
446
|
+
// Common header shown on every at-view: commit selector dropdown,
|
|
447
|
+
// byte-strip with the current chunk highlighted, then the value/
|
|
448
|
+
// storage tab nav. The byte-strip used to live only in the storage
|
|
449
|
+
// tab; promoting it lets you keep spatial context across tab and
|
|
450
|
+
// commit switches, and any data-addr hover (typed-tree chips,
|
|
451
|
+
// refs/referrers tables, kv addr links) cross-highlights and
|
|
452
|
+
// smooth-scrolls into view in the strip.
|
|
451
453
|
const tabs = h`
|
|
452
454
|
<nav class="tabs">
|
|
453
455
|
<a class=${() => { dep(); return ['tab', atTab === 'value' ? 'active' : null] }}
|
|
@@ -457,24 +459,25 @@ function AtView ({ keyHex, address }) {
|
|
|
457
459
|
</nav>
|
|
458
460
|
`
|
|
459
461
|
const selector = commitSelectorSection(repo, keyHex, resolvedAddr)
|
|
462
|
+
const bytes = byteStreamSection(repo, keyHex, resolvedAddr)
|
|
463
|
+
// Wrap in a sticky container so the selector + strip + tabs stay
|
|
464
|
+
// anchored as you scroll long value trees or storage detail.
|
|
465
|
+
const header = h`<div class="atview-header">${selector}${bytes}${tabs}</div>`
|
|
460
466
|
|
|
461
|
-
// Storage tab:
|
|
462
|
-
//
|
|
463
|
-
// referrers. The chunk graph from this chunk's perspective.
|
|
467
|
+
// Storage tab: this chunk's outgoing refs, raw bytes, and
|
|
468
|
+
// referrers. (byteStreamSection moved up into the header.)
|
|
464
469
|
if (atTab === 'storage') {
|
|
465
470
|
return h`
|
|
466
|
-
${
|
|
467
|
-
${tabs}
|
|
468
|
-
${byteStreamSection(repo, keyHex, resolvedAddr)}
|
|
471
|
+
${header}
|
|
469
472
|
${outgoingReferencesSection(repo, keyHex, resolvedAddr)}
|
|
470
473
|
${rawChunkSection(repo, resolvedAddr)}
|
|
471
474
|
${referrersSection(repo, keyHex, resolvedAddr)}
|
|
472
475
|
`
|
|
473
476
|
}
|
|
474
477
|
|
|
475
|
-
// Every value-tab branch below prepends ${
|
|
476
|
-
//
|
|
477
|
-
// top of the page when the repo has any
|
|
478
|
+
// Every value-tab branch below prepends ${header} so the UI is
|
|
479
|
+
// stable across navigation: selector + byte-strip + tabs are
|
|
480
|
+
// always at the top of the page when the repo has any commits.
|
|
478
481
|
|
|
479
482
|
// Value tab — branches by codec.
|
|
480
483
|
// Helper: render the kv-table of decoded fields for any Object/Array
|
|
@@ -543,13 +546,30 @@ function AtView ({ keyHex, address }) {
|
|
|
543
546
|
: h`<span class="verify-badge pending">…</span><span class="dim">not yet signed — sign in flight or pending</span>`,
|
|
544
547
|
covering ? 'verified' : 'unsigned'
|
|
545
548
|
)
|
|
549
|
+
// Commit fields render with two semantic specials: dataAddress and
|
|
550
|
+
// parent are *byte-address pointers* (their numeric value IS a
|
|
551
|
+
// navigation target), so they're clickable address pills directly
|
|
552
|
+
// — the chunk holding the FLOAT64 value is incidental and we
|
|
553
|
+
// skip the chunk-address column for those rows.
|
|
554
|
+
const addrLink = (addr) => addr === undefined
|
|
555
|
+
? h`<span class="dim">(none — first commit)</span>`
|
|
556
|
+
: h`<a class="addr-link" data-action="open-at" data-keyhex=${keyHex} data-addr=${addr}>@${addr}</a>`
|
|
557
|
+
const commitFieldsTable = h`
|
|
558
|
+
<table class="kv">
|
|
559
|
+
<tbody>
|
|
560
|
+
<tr><td class="mono">message</td><td>${typedValue(decoded.message)}</td></tr>
|
|
561
|
+
<tr><td class="mono">date</td><td>${typedValue(decoded.date)}</td></tr>
|
|
562
|
+
<tr><td class="mono">dataAddress</td><td>${addrLink(decoded.dataAddress)}</td></tr>
|
|
563
|
+
<tr><td class="mono">parent</td><td>${addrLink(decoded.parent)}</td></tr>
|
|
564
|
+
</tbody>
|
|
565
|
+
</table>
|
|
566
|
+
`
|
|
546
567
|
return h`
|
|
547
|
-
${
|
|
548
|
-
${tabs}
|
|
568
|
+
${header}
|
|
549
569
|
${banner}
|
|
550
|
-
${
|
|
551
|
-
<h3>
|
|
552
|
-
|
|
570
|
+
${commitFieldsTable}
|
|
571
|
+
<h3>value <span class="dim">at <a class="addr-link" data-action="open-at" data-keyhex=${keyHex} data-addr=${decoded.dataAddress}>@${decoded.dataAddress}</a></span></h3>
|
|
572
|
+
${valueTree(repo, keyHex, decoded.dataAddress)}
|
|
553
573
|
${changes
|
|
554
574
|
? h`
|
|
555
575
|
<h3>changed paths <span class="dim">(${changes.length})</span></h3>
|
|
@@ -581,8 +601,7 @@ function AtView ({ keyHex, address }) {
|
|
|
581
601
|
// Duple: explain what this tree-node IS, then show its two children.
|
|
582
602
|
if (codecType === 'DUPLE') {
|
|
583
603
|
return h`
|
|
584
|
-
${
|
|
585
|
-
${tabs}
|
|
604
|
+
${header}
|
|
586
605
|
${kindBanner('duple', h`<span class="dim">2-tuple, tree scaffolding</span>`)}
|
|
587
606
|
<p class="explainer">
|
|
588
607
|
A <strong>Duple</strong> is a 2-tuple — the building block streamo uses
|
|
@@ -616,8 +635,7 @@ function AtView ({ keyHex, address }) {
|
|
|
616
635
|
'verified'
|
|
617
636
|
)
|
|
618
637
|
return h`
|
|
619
|
-
${
|
|
620
|
-
${tabs}
|
|
638
|
+
${header}
|
|
621
639
|
${banner}
|
|
622
640
|
${sigDetailBody(repo, keyHex, resolvedAddr, decoded)}
|
|
623
641
|
<div class="dim" style="margin-top: 0.5rem;">switch to the <strong>storage</strong> tab above to see the raw chunk bytes, outgoing references, and what else points at this address.</div>
|
|
@@ -635,8 +653,7 @@ function AtView ({ keyHex, address }) {
|
|
|
635
653
|
? (isArray ? 'empty array' : 'empty object')
|
|
636
654
|
: (isArray ? 'array' : 'object')
|
|
637
655
|
return h`
|
|
638
|
-
${
|
|
639
|
-
${tabs}
|
|
656
|
+
${header}
|
|
640
657
|
${kindBanner(label, dim)}
|
|
641
658
|
${refsTable()}
|
|
642
659
|
${fieldCount > 0 ? h`
|
|
@@ -648,8 +665,7 @@ function AtView ({ keyHex, address }) {
|
|
|
648
665
|
|
|
649
666
|
// Primitive: just show it.
|
|
650
667
|
return h`
|
|
651
|
-
${
|
|
652
|
-
${tabs}
|
|
668
|
+
${header}
|
|
653
669
|
${kindBanner(codecType.toLowerCase())}
|
|
654
670
|
<pre class="value">${safeJSON(decoded)}</pre>
|
|
655
671
|
`
|
|
@@ -703,12 +719,22 @@ function byteStreamSection (repo, keyHex, currentAddress) {
|
|
|
703
719
|
const code = repo.resolve(addr)
|
|
704
720
|
if (!code || !code.length) break
|
|
705
721
|
const codec = repo.footerToCodec[code.at(-1)]
|
|
706
|
-
|
|
722
|
+
const chunk = {
|
|
707
723
|
address: addr,
|
|
708
724
|
start: addr - code.length + 1,
|
|
709
725
|
length: code.length,
|
|
710
726
|
codecType: codec?.type || '?'
|
|
711
|
-
}
|
|
727
|
+
}
|
|
728
|
+
// For sigs: precompute the byte range covered, so hover anywhere on
|
|
729
|
+
// the page can light up that range as an overlay band on the strip.
|
|
730
|
+
if (chunk.codecType === 'SIGNATURE') {
|
|
731
|
+
try {
|
|
732
|
+
const sig = repo.decode(addr)
|
|
733
|
+
chunk.signedFrom = sig.address
|
|
734
|
+
chunk.signedTo = addr - code.length
|
|
735
|
+
} catch {}
|
|
736
|
+
}
|
|
737
|
+
chunks.unshift(chunk)
|
|
712
738
|
addr -= code.length
|
|
713
739
|
}
|
|
714
740
|
if (!chunks.length) return null
|
|
@@ -744,6 +770,20 @@ function byteStreamSection (repo, keyHex, currentAddress) {
|
|
|
744
770
|
return item
|
|
745
771
|
})
|
|
746
772
|
const stripW = cursorX
|
|
773
|
+
// Map byte address → strip x. Used by the sig-coverage overlay so hover
|
|
774
|
+
// anywhere on the page can light up "what bytes does this sig sign".
|
|
775
|
+
// Stored as data attrs on the strip container so the hover handler
|
|
776
|
+
// can read without recomputing.
|
|
777
|
+
const xForByte = (byteAddr) => {
|
|
778
|
+
// Find the chunk containing this byte and interpolate within it.
|
|
779
|
+
for (const c of layout) {
|
|
780
|
+
if (byteAddr >= c.start && byteAddr <= c.address) {
|
|
781
|
+
const frac = c.length === 1 ? 0 : (byteAddr - c.start) / (c.length - 1)
|
|
782
|
+
return c.x + frac * c.w
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return 0
|
|
786
|
+
}
|
|
747
787
|
return h`
|
|
748
788
|
<h3>byte stream <span class="dim">(${total} bytes · ${chunks.length} chunks)</span></h3>
|
|
749
789
|
<div class="byte-map-legend">
|
|
@@ -756,21 +796,33 @@ function byteStreamSection (repo, keyHex, currentAddress) {
|
|
|
756
796
|
<span class="cat-num">num</span>
|
|
757
797
|
<span class="cat-var">var</span>
|
|
758
798
|
</div>
|
|
759
|
-
<div class="byte-strip-container" data-key=${`strip-${keyHex}`}>
|
|
799
|
+
<div class="byte-strip-container" data-key=${`strip-${keyHex}`} data-strip-w=${stripW}>
|
|
760
800
|
<svg class="byte-map byte-strip" width=${stripW} height=${H} viewBox=${`0 0 ${stripW} ${H}`}>
|
|
761
801
|
${layout.map(c => {
|
|
762
802
|
const cat = commitAddrs.has(c.address) ? 'commit' : codecCategory(c.codecType)
|
|
763
803
|
const cls = ['chunk', `cat-${cat}`, c.address === currentAddress ? 'current' : null]
|
|
804
|
+
// Sigs carry their coverage range in data-attrs so hover handlers
|
|
805
|
+
// (anywhere on the page) can position the coverage overlay.
|
|
806
|
+
// Non-sigs get null which removes the attrs.
|
|
807
|
+
const sigFromX = c.signedFrom != null ? xForByte(c.signedFrom) : null
|
|
808
|
+
const sigToX = c.signedTo != null ? xForByte(c.signedTo) : null
|
|
764
809
|
return h`<rect
|
|
765
810
|
class=${cls}
|
|
766
811
|
x=${c.x} y="0" width=${c.w} height=${H}
|
|
767
812
|
data-action="open-at"
|
|
768
813
|
data-keyhex=${keyHex}
|
|
769
814
|
data-addr=${c.address}
|
|
815
|
+
data-codec=${c.codecType}
|
|
816
|
+
data-len=${c.length}
|
|
817
|
+
data-sig-from-x=${sigFromX}
|
|
818
|
+
data-sig-to-x=${sigToX}
|
|
770
819
|
><title>${c.codecType} @${c.address} (${c.length} bytes)</title></rect>`
|
|
771
820
|
})}
|
|
821
|
+
<rect class="sig-coverage" x="0" y="0" width="0" height=${H} pointer-events="none"/>
|
|
772
822
|
</svg>
|
|
823
|
+
<div class="strip-direction"><span>← older</span><span>newer →</span></div>
|
|
773
824
|
</div>
|
|
825
|
+
<div class="chunk-inspector dim" data-key=${`inspector-${keyHex}`}>hover the strip to inspect a chunk</div>
|
|
774
826
|
`
|
|
775
827
|
}
|
|
776
828
|
|
|
@@ -861,38 +913,102 @@ function isDuple (v) {
|
|
|
861
913
|
// count chips ({ N fields } / [ N elements ]) — depth-controlled
|
|
862
914
|
// expansion is the next step in this thread (see THREADS.md).
|
|
863
915
|
function typedValue (v, depth = 0) {
|
|
864
|
-
if (v === null) return h`<span class="tv tv-null">null</span>`
|
|
865
|
-
if (v === undefined) return h`<span class="tv tv-undefined">undefined</span>`
|
|
916
|
+
if (v === null) return h`<span class="tv tv-null" title="NULL">null</span>`
|
|
917
|
+
if (v === undefined) return h`<span class="tv tv-undefined" title="UNDEFINED">undefined</span>`
|
|
866
918
|
if (typeof v === 'boolean') {
|
|
867
|
-
return h`<span class=${['tv', 'tv-bool', v ? 'tv-true' : 'tv-false']}>${v ? '✓' : '✗'} ${String(v)}</span>`
|
|
919
|
+
return h`<span class=${['tv', 'tv-bool', v ? 'tv-true' : 'tv-false']} title=${v ? 'TRUE' : 'FALSE'}>${v ? '✓' : '✗'} ${String(v)}</span>`
|
|
868
920
|
}
|
|
869
921
|
if (typeof v === 'string') {
|
|
870
922
|
const display = v.length > 60 ? v.slice(0, 60) + '…' : v
|
|
871
|
-
return h`<span class="tv tv-string"><span class="tv-quote">“</span>${display}<span class="tv-quote">”</span></span>`
|
|
923
|
+
return h`<span class="tv tv-string" title=${v.length === 0 ? 'EMPTY_STRING' : 'STRING'}><span class="tv-quote">“</span>${display}<span class="tv-quote">”</span></span>`
|
|
872
924
|
}
|
|
873
925
|
if (typeof v === 'number') {
|
|
874
|
-
|
|
926
|
+
// UINT7 is the codec for non-negative integers < 128; everything else
|
|
927
|
+
// routes through FLOAT64. Surfacing this distinction makes "why is
|
|
928
|
+
// this 1 byte vs 9" tactile when you hover.
|
|
929
|
+
const codec = (Number.isInteger(v) && v >= 0 && v < 128) ? 'UINT7' : 'FLOAT64'
|
|
930
|
+
return h`<span class="tv tv-num" title=${codec}>${String(v)}</span>`
|
|
875
931
|
}
|
|
876
932
|
if (v instanceof Date) {
|
|
877
|
-
return h`<span class="tv tv-date"><span class="tv-glyph">📅</span><time datetime=${v.toISOString()}>${v.toLocaleString()}</time></span>`
|
|
933
|
+
return h`<span class="tv tv-date" title="DATE"><span class="tv-glyph">📅</span><time datetime=${v.toISOString()}>${v.toLocaleString()}</time></span>`
|
|
878
934
|
}
|
|
879
935
|
if (v instanceof Uint8Array) {
|
|
880
|
-
return h`<span class="tv tv-bytes">Uint8Array(${v.length})</span>`
|
|
936
|
+
return h`<span class="tv tv-bytes" title=${v.length === 0 ? 'EMPTY_UINT8ARRAY' : (v.length <= 4 ? 'WORD or UINT8ARRAY' : 'UINT8ARRAY')}>Uint8Array(${v.length})</span>`
|
|
881
937
|
}
|
|
882
938
|
if (isDuple(v)) {
|
|
883
|
-
if (depth > 1) return h`<span class="tv tv-duple">Duple(…)</span>`
|
|
884
|
-
return h`<span class="tv tv-duple">Duple(${typedValue(v.v[0], depth + 1)}, ${typedValue(v.v[1], depth + 1)})</span>`
|
|
939
|
+
if (depth > 1) return h`<span class="tv tv-duple" title="DUPLE">Duple(…)</span>`
|
|
940
|
+
return h`<span class="tv tv-duple" title="DUPLE">Duple(${typedValue(v.v[0], depth + 1)}, ${typedValue(v.v[1], depth + 1)})</span>`
|
|
885
941
|
}
|
|
886
942
|
if (Array.isArray(v)) {
|
|
887
|
-
return h`<span class="tv tv-array">[ ${v.length} ${v.length === 1 ? 'element' : 'elements'} ]</span>`
|
|
943
|
+
return h`<span class="tv tv-array" title=${v.length === 0 ? 'EMPTY_ARRAY' : 'ARRAY'}>[ ${v.length} ${v.length === 1 ? 'element' : 'elements'} ]</span>`
|
|
888
944
|
}
|
|
889
945
|
if (typeof v === 'object') {
|
|
890
946
|
const n = Object.keys(v).length
|
|
891
|
-
return h`<span class="tv tv-object">{ ${n} ${n === 1 ? 'field' : 'fields'} }</span>`
|
|
947
|
+
return h`<span class="tv tv-object" title=${n === 0 ? 'EMPTY_OBJECT' : 'OBJECT'}>{ ${n} ${n === 1 ? 'field' : 'fields'} }</span>`
|
|
892
948
|
}
|
|
893
949
|
return h`<span class="tv">${String(v)}</span>`
|
|
894
950
|
}
|
|
895
951
|
|
|
952
|
+
// Recursive typed-value tree — like typedValue, but expands composites
|
|
953
|
+
// inline up to `depth` levels deep. Beyond depth, composites render as
|
|
954
|
+
// un-expanded chips. Click a chip to expand IN PLACE (forceExpanded);
|
|
955
|
+
// click an expanded composite's opening bracket to collapse it back to
|
|
956
|
+
// a chip (forceCollapsed). Force-expand and force-collapse override
|
|
957
|
+
// the default depth-based decision.
|
|
958
|
+
//
|
|
959
|
+
// Default depth=3 covers `{ name, messages: [{text, at}, ...] }` —
|
|
960
|
+
// outer object expanded, messages array expanded, message objects
|
|
961
|
+
// expanded, and primitives like text/at render inline.
|
|
962
|
+
const forceExpanded = new Set() // `${keyHex}:${address}` → user clicked chip
|
|
963
|
+
const forceCollapsed = new Set() // `${keyHex}:${address}` → user clicked bracket
|
|
964
|
+
|
|
965
|
+
function valueTree (repo, keyHex, address, depth = 3) {
|
|
966
|
+
let value, refs
|
|
967
|
+
try {
|
|
968
|
+
value = repo.decode(address)
|
|
969
|
+
refs = repo.asRefs(address)
|
|
970
|
+
} catch {
|
|
971
|
+
return h`<span class="dim">(decode error @${address})</span>`
|
|
972
|
+
}
|
|
973
|
+
if (typeof value !== 'object' || value === null || value instanceof Date || value instanceof Uint8Array) {
|
|
974
|
+
return typedValue(value)
|
|
975
|
+
}
|
|
976
|
+
const k = `${keyHex}:${address}`
|
|
977
|
+
const userExpanded = forceExpanded.has(k)
|
|
978
|
+
const userCollapsed = forceCollapsed.has(k)
|
|
979
|
+
const expand = userExpanded || (!userCollapsed && depth > 0)
|
|
980
|
+
if (!expand) {
|
|
981
|
+
return h`<a class="tv-drill" data-action="expand-tree"
|
|
982
|
+
data-keyhex=${keyHex} data-addr=${address}
|
|
983
|
+
title="click to expand · drill via storage tab if you need a full at-view"
|
|
984
|
+
>${typedValue(value)}</a>`
|
|
985
|
+
}
|
|
986
|
+
const isArray = Array.isArray(value)
|
|
987
|
+
const entries = isArray
|
|
988
|
+
? value.map((v, i) => [String(i), v, refs?.[i]])
|
|
989
|
+
: Object.entries(value).map(([k, v]) => [k, v, refs?.[k]])
|
|
990
|
+
if (entries.length === 0) {
|
|
991
|
+
return h`<span class="tv ${isArray ? 'tv-array' : 'tv-object'}">${isArray ? '[ ]' : '{ }'}</span>`
|
|
992
|
+
}
|
|
993
|
+
return h`
|
|
994
|
+
<div class="tv-tree ${isArray ? 'tv-tree-array' : 'tv-tree-object'}">
|
|
995
|
+
<span class="tv-bracket clickable" data-action="collapse-tree"
|
|
996
|
+
data-keyhex=${keyHex} data-addr=${address}
|
|
997
|
+
title="click to collapse"
|
|
998
|
+
>${isArray ? '[' : '{'}</span>
|
|
999
|
+
${entries.map(([k, v, addr]) => h`
|
|
1000
|
+
<div class="tv-tree-row">
|
|
1001
|
+
<span class="tv-key">${k}:</span>
|
|
1002
|
+
${addr !== undefined
|
|
1003
|
+
? valueTree(repo, keyHex, addr, depth - 1)
|
|
1004
|
+
: typedValue(v)}
|
|
1005
|
+
</div>
|
|
1006
|
+
`)}
|
|
1007
|
+
<span class="tv-bracket">${isArray ? ']' : '}'}</span>
|
|
1008
|
+
</div>
|
|
1009
|
+
`
|
|
1010
|
+
}
|
|
1011
|
+
|
|
896
1012
|
function safeGet (f) { try { return f() } catch { return undefined } }
|
|
897
1013
|
|
|
898
1014
|
// Build a child→parents index for the entire repo in one pass, so we can
|
|
@@ -1065,6 +1181,18 @@ appEl.addEventListener('click', e => {
|
|
|
1065
1181
|
el.closest('details.commit-selector')?.removeAttribute('open')
|
|
1066
1182
|
return go({ kind: 'at', keyHex: el.dataset.keyhex, address: +el.dataset.addr })
|
|
1067
1183
|
}
|
|
1184
|
+
case 'expand-tree': {
|
|
1185
|
+
const k = `${el.dataset.keyhex}:${el.dataset.addr}`
|
|
1186
|
+
forceExpanded.add(k)
|
|
1187
|
+
forceCollapsed.delete(k)
|
|
1188
|
+
return fire()
|
|
1189
|
+
}
|
|
1190
|
+
case 'collapse-tree': {
|
|
1191
|
+
const k = `${el.dataset.keyhex}:${el.dataset.addr}`
|
|
1192
|
+
forceCollapsed.add(k)
|
|
1193
|
+
forceExpanded.delete(k)
|
|
1194
|
+
return fire()
|
|
1195
|
+
}
|
|
1068
1196
|
}
|
|
1069
1197
|
})
|
|
1070
1198
|
|
|
@@ -1126,11 +1254,11 @@ appEl.addEventListener('pointerup', endDrag)
|
|
|
1126
1254
|
appEl.addEventListener('pointercancel', endDrag)
|
|
1127
1255
|
|
|
1128
1256
|
// Cross-highlight: hovering any element with data-addr highlights the
|
|
1129
|
-
// matching chunk in the byte-map
|
|
1130
|
-
//
|
|
1131
|
-
//
|
|
1132
|
-
// the
|
|
1133
|
-
//
|
|
1257
|
+
// matching chunk in the byte-map, populates the chunk inspector below
|
|
1258
|
+
// the strip with codec/addr/length, and (if the hovered chunk is a
|
|
1259
|
+
// signature) lights up its covered byte range as an overlay band on
|
|
1260
|
+
// the strip. If the hover came from somewhere other than the strip
|
|
1261
|
+
// itself, smooth-scroll the matching chunk into view in the strip.
|
|
1134
1262
|
appEl.addEventListener('mouseover', e => {
|
|
1135
1263
|
const el = e.target.closest('[data-addr]')
|
|
1136
1264
|
if (!el) return
|
|
@@ -1144,10 +1272,37 @@ appEl.addEventListener('mouseover', e => {
|
|
|
1144
1272
|
}
|
|
1145
1273
|
})
|
|
1146
1274
|
}
|
|
1275
|
+
// Look up the chunk's data on the strip rect for inspector + sig coverage.
|
|
1276
|
+
const stripRect = matches[0]?.closest('.byte-strip-container') ? matches[0]
|
|
1277
|
+
: appEl.querySelector(`.byte-strip .chunk[data-addr="${addr}"]`)
|
|
1278
|
+
if (stripRect) {
|
|
1279
|
+
const codec = stripRect.dataset.codec
|
|
1280
|
+
const len = stripRect.dataset.len
|
|
1281
|
+
for (const ins of appEl.querySelectorAll('.chunk-inspector')) {
|
|
1282
|
+
ins.textContent = codec ? `${codec} · @${addr} · ${len} bytes` : `chunk @${addr}`
|
|
1283
|
+
ins.classList.remove('dim')
|
|
1284
|
+
ins.classList.add('active')
|
|
1285
|
+
}
|
|
1286
|
+
const fromX = stripRect.getAttribute('data-sig-from-x')
|
|
1287
|
+
const toX = stripRect.getAttribute('data-sig-to-x')
|
|
1288
|
+
if (fromX != null && toX != null) {
|
|
1289
|
+
const overlay = stripRect.closest('.byte-strip').querySelector('.sig-coverage')
|
|
1290
|
+
if (overlay) {
|
|
1291
|
+
overlay.setAttribute('x', fromX)
|
|
1292
|
+
overlay.setAttribute('width', String(parseFloat(toX) - parseFloat(fromX)))
|
|
1293
|
+
overlay.classList.add('active')
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1147
1297
|
})
|
|
1148
1298
|
appEl.addEventListener('mouseout', e => {
|
|
1149
1299
|
const el = e.target.closest('[data-addr]')
|
|
1150
1300
|
if (!el) return
|
|
1151
|
-
appEl.querySelectorAll('.byte-map .chunk.hovered')
|
|
1152
|
-
|
|
1301
|
+
appEl.querySelectorAll('.byte-map .chunk.hovered').forEach(c => c.classList.remove('hovered'))
|
|
1302
|
+
appEl.querySelectorAll('.sig-coverage.active').forEach(o => o.classList.remove('active'))
|
|
1303
|
+
for (const ins of appEl.querySelectorAll('.chunk-inspector')) {
|
|
1304
|
+
ins.textContent = 'hover the strip to inspect a chunk'
|
|
1305
|
+
ins.classList.remove('active')
|
|
1306
|
+
ins.classList.add('dim')
|
|
1307
|
+
}
|
|
1153
1308
|
})
|
package/public/index.html
CHANGED
|
@@ -4,18 +4,56 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>streamo</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="/streamo.svg">
|
|
7
8
|
<link rel="stylesheet" href="/apps/styles/proto.css">
|
|
8
9
|
<style>
|
|
9
10
|
body { max-width: 44rem; margin: 0 auto; padding: 2.5rem 1.25rem; }
|
|
10
11
|
|
|
11
|
-
.wordmark {
|
|
12
|
-
|
|
12
|
+
.wordmark {
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
gap: 0.6rem;
|
|
16
|
+
font-size: 2.4rem;
|
|
17
|
+
letter-spacing: -0.02em;
|
|
18
|
+
margin-bottom: 0.15rem;
|
|
19
|
+
}
|
|
20
|
+
.wordmark img { width: 2.6rem; height: 2.6rem; }
|
|
21
|
+
.tagline { color: var(--ink-dim); font-size: 0.95rem; margin-bottom: 0.4rem; }
|
|
22
|
+
|
|
23
|
+
/* "what's running here" line — small, clickable, places the abstract
|
|
24
|
+
claims in the next paragraph onto a real running server. */
|
|
25
|
+
.here {
|
|
26
|
+
font-size: 0.78rem;
|
|
27
|
+
color: var(--ink-dim);
|
|
28
|
+
margin-bottom: 2rem;
|
|
29
|
+
font-family: monospace;
|
|
30
|
+
}
|
|
31
|
+
.here a {
|
|
32
|
+
color: var(--ink-dim);
|
|
33
|
+
text-decoration: underline dotted;
|
|
34
|
+
}
|
|
35
|
+
.here a:hover { color: var(--ink); text-decoration-style: solid; }
|
|
36
|
+
.here .pulse {
|
|
37
|
+
display: inline-block;
|
|
38
|
+
width: 0.5rem;
|
|
39
|
+
height: 0.5rem;
|
|
40
|
+
border-radius: 50%;
|
|
41
|
+
background: #16a34a;
|
|
42
|
+
margin-right: 0.4rem;
|
|
43
|
+
vertical-align: 0.05em;
|
|
44
|
+
animation: pulse 2s ease-in-out infinite;
|
|
45
|
+
}
|
|
46
|
+
.here .pulse.err { background: #dc2626; animation: none; }
|
|
47
|
+
@keyframes pulse {
|
|
48
|
+
0%, 100% { opacity: 0.4; }
|
|
49
|
+
50% { opacity: 1; }
|
|
50
|
+
}
|
|
13
51
|
|
|
14
52
|
.ideas {
|
|
15
53
|
display: flex;
|
|
16
54
|
flex-direction: column;
|
|
17
55
|
gap: 0.6rem;
|
|
18
|
-
margin-bottom:
|
|
56
|
+
margin-bottom: 1.5rem;
|
|
19
57
|
}
|
|
20
58
|
.idea {
|
|
21
59
|
display: flex;
|
|
@@ -27,6 +65,72 @@
|
|
|
27
65
|
.idea-text { color: var(--ink-dim); }
|
|
28
66
|
.idea-text strong { color: var(--ink); }
|
|
29
67
|
|
|
68
|
+
/* "try it" card — folds shut by default. When opened, derives a real
|
|
69
|
+
secp256k1 keypair from credentials in the browser. Concrete proof
|
|
70
|
+
of "your identity travels with you" instead of just claiming it. */
|
|
71
|
+
details.try-it {
|
|
72
|
+
border: 1.5px dashed var(--rule);
|
|
73
|
+
border-radius: var(--radius);
|
|
74
|
+
padding: 0.6rem 0.85rem;
|
|
75
|
+
margin-bottom: 2rem;
|
|
76
|
+
}
|
|
77
|
+
details.try-it[open] { border-color: var(--ink); border-style: solid; }
|
|
78
|
+
details.try-it > summary {
|
|
79
|
+
cursor: pointer;
|
|
80
|
+
font-size: 0.85rem;
|
|
81
|
+
color: var(--ink-dim);
|
|
82
|
+
}
|
|
83
|
+
details.try-it[open] > summary { color: var(--ink); margin-bottom: 0.65rem; }
|
|
84
|
+
details.try-it > summary::before {
|
|
85
|
+
content: '↳ ';
|
|
86
|
+
color: var(--ink-dim);
|
|
87
|
+
}
|
|
88
|
+
.try-it-note {
|
|
89
|
+
font-size: 0.78rem;
|
|
90
|
+
color: var(--ink-dim);
|
|
91
|
+
line-height: 1.5;
|
|
92
|
+
margin: 0 0 0.65rem;
|
|
93
|
+
}
|
|
94
|
+
.try-it-note strong { color: var(--ink); }
|
|
95
|
+
.try-it-form {
|
|
96
|
+
display: flex;
|
|
97
|
+
flex-wrap: wrap;
|
|
98
|
+
gap: 0.5rem;
|
|
99
|
+
margin-bottom: 0.65rem;
|
|
100
|
+
}
|
|
101
|
+
.try-it-form input {
|
|
102
|
+
flex: 1 1 8rem;
|
|
103
|
+
font-family: monospace;
|
|
104
|
+
font-size: 0.85rem;
|
|
105
|
+
padding: 0.35rem 0.55rem;
|
|
106
|
+
border: 1px solid var(--rule);
|
|
107
|
+
border-radius: var(--radius);
|
|
108
|
+
background: transparent;
|
|
109
|
+
color: var(--ink);
|
|
110
|
+
}
|
|
111
|
+
.try-it-form input:focus {
|
|
112
|
+
outline: none;
|
|
113
|
+
border-color: var(--ink);
|
|
114
|
+
}
|
|
115
|
+
.derived-key {
|
|
116
|
+
font-size: 0.78rem;
|
|
117
|
+
color: var(--ink-dim);
|
|
118
|
+
padding: 0.35rem 0.55rem;
|
|
119
|
+
border-radius: var(--radius);
|
|
120
|
+
background: rgba(0, 0, 0, 0.03);
|
|
121
|
+
word-break: break-all;
|
|
122
|
+
}
|
|
123
|
+
.derived-key .key-label {
|
|
124
|
+
display: block;
|
|
125
|
+
margin-bottom: 0.2rem;
|
|
126
|
+
font-family: monospace;
|
|
127
|
+
}
|
|
128
|
+
.derived-key .key-value {
|
|
129
|
+
font-family: monospace;
|
|
130
|
+
color: var(--ink);
|
|
131
|
+
}
|
|
132
|
+
.derived-key.computing .key-value { color: var(--ink-dim); font-style: italic; }
|
|
133
|
+
|
|
30
134
|
hr { margin: 2rem 0; }
|
|
31
135
|
|
|
32
136
|
.apps-heading {
|
|
@@ -44,7 +148,9 @@
|
|
|
44
148
|
}
|
|
45
149
|
|
|
46
150
|
.app-card {
|
|
47
|
-
display:
|
|
151
|
+
display: flex;
|
|
152
|
+
flex-direction: column;
|
|
153
|
+
gap: 0.25rem;
|
|
48
154
|
text-decoration: none;
|
|
49
155
|
color: var(--ink);
|
|
50
156
|
border: 1.5px solid var(--ink);
|
|
@@ -56,22 +162,41 @@
|
|
|
56
162
|
.app-card:hover { transform: translate(-1px, -1px); box-shadow: 3px 4px 0 var(--ink); }
|
|
57
163
|
.app-card:active { transform: translate(1px, 2px); box-shadow: none; }
|
|
58
164
|
|
|
59
|
-
.app-name {
|
|
165
|
+
.app-name-row {
|
|
166
|
+
display: flex;
|
|
167
|
+
align-items: baseline;
|
|
168
|
+
gap: 0.45rem;
|
|
169
|
+
}
|
|
170
|
+
.app-glyph { font-size: 1.1rem; }
|
|
171
|
+
.app-name { font-size: 1rem; }
|
|
60
172
|
.app-desc { font-size: 0.78rem; color: var(--ink-dim); line-height: 1.4; }
|
|
61
173
|
|
|
62
174
|
.footer {
|
|
175
|
+
display: flex;
|
|
176
|
+
flex-wrap: wrap;
|
|
177
|
+
gap: 0.5rem;
|
|
63
178
|
margin-top: 3rem;
|
|
64
|
-
|
|
179
|
+
}
|
|
180
|
+
.footer-chip {
|
|
181
|
+
font-size: 0.78rem;
|
|
65
182
|
color: var(--ink-dim);
|
|
183
|
+
text-decoration: none;
|
|
184
|
+
padding: 0.25rem 0.6rem;
|
|
185
|
+
border: 1px solid var(--rule);
|
|
186
|
+
border-radius: var(--radius);
|
|
66
187
|
}
|
|
67
|
-
.footer
|
|
188
|
+
.footer-chip:hover { color: var(--ink); border-color: var(--ink); }
|
|
68
189
|
</style>
|
|
69
190
|
</head>
|
|
70
191
|
<body>
|
|
71
192
|
|
|
72
|
-
<div class="wordmark">streamo</div>
|
|
193
|
+
<div class="wordmark"><img src="/streamo.svg" alt="streamo">streamo</div>
|
|
73
194
|
<p class="tagline">every device is an equal author</p>
|
|
74
195
|
|
|
196
|
+
<p class="here" id="here">
|
|
197
|
+
<span class="pulse" id="pulse"></span><span id="here-text">connecting…</span>
|
|
198
|
+
</p>
|
|
199
|
+
|
|
75
200
|
<div class="ideas">
|
|
76
201
|
<div class="idea">
|
|
77
202
|
<span class="idea-glyph">↔</span>
|
|
@@ -91,26 +216,106 @@
|
|
|
91
216
|
</div>
|
|
92
217
|
</div>
|
|
93
218
|
|
|
219
|
+
<details class="try-it">
|
|
220
|
+
<summary>try it — derive a streamo identity right here</summary>
|
|
221
|
+
<p class="try-it-note">
|
|
222
|
+
type any username and password. the keypair is computed in your browser via PBKDF2-SHA256 and is <strong>never sent</strong> anywhere. same inputs always produce the same key — that's how streamo identities travel without key files. <strong>don't use a real password</strong>; this is just a demo.
|
|
223
|
+
</p>
|
|
224
|
+
<div class="try-it-form">
|
|
225
|
+
<input type="text" placeholder="username" id="demo-username" autocomplete="off">
|
|
226
|
+
<input type="password" placeholder="password" id="demo-password" autocomplete="new-password">
|
|
227
|
+
</div>
|
|
228
|
+
<div class="derived-key" id="demo-key-row">
|
|
229
|
+
<span class="key-label">public key (secp256k1):</span>
|
|
230
|
+
<code class="key-value" id="demo-key">— enter credentials above —</code>
|
|
231
|
+
</div>
|
|
232
|
+
</details>
|
|
233
|
+
|
|
94
234
|
<hr>
|
|
95
235
|
|
|
96
236
|
<p class="apps-heading">apps</p>
|
|
97
237
|
<div class="app-grid">
|
|
98
238
|
<a class="app-card" href="/apps/chat/">
|
|
99
|
-
<div class="app-name">
|
|
100
|
-
|
|
239
|
+
<div class="app-name-row">
|
|
240
|
+
<span class="app-glyph">💬</span>
|
|
241
|
+
<span class="app-name">chat</span>
|
|
242
|
+
</div>
|
|
243
|
+
<div class="app-desc">p2p messaging — each participant owns their own signed message stream</div>
|
|
101
244
|
</a>
|
|
102
245
|
<a class="app-card" href="/apps/explorer/">
|
|
103
|
-
<div class="app-name">
|
|
104
|
-
|
|
246
|
+
<div class="app-name-row">
|
|
247
|
+
<span class="app-glyph">🔍</span>
|
|
248
|
+
<span class="app-name">explorer</span>
|
|
249
|
+
</div>
|
|
250
|
+
<div class="app-desc">browse repos, commit history, and the bytes underneath</div>
|
|
105
251
|
</a>
|
|
106
252
|
</div>
|
|
107
253
|
<p class="dim" style="margin-top: 1rem; font-size: 0.78rem;">
|
|
108
254
|
open both side by side — watch commits roll in as you chat
|
|
109
255
|
</p>
|
|
110
256
|
|
|
111
|
-
<
|
|
112
|
-
<a href="https://github.com/dtudury/streamo">github</a>
|
|
113
|
-
|
|
257
|
+
<div class="footer">
|
|
258
|
+
<a class="footer-chip" href="https://github.com/dtudury/streamo">github</a>
|
|
259
|
+
<a class="footer-chip" href="https://github.com/dtudury/streamo/blob/main/design.md">design.md</a>
|
|
260
|
+
<a class="footer-chip" href="https://github.com/dtudury/streamo/blob/main/ROADMAP.md">roadmap</a>
|
|
261
|
+
<a class="footer-chip" href="https://www.npmjs.com/package/@dtudury/streamo">npm</a>
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
<script type="module">
|
|
265
|
+
import { Signer } from '/streamo/Signer.js'
|
|
266
|
+
import { bytesToHex } from '/streamo/utils.js'
|
|
267
|
+
|
|
268
|
+
// Surface "what's running here" — server's primary key as a clickable
|
|
269
|
+
// link to the explorer. Replaces the abstract "no server holds
|
|
270
|
+
// authority" claim above with a concrete, navigable instance of it.
|
|
271
|
+
const pulseEl = document.getElementById('pulse')
|
|
272
|
+
const hereEl = document.getElementById('here-text')
|
|
273
|
+
try {
|
|
274
|
+
const info = await fetch('/api/info').then(r => r.json())
|
|
275
|
+
const truncKey = info.primaryKeyHex.slice(0, 12) + '…'
|
|
276
|
+
hereEl.innerHTML = `relaying <a href="/apps/explorer/#/repo/${info.primaryKeyHex}">${truncKey}</a> as "${info.name || 'this server'}"`
|
|
277
|
+
} catch (e) {
|
|
278
|
+
pulseEl.classList.add('err')
|
|
279
|
+
hereEl.textContent = 'not connected to a streamo server'
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Derive-on-type for the try-it widget. Debounced because PBKDF2 is
|
|
283
|
+
// intentionally slow; we don't want to hammer the browser per keystroke.
|
|
284
|
+
const u = document.getElementById('demo-username')
|
|
285
|
+
const p = document.getElementById('demo-password')
|
|
286
|
+
const out = document.getElementById('demo-key')
|
|
287
|
+
const row = document.getElementById('demo-key-row')
|
|
288
|
+
let timer = null
|
|
289
|
+
let inFlight = 0
|
|
290
|
+
function update () {
|
|
291
|
+
clearTimeout(timer)
|
|
292
|
+
const username = u.value.trim()
|
|
293
|
+
const password = p.value
|
|
294
|
+
if (!username || !password) {
|
|
295
|
+
row.classList.remove('computing')
|
|
296
|
+
out.textContent = '— enter credentials above —'
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
row.classList.add('computing')
|
|
300
|
+
out.textContent = 'computing PBKDF2…'
|
|
301
|
+
const me = ++inFlight
|
|
302
|
+
timer = setTimeout(async () => {
|
|
303
|
+
try {
|
|
304
|
+
const signer = new Signer(username, password, 1)
|
|
305
|
+
const { publicKey } = await signer.keysFor('chat')
|
|
306
|
+
if (me !== inFlight) return // user kept typing; stale
|
|
307
|
+
row.classList.remove('computing')
|
|
308
|
+
out.textContent = '0x' + bytesToHex(publicKey)
|
|
309
|
+
} catch (e) {
|
|
310
|
+
if (me !== inFlight) return
|
|
311
|
+
row.classList.remove('computing')
|
|
312
|
+
out.textContent = `error: ${e.message}`
|
|
313
|
+
}
|
|
314
|
+
}, 300)
|
|
315
|
+
}
|
|
316
|
+
u.addEventListener('input', update)
|
|
317
|
+
p.addEventListener('input', update)
|
|
318
|
+
</script>
|
|
114
319
|
|
|
115
320
|
</body>
|
|
116
321
|
</html>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<svg viewBox="0 0 680 680" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
|
|
2
|
+
<!-- streamo mark — yin-yang seam meets basketball seam, asymmetric so the
|
|
3
|
+
S flows in one direction (append-only). 7 named circles intersect to
|
|
4
|
+
define every curve in the mark; nothing is freehand, every point is
|
|
5
|
+
provably-related to every other point. -->
|
|
6
|
+
<!-- 7 circles: Ball(340,340,r280), Yang(340,200,r140), Yin(340,480,r140), -->
|
|
7
|
+
<!-- Happy(397.7,240.2,r302.4), Sad(370.9,370.9,r271.6), -->
|
|
8
|
+
<!-- Little(394,543,r70), Big(424,292,r183.2) -->
|
|
9
|
+
|
|
10
|
+
<!-- ABJ: Ball A→B, Sad B→J, Yang J→A -->
|
|
11
|
+
<path d="M 340,60 A 280,280 0 0,1 582,200 A 271.6,271.6 0 0,0 447,110 A 140,140 0 0,0 340,60 Z"/>
|
|
12
|
+
|
|
13
|
+
<!-- JDKL: Yang J→D, Yin D→K, Happy K→L, Sad L→J -->
|
|
14
|
+
<path d="M 447,110 A 140,140 0 0,1 340,340 A 140,140 0 0,0 200,465 A 302.4,302.4 0 0,1 105,319 A 271.6,271.6 0 0,1 447,110 Z"/>
|
|
15
|
+
|
|
16
|
+
<!-- BFME: Ball B→F, Happy F→M, Little M→E, Big E→B -->
|
|
17
|
+
<path d="M 582,200 A 280,280 0 0,1 582,480 A 302.4,302.4 0 0,1 327,535 A 70,70 0 0,1 402,474 A 183.2,183.2 0 0,0 582,200 Z"/>
|
|
18
|
+
|
|
19
|
+
<!-- GCL: Ball G→C, Happy C→L, Sad L→G -->
|
|
20
|
+
<path d="M 200,582 A 280,280 0 0,1 98,200 A 302.4,302.4 0 0,0 105,319 A 271.6,271.6 0 0,0 200,582 Z"/>
|
|
21
|
+
|
|
22
|
+
<!-- HIKM: Ball H→I, Yin I→K, Happy K→M, Little M→H -->
|
|
23
|
+
<path d="M 412,611 A 280,280 0 0,1 340,620 A 140,140 0 0,1 200,465 A 302.4,302.4 0 0,0 327,535 A 70,70 0 0,0 412,611 Z"/>
|
|
24
|
+
</svg>
|