@dtudury/streamo 4.0.2 → 4.0.5
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 +5 -1
- package/package.json +1 -1
- package/public/apps/chat/index.html +19 -3
- package/public/apps/chat/server.js +23 -3
- package/public/apps/explorer/index.html +121 -5
- package/public/apps/explorer/main.js +469 -96
- package/public/index.html +329 -15
- package/public/streamo/mount.js +135 -21
- package/public/streamo.svg +24 -0
|
@@ -58,6 +58,31 @@ function fire () {
|
|
|
58
58
|
requestAnimationFrame(() => { stripSyncScheduled = false; syncByteStrips() })
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// Hover-only signal — separate from the bridge. Hover events that
|
|
62
|
+
// only set hoveredAddress fire hoverSignal exclusively; slots that
|
|
63
|
+
// read hoverDep() re-run, slots that don't are left alone. This is
|
|
64
|
+
// what keeps hovering the strip from re-rendering the strip itself.
|
|
65
|
+
const hoverSignal = {}
|
|
66
|
+
const hoverDep = () => recaller.reportKeyAccess(hoverSignal, 'data')
|
|
67
|
+
const hoverFire = () => recaller.reportKeyMutation(hoverSignal, 'data')
|
|
68
|
+
|
|
69
|
+
// View-shape signal — fires only when view.kind or view.keyHex
|
|
70
|
+
// changes. The outer mount slot watches this (NOT bridge), so
|
|
71
|
+
// intra-repo navigation (address changes within an at-view) does
|
|
72
|
+
// NOT re-run the outer slot, does NOT recreate AtView's inner slots,
|
|
73
|
+
// does NOT fresh-mount the byte-strip-container. Inner slots watch
|
|
74
|
+
// bridge — they re-run on address change, chunk arrivals, tab clicks,
|
|
75
|
+
// async results — and recursive-reconcile preserves the strip's DOM
|
|
76
|
+
// (scrollLeft, focus, keyed children) across those re-runs.
|
|
77
|
+
//
|
|
78
|
+
// Together with hoverSignal, this is the full signal decomposition:
|
|
79
|
+
// viewKindSignal — kind/keyHex (registry ↔ at-view, repo switch)
|
|
80
|
+
// bridge — chunks, address, tab, async (everything else)
|
|
81
|
+
// hoverSignal — strip hover preview
|
|
82
|
+
const viewKindSignal = {}
|
|
83
|
+
const viewKindDep = () => recaller.reportKeyAccess(viewKindSignal, 'data')
|
|
84
|
+
const viewKindFire = () => recaller.reportKeyMutation(viewKindSignal, 'data')
|
|
85
|
+
|
|
61
86
|
// ── Hash routing ──────────────────────────────────────────────────────────
|
|
62
87
|
|
|
63
88
|
function viewFromHash () {
|
|
@@ -80,15 +105,19 @@ function hashFromView (v) {
|
|
|
80
105
|
|
|
81
106
|
let view = viewFromHash()
|
|
82
107
|
function go (next) {
|
|
108
|
+
const kindChanged = next.kind !== view.kind || next.keyHex !== view.keyHex
|
|
83
109
|
view = next
|
|
84
110
|
const target = hashFromView(next)
|
|
85
111
|
if (location.hash !== target) location.hash = target
|
|
112
|
+
if (kindChanged) viewKindFire()
|
|
86
113
|
fire()
|
|
87
114
|
}
|
|
88
115
|
window.addEventListener('hashchange', () => {
|
|
89
116
|
const next = viewFromHash()
|
|
90
117
|
if (next.kind === view.kind && next.keyHex === view.keyHex && next.address === view.address) return
|
|
118
|
+
const kindChanged = next.kind !== view.kind || next.keyHex !== view.keyHex
|
|
91
119
|
view = next
|
|
120
|
+
if (kindChanged) viewKindFire()
|
|
92
121
|
fire()
|
|
93
122
|
})
|
|
94
123
|
|
|
@@ -97,6 +126,15 @@ window.addEventListener('hashchange', () => {
|
|
|
97
126
|
// drill-down. Reset to default on registry/repo views (set in go()).
|
|
98
127
|
let atTab = 'value'
|
|
99
128
|
|
|
129
|
+
// Live-hover-preview state. When the user hovers a chunk on the byte
|
|
130
|
+
// strip, the page content below the tabs renders that chunk's value/
|
|
131
|
+
// storage instead of the URL's. Click to actually navigate. The header
|
|
132
|
+
// (selector + strip + tabs) keeps showing where you ARE; only the
|
|
133
|
+
// content area peeks ahead. Set by the mouseover handler when on a
|
|
134
|
+
// strip; cleared on mouseout. Module-level so AtView reads it inside
|
|
135
|
+
// the slot's reactive cell.
|
|
136
|
+
let hoveredAddress = null
|
|
137
|
+
|
|
100
138
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
101
139
|
|
|
102
140
|
const truncKey = k => k.slice(0, 12) + '…'
|
|
@@ -407,75 +445,102 @@ function repoExtras (repo, keyHex) {
|
|
|
407
445
|
`
|
|
408
446
|
}
|
|
409
447
|
|
|
410
|
-
function AtView ({ keyHex
|
|
448
|
+
function AtView ({ keyHex }) {
|
|
449
|
+
// AtView's body is built once per repo (the outer mount slot only
|
|
450
|
+
// re-runs on view.kind / view.keyHex changes — see viewKindSignal).
|
|
451
|
+
// Anything that depends on view.address must read it fresh inside a
|
|
452
|
+
// reactive cell, NOT capture it in closure here. Same for resolved
|
|
453
|
+
// chunk lookups: each slot calls resolveContext(repo) on every run.
|
|
454
|
+
const resolveContext = (repo) => {
|
|
455
|
+
let resolvedAddr = view.address
|
|
456
|
+
if (view.address === 'HEAD') {
|
|
457
|
+
resolvedAddr = resolveHead(repo)
|
|
458
|
+
if (resolvedAddr === undefined) return { state: 'no-head' }
|
|
459
|
+
}
|
|
460
|
+
if (resolvedAddr >= repo.byteLength) return { state: 'loading' }
|
|
461
|
+
return { state: 'ok', resolvedAddr }
|
|
462
|
+
}
|
|
463
|
+
|
|
411
464
|
return h`
|
|
412
465
|
<a class="back" data-action="back-registry">← all repos</a>
|
|
413
466
|
<div class="keyfull">
|
|
414
467
|
<a class="repo-link" data-action="back-repo" data-keyhex=${keyHex}>${truncKey(keyHex)}</a>
|
|
415
|
-
<span class="dim"> @ ${address}</span>
|
|
468
|
+
<span class="dim"> @ ${() => { dep(); return view.address }}</span>
|
|
416
469
|
</div>
|
|
470
|
+
|
|
417
471
|
${() => {
|
|
472
|
+
// HEADER slot — bridge only. Re-runs on chunk arrivals, navigation,
|
|
473
|
+
// tab clicks, and async results. Does NOT re-run on hover (which
|
|
474
|
+
// fires hoverSignal exclusively), so hovering the strip leaves the
|
|
475
|
+
// selector + strip itself + tabs untouched. This is the whole
|
|
476
|
+
// reason the hover signal exists as a separate channel.
|
|
418
477
|
dep()
|
|
419
478
|
const repo = registry.get(keyHex)
|
|
420
|
-
if (!repo) return
|
|
479
|
+
if (!repo) return null
|
|
480
|
+
const ctx = resolveContext(repo)
|
|
481
|
+
if (ctx.state !== 'ok') return null
|
|
482
|
+
const tabs = h`
|
|
483
|
+
<nav class="tabs">
|
|
484
|
+
<a class=${() => { dep(); return ['tab', atTab === 'value' ? 'active' : null] }}
|
|
485
|
+
data-action="set-tab" data-tab="value">value</a>
|
|
486
|
+
<a class=${() => { dep(); return ['tab', atTab === 'storage' ? 'active' : null] }}
|
|
487
|
+
data-action="set-tab" data-tab="storage">storage</a>
|
|
488
|
+
</nav>
|
|
489
|
+
`
|
|
490
|
+
const selector = commitSelectorSection(repo, keyHex, ctx.resolvedAddr)
|
|
491
|
+
const bytes = byteStreamSection(repo, keyHex, ctx.resolvedAddr)
|
|
492
|
+
return h`<div class="atview-header">${selector}${bytes}${tabs}</div>`
|
|
493
|
+
}}
|
|
421
494
|
|
|
422
|
-
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
495
|
+
${() => {
|
|
496
|
+
// CONTENT slot — bridge + hover. Re-runs on hover (so the page
|
|
497
|
+
// content peeks at the hovered chunk) AND on bridge fires (for
|
|
498
|
+
// chunk arrivals, tab clicks, async results). Renders the tab
|
|
499
|
+
// body for contentAddr — the hovered address if peeking, else the
|
|
500
|
+
// URL's address.
|
|
501
|
+
dep()
|
|
502
|
+
hoverDep()
|
|
503
|
+
const repo = registry.get(keyHex)
|
|
504
|
+
if (!repo) return h`<div class="empty">opening…</div>`
|
|
505
|
+
const ctx = resolveContext(repo)
|
|
506
|
+
if (ctx.state === 'no-head') {
|
|
507
|
+
return h`
|
|
508
|
+
<h2>at HEAD <span class="dim">(no commits yet)</span></h2>
|
|
509
|
+
<div class="empty">this repo doesn't have any commits yet — HEAD will resolve to the most-recent commit once one lands.</div>
|
|
510
|
+
${repoExtras(repo, keyHex)}
|
|
511
|
+
`
|
|
435
512
|
}
|
|
436
|
-
if (
|
|
513
|
+
if (ctx.state === 'loading') return h`<div class="empty">loading…</div>`
|
|
514
|
+
const resolvedAddr = ctx.resolvedAddr
|
|
515
|
+
|
|
516
|
+
// Live hover preview: contentAddr peeks at the hovered chunk;
|
|
517
|
+
// header still shows resolvedAddr (the URL position). Click to
|
|
518
|
+
// navigate.
|
|
519
|
+
const contentAddr = hoveredAddress != null && hoveredAddress < repo.byteLength
|
|
520
|
+
? hoveredAddress
|
|
521
|
+
: resolvedAddr
|
|
437
522
|
|
|
438
523
|
let info
|
|
439
|
-
try { info = valueAndChildren(repo,
|
|
524
|
+
try { info = valueAndChildren(repo, contentAddr) }
|
|
440
525
|
catch (e) { return h`<pre class="value">decode error: ${e.message}</pre>` }
|
|
441
526
|
|
|
442
527
|
const { codecType, refs, decoded } = info
|
|
443
528
|
const isCommit = isCommitShape(decoded)
|
|
444
529
|
const isSig = codecType === 'SIGNATURE'
|
|
445
530
|
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
// the current address isn't a commit, the summary shows "detached".
|
|
451
|
-
const tabs = h`
|
|
452
|
-
<nav class="tabs">
|
|
453
|
-
<a class=${() => { dep(); return ['tab', atTab === 'value' ? 'active' : null] }}
|
|
454
|
-
data-action="set-tab" data-tab="value">value</a>
|
|
455
|
-
<a class=${() => { dep(); return ['tab', atTab === 'storage' ? 'active' : null] }}
|
|
456
|
-
data-action="set-tab" data-tab="storage">storage</a>
|
|
457
|
-
</nav>
|
|
458
|
-
`
|
|
459
|
-
const selector = commitSelectorSection(repo, keyHex, resolvedAddr)
|
|
460
|
-
|
|
461
|
-
// Storage tab: spatial view of where this chunk lives in the byte
|
|
462
|
-
// stream + outgoing references + this chunk's bytes + incoming
|
|
463
|
-
// referrers. The chunk graph from this chunk's perspective.
|
|
531
|
+
// Storage tab: position in stream + reachable commit, then this
|
|
532
|
+
// chunk's outgoing refs, raw bytes, and referrers. All for
|
|
533
|
+
// contentAddr (the peeked chunk during hover, otherwise the
|
|
534
|
+
// URL's address). Header is rendered by the sibling header slot.
|
|
464
535
|
if (atTab === 'storage') {
|
|
465
536
|
return h`
|
|
466
|
-
${
|
|
467
|
-
${
|
|
468
|
-
${
|
|
469
|
-
${
|
|
470
|
-
${rawChunkSection(repo, resolvedAddr)}
|
|
471
|
-
${referrersSection(repo, keyHex, resolvedAddr)}
|
|
537
|
+
${chunkContextSection(repo, keyHex, contentAddr)}
|
|
538
|
+
${outgoingReferencesSection(repo, keyHex, contentAddr)}
|
|
539
|
+
${rawChunkSection(repo, contentAddr)}
|
|
540
|
+
${referrersSection(repo, keyHex, contentAddr)}
|
|
472
541
|
`
|
|
473
542
|
}
|
|
474
543
|
|
|
475
|
-
// Every value-tab branch below prepends ${selector}${tabs} so the
|
|
476
|
-
// UI is stable across navigation: the selector is always at the
|
|
477
|
-
// top of the page when the repo has any sigs.
|
|
478
|
-
|
|
479
544
|
// Value tab — branches by codec.
|
|
480
545
|
// Helper: render the kv-table of decoded fields for any Object/Array
|
|
481
546
|
// (including commits, which are just OBJECTs with a known shape).
|
|
@@ -525,7 +590,7 @@ function AtView ({ keyHex, address }) {
|
|
|
525
590
|
// verify state from the covering sig; the verification table at the
|
|
526
591
|
// bottom links to that sig and shows its bytes.
|
|
527
592
|
if (isCommit) {
|
|
528
|
-
const covering = findCoveringSig(repo,
|
|
593
|
+
const covering = findCoveringSig(repo, contentAddr)
|
|
529
594
|
const parentDataAddr = decoded.parent !== undefined
|
|
530
595
|
? safeGet(() => repo.decode(decoded.parent)?.dataAddress)
|
|
531
596
|
: undefined
|
|
@@ -543,13 +608,35 @@ function AtView ({ keyHex, address }) {
|
|
|
543
608
|
: h`<span class="verify-badge pending">…</span><span class="dim">not yet signed — sign in flight or pending</span>`,
|
|
544
609
|
covering ? 'verified' : 'unsigned'
|
|
545
610
|
)
|
|
611
|
+
// Commit fields render with two semantic specials: dataAddress and
|
|
612
|
+
// parent are *byte-address pointers* (their numeric value IS a
|
|
613
|
+
// navigation target), so they're clickable address pills directly
|
|
614
|
+
// — the chunk holding the FLOAT64 value is incidental and we
|
|
615
|
+
// skip the chunk-address column for those rows.
|
|
616
|
+
// dataAddress and parent are byte-address pointers held as
|
|
617
|
+
// FLOAT64 values. The → glyph signals "this number is a
|
|
618
|
+
// pointer; click to follow it." Without it, the address pill
|
|
619
|
+
// looks like any other typedValue(number) and the navigation
|
|
620
|
+
// hint depends on the user already knowing what these fields
|
|
621
|
+
// mean.
|
|
622
|
+
const addrLink = (addr) => addr === undefined
|
|
623
|
+
? h`<span class="dim">(none — first commit)</span>`
|
|
624
|
+
: h`<span class="dim">→ </span><a class="addr-link" data-action="open-at" data-keyhex=${keyHex} data-addr=${addr}>@${addr}</a>`
|
|
625
|
+
const commitFieldsTable = h`
|
|
626
|
+
<table class="kv">
|
|
627
|
+
<tbody>
|
|
628
|
+
<tr><td class="mono">message</td><td>${typedValue(decoded.message)}</td></tr>
|
|
629
|
+
<tr><td class="mono">date</td><td>${typedValue(decoded.date)}</td></tr>
|
|
630
|
+
<tr><td class="mono">dataAddress</td><td>${addrLink(decoded.dataAddress)}</td></tr>
|
|
631
|
+
<tr><td class="mono">parent</td><td>${addrLink(decoded.parent)}</td></tr>
|
|
632
|
+
</tbody>
|
|
633
|
+
</table>
|
|
634
|
+
`
|
|
546
635
|
return h`
|
|
547
|
-
${selector}
|
|
548
|
-
${tabs}
|
|
549
636
|
${banner}
|
|
550
|
-
${
|
|
551
|
-
<h3>
|
|
552
|
-
|
|
637
|
+
${commitFieldsTable}
|
|
638
|
+
<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>
|
|
639
|
+
${valueTree(repo, keyHex, decoded.dataAddress)}
|
|
553
640
|
${changes
|
|
554
641
|
? h`
|
|
555
642
|
<h3>changed paths <span class="dim">(${changes.length})</span></h3>
|
|
@@ -581,8 +668,6 @@ function AtView ({ keyHex, address }) {
|
|
|
581
668
|
// Duple: explain what this tree-node IS, then show its two children.
|
|
582
669
|
if (codecType === 'DUPLE') {
|
|
583
670
|
return h`
|
|
584
|
-
${selector}
|
|
585
|
-
${tabs}
|
|
586
671
|
${kindBanner('duple', h`<span class="dim">2-tuple, tree scaffolding</span>`)}
|
|
587
672
|
<p class="explainer">
|
|
588
673
|
A <strong>Duple</strong> is a 2-tuple — the building block streamo uses
|
|
@@ -610,16 +695,14 @@ function AtView ({ keyHex, address }) {
|
|
|
610
695
|
'signature chunk',
|
|
611
696
|
() => {
|
|
612
697
|
dep()
|
|
613
|
-
const status = verifyStatus(repo, keyHex, decoded,
|
|
698
|
+
const status = verifyStatus(repo, keyHex, decoded, contentAddr)
|
|
614
699
|
return h`${verifyBadge(status)} <span class="dim">${verifyLabel(status)}</span>`
|
|
615
700
|
},
|
|
616
701
|
'verified'
|
|
617
702
|
)
|
|
618
703
|
return h`
|
|
619
|
-
${selector}
|
|
620
|
-
${tabs}
|
|
621
704
|
${banner}
|
|
622
|
-
${sigDetailBody(repo, keyHex,
|
|
705
|
+
${sigDetailBody(repo, keyHex, contentAddr, decoded)}
|
|
623
706
|
<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>
|
|
624
707
|
`
|
|
625
708
|
}
|
|
@@ -635,8 +718,6 @@ function AtView ({ keyHex, address }) {
|
|
|
635
718
|
? (isArray ? 'empty array' : 'empty object')
|
|
636
719
|
: (isArray ? 'array' : 'object')
|
|
637
720
|
return h`
|
|
638
|
-
${selector}
|
|
639
|
-
${tabs}
|
|
640
721
|
${kindBanner(label, dim)}
|
|
641
722
|
${refsTable()}
|
|
642
723
|
${fieldCount > 0 ? h`
|
|
@@ -648,8 +729,6 @@ function AtView ({ keyHex, address }) {
|
|
|
648
729
|
|
|
649
730
|
// Primitive: just show it.
|
|
650
731
|
return h`
|
|
651
|
-
${selector}
|
|
652
|
-
${tabs}
|
|
653
732
|
${kindBanner(codecType.toLowerCase())}
|
|
654
733
|
<pre class="value">${safeJSON(decoded)}</pre>
|
|
655
734
|
`
|
|
@@ -703,12 +782,22 @@ function byteStreamSection (repo, keyHex, currentAddress) {
|
|
|
703
782
|
const code = repo.resolve(addr)
|
|
704
783
|
if (!code || !code.length) break
|
|
705
784
|
const codec = repo.footerToCodec[code.at(-1)]
|
|
706
|
-
|
|
785
|
+
const chunk = {
|
|
707
786
|
address: addr,
|
|
708
787
|
start: addr - code.length + 1,
|
|
709
788
|
length: code.length,
|
|
710
789
|
codecType: codec?.type || '?'
|
|
711
|
-
}
|
|
790
|
+
}
|
|
791
|
+
// For sigs: precompute the byte range covered, so hover anywhere on
|
|
792
|
+
// the page can light up that range as an overlay band on the strip.
|
|
793
|
+
if (chunk.codecType === 'SIGNATURE') {
|
|
794
|
+
try {
|
|
795
|
+
const sig = repo.decode(addr)
|
|
796
|
+
chunk.signedFrom = sig.address
|
|
797
|
+
chunk.signedTo = addr - code.length
|
|
798
|
+
} catch {}
|
|
799
|
+
}
|
|
800
|
+
chunks.unshift(chunk)
|
|
712
801
|
addr -= code.length
|
|
713
802
|
}
|
|
714
803
|
if (!chunks.length) return null
|
|
@@ -744,6 +833,26 @@ function byteStreamSection (repo, keyHex, currentAddress) {
|
|
|
744
833
|
return item
|
|
745
834
|
})
|
|
746
835
|
const stripW = cursorX
|
|
836
|
+
// Inspector text — codec, address, length, percentage. Defaults to
|
|
837
|
+
// the at-view's current chunk (currentAddress) but follows the
|
|
838
|
+
// hovered chunk when the user is peeking at one on the strip. The
|
|
839
|
+
// slot re-renders on hover (via the live-preview path), so reading
|
|
840
|
+
// hoveredAddress in a nested slot below — reacts to hoverDep so its
|
|
841
|
+
// text updates without re-rendering the strip itself.
|
|
842
|
+
// Map byte address → strip x. Used by the sig-coverage overlay so hover
|
|
843
|
+
// anywhere on the page can light up "what bytes does this sig sign".
|
|
844
|
+
// Stored as data attrs on the strip container so the hover handler
|
|
845
|
+
// can read without recomputing.
|
|
846
|
+
const xForByte = (byteAddr) => {
|
|
847
|
+
// Find the chunk containing this byte and interpolate within it.
|
|
848
|
+
for (const c of layout) {
|
|
849
|
+
if (byteAddr >= c.start && byteAddr <= c.address) {
|
|
850
|
+
const frac = c.length === 1 ? 0 : (byteAddr - c.start) / (c.length - 1)
|
|
851
|
+
return c.x + frac * c.w
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
return 0
|
|
855
|
+
}
|
|
747
856
|
return h`
|
|
748
857
|
<h3>byte stream <span class="dim">(${total} bytes · ${chunks.length} chunks)</span></h3>
|
|
749
858
|
<div class="byte-map-legend">
|
|
@@ -756,21 +865,161 @@ function byteStreamSection (repo, keyHex, currentAddress) {
|
|
|
756
865
|
<span class="cat-num">num</span>
|
|
757
866
|
<span class="cat-var">var</span>
|
|
758
867
|
</div>
|
|
759
|
-
<div class="byte-strip-container" data-key=${`strip-${keyHex}`}>
|
|
868
|
+
<div class="byte-strip-container" data-key=${`strip-${keyHex}`} data-strip-w=${stripW}>
|
|
760
869
|
<svg class="byte-map byte-strip" width=${stripW} height=${H} viewBox=${`0 0 ${stripW} ${H}`}>
|
|
761
870
|
${layout.map(c => {
|
|
762
871
|
const cat = commitAddrs.has(c.address) ? 'commit' : codecCategory(c.codecType)
|
|
763
872
|
const cls = ['chunk', `cat-${cat}`, c.address === currentAddress ? 'current' : null]
|
|
873
|
+
// Sigs carry their coverage range in data-attrs so hover handlers
|
|
874
|
+
// (anywhere on the page) can position the coverage overlay.
|
|
875
|
+
// Non-sigs get null which removes the attrs.
|
|
876
|
+
const sigFromX = c.signedFrom != null ? xForByte(c.signedFrom) : null
|
|
877
|
+
const sigToX = c.signedTo != null ? xForByte(c.signedTo) : null
|
|
764
878
|
return h`<rect
|
|
765
879
|
class=${cls}
|
|
766
880
|
x=${c.x} y="0" width=${c.w} height=${H}
|
|
767
881
|
data-action="open-at"
|
|
768
882
|
data-keyhex=${keyHex}
|
|
769
883
|
data-addr=${c.address}
|
|
884
|
+
data-codec=${c.codecType}
|
|
885
|
+
data-len=${c.length}
|
|
886
|
+
data-sig-from-x=${sigFromX}
|
|
887
|
+
data-sig-to-x=${sigToX}
|
|
770
888
|
><title>${c.codecType} @${c.address} (${c.length} bytes)</title></rect>`
|
|
771
889
|
})}
|
|
890
|
+
<rect class="sig-coverage" x="0" y="0" width="0" height=${H} pointer-events="none"/>
|
|
772
891
|
</svg>
|
|
773
892
|
</div>
|
|
893
|
+
${() => {
|
|
894
|
+
// Inspector slot — reads hoverDep so it updates as the user moves
|
|
895
|
+
// across the strip, but doesn't re-render the strip itself. layout,
|
|
896
|
+
// total, currentAddress are closed over from byteStreamSection's
|
|
897
|
+
// call (which only runs on bridge fires; chunk content is fixed
|
|
898
|
+
// per render). isPeekActive lights up the .active background only
|
|
899
|
+
// when the inspector is showing something other than the URL's
|
|
900
|
+
// chunk.
|
|
901
|
+
hoverDep()
|
|
902
|
+
const inspectorAddr = hoveredAddress != null && hoveredAddress < total
|
|
903
|
+
? hoveredAddress
|
|
904
|
+
: currentAddress
|
|
905
|
+
const inspectorChunk = layout.find(c => c.address === inspectorAddr)
|
|
906
|
+
const inspectorText = inspectorChunk
|
|
907
|
+
? `${inspectorChunk.codecType} · @${inspectorChunk.address} · ${inspectorChunk.length} bytes${total > 0 ? ` (${((inspectorChunk.length / total) * 100).toFixed(2)}% of ${total})` : ''}`
|
|
908
|
+
: `${chunks.length} chunks · ${total} bytes`
|
|
909
|
+
const isPeekActive = hoveredAddress != null && hoveredAddress !== currentAddress
|
|
910
|
+
return h`<div class=${['chunk-inspector', isPeekActive ? 'active' : null]}
|
|
911
|
+
data-key=${`inspector-${keyHex}`}>${inspectorText}</div>`
|
|
912
|
+
}}
|
|
913
|
+
`
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Commit reachability — the *semantic* parent of every chunk. Commits
|
|
917
|
+
// don't reference their data tree via asRefs; they store the address
|
|
918
|
+
// as a number value (a FLOAT64), and the convention "follow this
|
|
919
|
+
// number to find your data" is implicit. So the structural referrer
|
|
920
|
+
// index doesn't connect chunks to their owning commits. This walk
|
|
921
|
+
// makes the connection: for each commit, BFS from commit.dataAddress
|
|
922
|
+
// through asRefs and mark every reachable chunk. Result: chunk
|
|
923
|
+
// address → set of commit addresses whose dataAddress reach it.
|
|
924
|
+
function buildCommitReachabilityIndex (repo) {
|
|
925
|
+
const reach = new Map() // chunk addr → Set<commit addr>
|
|
926
|
+
let walk = repo.byteLength - 1
|
|
927
|
+
while (walk >= 0) {
|
|
928
|
+
const code = repo.resolve(walk)
|
|
929
|
+
if (!code || !code.length) break
|
|
930
|
+
const type = repo.footerToCodec[code.at(-1)]?.type
|
|
931
|
+
if (type === 'OBJECT') {
|
|
932
|
+
let value
|
|
933
|
+
try { value = repo.decode(walk) } catch {}
|
|
934
|
+
if (value && isCommitShape(value)) {
|
|
935
|
+
const visited = new Set()
|
|
936
|
+
const stack = [value.dataAddress]
|
|
937
|
+
while (stack.length) {
|
|
938
|
+
const a = stack.pop()
|
|
939
|
+
if (typeof a !== 'number' || visited.has(a)) continue
|
|
940
|
+
visited.add(a)
|
|
941
|
+
if (!reach.has(a)) reach.set(a, new Set())
|
|
942
|
+
reach.get(a).add(walk)
|
|
943
|
+
let refs
|
|
944
|
+
try { refs = repo.asRefs(a) } catch {}
|
|
945
|
+
if (Array.isArray(refs)) {
|
|
946
|
+
for (const c of refs) if (typeof c === 'number') stack.push(c)
|
|
947
|
+
} else if (refs && typeof refs === 'object' && !(refs instanceof Date) && !(refs instanceof Uint8Array)) {
|
|
948
|
+
if (Array.isArray(refs.v)) {
|
|
949
|
+
for (const c of refs.v) if (typeof c === 'number') stack.push(c)
|
|
950
|
+
} else {
|
|
951
|
+
for (const c of Object.values(refs)) if (typeof c === 'number') stack.push(c)
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
walk -= code.length
|
|
958
|
+
}
|
|
959
|
+
return reach
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// Storage-tab context: which commits reach this chunk. (Position info
|
|
963
|
+
// — byte range, percentage, codec — now lives in the persistent chunk
|
|
964
|
+
// inspector under the byte strip, so this section focuses on the
|
|
965
|
+
// "story" of the chunk's place in user data.) Uses the commit-
|
|
966
|
+
// reachability index above, falling back to the structural referrer
|
|
967
|
+
// BFS for chunks not reachable from any commit (typically sigs and
|
|
968
|
+
// other top-level chunks).
|
|
969
|
+
function chunkContextSection (repo, keyHex, address) {
|
|
970
|
+
const reach = buildCommitReachabilityIndex(repo)
|
|
971
|
+
const reachingCommits = reach.get(address)
|
|
972
|
+
let label = null
|
|
973
|
+
if (reachingCommits && reachingCommits.size) {
|
|
974
|
+
// Show the most recent commit (highest address) reaching this chunk,
|
|
975
|
+
// plus a count if there are more.
|
|
976
|
+
const sorted = [...reachingCommits].sort((a, b) => b - a)
|
|
977
|
+
const newest = sorted[0]
|
|
978
|
+
label = h`
|
|
979
|
+
<a class="addr-link" data-action="open-at" data-keyhex=${keyHex} data-addr=${newest}>@${newest}</a>
|
|
980
|
+
${sorted.length > 1
|
|
981
|
+
? h` <span class="dim">(and ${sorted.length - 1} earlier commit${sorted.length === 2 ? '' : 's'})</span>`
|
|
982
|
+
: null}
|
|
983
|
+
`
|
|
984
|
+
} else {
|
|
985
|
+
// Fall back to the structural BFS — catches sig chunks (referenced
|
|
986
|
+
// by nothing in the asRefs sense, but the user might want to know
|
|
987
|
+
// some commit they cover).
|
|
988
|
+
const index = buildReferrerIndex(repo)
|
|
989
|
+
const visited = new Set()
|
|
990
|
+
let frontier = [address]
|
|
991
|
+
let depth = 0
|
|
992
|
+
while (frontier.length && depth < 64 && !label) {
|
|
993
|
+
const next = []
|
|
994
|
+
for (const a of frontier) {
|
|
995
|
+
if (visited.has(a)) continue
|
|
996
|
+
visited.add(a)
|
|
997
|
+
const parents = index.get(a) ?? []
|
|
998
|
+
for (const p of parents) {
|
|
999
|
+
try {
|
|
1000
|
+
const decoded = repo.decode(p.address)
|
|
1001
|
+
if (isCommitShape(decoded)) {
|
|
1002
|
+
label = h`<a class="addr-link" data-action="open-at" data-keyhex=${keyHex} data-addr=${p.address}>@${p.address}</a> <span class="dim">(via structural ref)</span>`
|
|
1003
|
+
break
|
|
1004
|
+
}
|
|
1005
|
+
} catch {}
|
|
1006
|
+
next.push(p.address)
|
|
1007
|
+
}
|
|
1008
|
+
if (label) break
|
|
1009
|
+
}
|
|
1010
|
+
frontier = next
|
|
1011
|
+
depth++
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
if (!label) {
|
|
1015
|
+
label = h`<span class="dim">no commit references this chunk</span>`
|
|
1016
|
+
}
|
|
1017
|
+
return h`
|
|
1018
|
+
<h3>reachable from <span class="dim">user-meaningful commits</span></h3>
|
|
1019
|
+
<p class="dim" style="font-size: 0.85rem; margin-bottom: 0.4rem;">
|
|
1020
|
+
commits hold their data's location as a number-valued <code>dataAddress</code> field, so reachability isn't a structural ref — it's "starting from each commit's data, this chunk shows up."
|
|
1021
|
+
</p>
|
|
1022
|
+
<p>${label}</p>
|
|
774
1023
|
`
|
|
775
1024
|
}
|
|
776
1025
|
|
|
@@ -778,7 +1027,8 @@ function byteStreamSection (repo, keyHex, currentAddress) {
|
|
|
778
1027
|
// opposed to "referenced by", which is what points to this chunk). Walks
|
|
779
1028
|
// the codec's parts via repo.directReferences. Codec-by-codec — exposes
|
|
780
1029
|
// the storage chain so e.g. STRING → UINT8ARRAY → DUPLE → DUPLE → … → WORD
|
|
781
|
-
// is browsable one click at a time.
|
|
1030
|
+
// is browsable one click at a time. Codec column is color-coded to match
|
|
1031
|
+
// the byte-strip palette so the chain reads visually.
|
|
782
1032
|
function outgoingReferencesSection (repo, keyHex, address) {
|
|
783
1033
|
const refs = repo.directReferences(address)
|
|
784
1034
|
if (!refs.length) return null
|
|
@@ -797,7 +1047,7 @@ function outgoingReferencesSection (repo, keyHex, address) {
|
|
|
797
1047
|
return h`
|
|
798
1048
|
<tr data-key=${`out${i}@${childAddr}`} data-action="open-at"
|
|
799
1049
|
data-keyhex=${keyHex} data-addr=${childAddr}>
|
|
800
|
-
<td class
|
|
1050
|
+
<td class=${['mono', 'codec-tag', `codec-${codecCategory(codecType)}`]}>${codecType}</td>
|
|
801
1051
|
<td>${preview}</td>
|
|
802
1052
|
<td class="mono dim">@${childAddr}</td>
|
|
803
1053
|
</tr>
|
|
@@ -835,7 +1085,7 @@ function referrersSection (repo, keyHex, address) {
|
|
|
835
1085
|
return h`
|
|
836
1086
|
<tr data-key=${`r${r.address}`} data-action="open-at"
|
|
837
1087
|
data-keyhex=${keyHex} data-addr=${r.address}>
|
|
838
|
-
<td class
|
|
1088
|
+
<td class=${['mono', 'codec-tag', `codec-${codecCategory(r.codecType || '?')}`]}>${r.codecType || '?'}${r.count > 1 ? ` ×${r.count}` : ''}</td>
|
|
839
1089
|
<td>${preview}</td>
|
|
840
1090
|
<td class="mono dim">@${r.address}</td>
|
|
841
1091
|
</tr>
|
|
@@ -861,38 +1111,102 @@ function isDuple (v) {
|
|
|
861
1111
|
// count chips ({ N fields } / [ N elements ]) — depth-controlled
|
|
862
1112
|
// expansion is the next step in this thread (see THREADS.md).
|
|
863
1113
|
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>`
|
|
1114
|
+
if (v === null) return h`<span class="tv tv-null" title="NULL">null</span>`
|
|
1115
|
+
if (v === undefined) return h`<span class="tv tv-undefined" title="UNDEFINED">undefined</span>`
|
|
866
1116
|
if (typeof v === 'boolean') {
|
|
867
|
-
return h`<span class=${['tv', 'tv-bool', v ? 'tv-true' : 'tv-false']}>${v ? '✓' : '✗'} ${String(v)}</span>`
|
|
1117
|
+
return h`<span class=${['tv', 'tv-bool', v ? 'tv-true' : 'tv-false']} title=${v ? 'TRUE' : 'FALSE'}>${v ? '✓' : '✗'} ${String(v)}</span>`
|
|
868
1118
|
}
|
|
869
1119
|
if (typeof v === 'string') {
|
|
870
1120
|
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>`
|
|
1121
|
+
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
1122
|
}
|
|
873
1123
|
if (typeof v === 'number') {
|
|
874
|
-
|
|
1124
|
+
// UINT7 is the codec for non-negative integers < 128; everything else
|
|
1125
|
+
// routes through FLOAT64. Surfacing this distinction makes "why is
|
|
1126
|
+
// this 1 byte vs 9" tactile when you hover.
|
|
1127
|
+
const codec = (Number.isInteger(v) && v >= 0 && v < 128) ? 'UINT7' : 'FLOAT64'
|
|
1128
|
+
return h`<span class="tv tv-num" title=${codec}>${String(v)}</span>`
|
|
875
1129
|
}
|
|
876
1130
|
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>`
|
|
1131
|
+
return h`<span class="tv tv-date" title="DATE"><span class="tv-glyph">📅</span><time datetime=${v.toISOString()}>${v.toLocaleString()}</time></span>`
|
|
878
1132
|
}
|
|
879
1133
|
if (v instanceof Uint8Array) {
|
|
880
|
-
return h`<span class="tv tv-bytes">Uint8Array(${v.length})</span>`
|
|
1134
|
+
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
1135
|
}
|
|
882
1136
|
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>`
|
|
1137
|
+
if (depth > 1) return h`<span class="tv tv-duple" title="DUPLE">Duple(…)</span>`
|
|
1138
|
+
return h`<span class="tv tv-duple" title="DUPLE">Duple(${typedValue(v.v[0], depth + 1)}, ${typedValue(v.v[1], depth + 1)})</span>`
|
|
885
1139
|
}
|
|
886
1140
|
if (Array.isArray(v)) {
|
|
887
|
-
return h`<span class="tv tv-array">[ ${v.length} ${v.length === 1 ? 'element' : 'elements'} ]</span>`
|
|
1141
|
+
return h`<span class="tv tv-array" title=${v.length === 0 ? 'EMPTY_ARRAY' : 'ARRAY'}>[ ${v.length} ${v.length === 1 ? 'element' : 'elements'} ]</span>`
|
|
888
1142
|
}
|
|
889
1143
|
if (typeof v === 'object') {
|
|
890
1144
|
const n = Object.keys(v).length
|
|
891
|
-
return h`<span class="tv tv-object">{ ${n} ${n === 1 ? 'field' : 'fields'} }</span>`
|
|
1145
|
+
return h`<span class="tv tv-object" title=${n === 0 ? 'EMPTY_OBJECT' : 'OBJECT'}>{ ${n} ${n === 1 ? 'field' : 'fields'} }</span>`
|
|
892
1146
|
}
|
|
893
1147
|
return h`<span class="tv">${String(v)}</span>`
|
|
894
1148
|
}
|
|
895
1149
|
|
|
1150
|
+
// Recursive typed-value tree — like typedValue, but expands composites
|
|
1151
|
+
// inline up to `depth` levels deep. Beyond depth, composites render as
|
|
1152
|
+
// un-expanded chips. Click a chip to expand IN PLACE (forceExpanded);
|
|
1153
|
+
// click an expanded composite's opening bracket to collapse it back to
|
|
1154
|
+
// a chip (forceCollapsed). Force-expand and force-collapse override
|
|
1155
|
+
// the default depth-based decision.
|
|
1156
|
+
//
|
|
1157
|
+
// Default depth=3 covers `{ name, messages: [{text, at}, ...] }` —
|
|
1158
|
+
// outer object expanded, messages array expanded, message objects
|
|
1159
|
+
// expanded, and primitives like text/at render inline.
|
|
1160
|
+
const forceExpanded = new Set() // `${keyHex}:${address}` → user clicked chip
|
|
1161
|
+
const forceCollapsed = new Set() // `${keyHex}:${address}` → user clicked bracket
|
|
1162
|
+
|
|
1163
|
+
function valueTree (repo, keyHex, address, depth = 3) {
|
|
1164
|
+
let value, refs
|
|
1165
|
+
try {
|
|
1166
|
+
value = repo.decode(address)
|
|
1167
|
+
refs = repo.asRefs(address)
|
|
1168
|
+
} catch {
|
|
1169
|
+
return h`<span class="dim">(decode error @${address})</span>`
|
|
1170
|
+
}
|
|
1171
|
+
if (typeof value !== 'object' || value === null || value instanceof Date || value instanceof Uint8Array) {
|
|
1172
|
+
return typedValue(value)
|
|
1173
|
+
}
|
|
1174
|
+
const k = `${keyHex}:${address}`
|
|
1175
|
+
const userExpanded = forceExpanded.has(k)
|
|
1176
|
+
const userCollapsed = forceCollapsed.has(k)
|
|
1177
|
+
const expand = userExpanded || (!userCollapsed && depth > 0)
|
|
1178
|
+
if (!expand) {
|
|
1179
|
+
return h`<a class="tv-drill" data-action="expand-tree"
|
|
1180
|
+
data-keyhex=${keyHex} data-addr=${address}
|
|
1181
|
+
title="click to expand · drill via storage tab if you need a full at-view"
|
|
1182
|
+
>${typedValue(value)}</a>`
|
|
1183
|
+
}
|
|
1184
|
+
const isArray = Array.isArray(value)
|
|
1185
|
+
const entries = isArray
|
|
1186
|
+
? value.map((v, i) => [String(i), v, refs?.[i]])
|
|
1187
|
+
: Object.entries(value).map(([k, v]) => [k, v, refs?.[k]])
|
|
1188
|
+
if (entries.length === 0) {
|
|
1189
|
+
return h`<span class="tv ${isArray ? 'tv-array' : 'tv-object'}">${isArray ? '[ ]' : '{ }'}</span>`
|
|
1190
|
+
}
|
|
1191
|
+
return h`
|
|
1192
|
+
<div class="tv-tree ${isArray ? 'tv-tree-array' : 'tv-tree-object'}">
|
|
1193
|
+
<span class="tv-bracket clickable" data-action="collapse-tree"
|
|
1194
|
+
data-keyhex=${keyHex} data-addr=${address}
|
|
1195
|
+
title="click to collapse"
|
|
1196
|
+
>${isArray ? '[' : '{'}</span>
|
|
1197
|
+
${entries.map(([k, v, addr]) => h`
|
|
1198
|
+
<div class="tv-tree-row">
|
|
1199
|
+
<span class="tv-key">${k}:</span>
|
|
1200
|
+
${addr !== undefined
|
|
1201
|
+
? valueTree(repo, keyHex, addr, depth - 1)
|
|
1202
|
+
: typedValue(v)}
|
|
1203
|
+
</div>
|
|
1204
|
+
`)}
|
|
1205
|
+
<span class="tv-bracket">${isArray ? ']' : '}'}</span>
|
|
1206
|
+
</div>
|
|
1207
|
+
`
|
|
1208
|
+
}
|
|
1209
|
+
|
|
896
1210
|
function safeGet (f) { try { return f() } catch { return undefined } }
|
|
897
1211
|
|
|
898
1212
|
// Build a child→parents index for the entire repo in one pass, so we can
|
|
@@ -1027,17 +1341,24 @@ function hexDump (bytes, maxLen = 256) {
|
|
|
1027
1341
|
|
|
1028
1342
|
const appEl = document.getElementById('app')
|
|
1029
1343
|
|
|
1030
|
-
//
|
|
1031
|
-
//
|
|
1032
|
-
//
|
|
1033
|
-
//
|
|
1034
|
-
//
|
|
1035
|
-
//
|
|
1344
|
+
// Outer mount slot. Reads viewKindDep ONLY — re-runs on view.kind
|
|
1345
|
+
// or view.keyHex changes (registry ↔ at, or switching repos). It does
|
|
1346
|
+
// NOT re-run on address changes, chunk arrivals, tab clicks, or any
|
|
1347
|
+
// other bridge fire. That's the whole point of the decomposition:
|
|
1348
|
+
// keep the at-view's <section> (and the strip-container inside it)
|
|
1349
|
+
// alive across intra-repo navigation so click-to-navigate doesn't
|
|
1350
|
+
// rebuild the strip and reset its scrollLeft.
|
|
1351
|
+
//
|
|
1352
|
+
// Each view gets a data-keyed <section> so mount's matcher distinguishes
|
|
1353
|
+
// them — switching from registry to an at-view, or between repos, drops
|
|
1354
|
+
// the old section and fresh-mounts the new one. RegistryView and AtView
|
|
1355
|
+
// each do their own internal reactivity (inner slots reading dep() and
|
|
1356
|
+
// hoverDep()) for everything within a view.
|
|
1036
1357
|
mount(h`${() => {
|
|
1037
|
-
|
|
1358
|
+
viewKindDep()
|
|
1038
1359
|
switch (view.kind) {
|
|
1039
1360
|
case 'registry': return h`<section class="view" data-key="view-registry">${RegistryView()}</section>`
|
|
1040
|
-
case 'at': return h`<section class="view" data-key=${`view-at-${view.keyHex}
|
|
1361
|
+
case 'at': return h`<section class="view" data-key=${`view-at-${view.keyHex}`}>${AtView({ keyHex: view.keyHex })}</section>`
|
|
1041
1362
|
default: return h`<div class="empty">?</div>`
|
|
1042
1363
|
}
|
|
1043
1364
|
}}`, appEl, recaller)
|
|
@@ -1065,6 +1386,18 @@ appEl.addEventListener('click', e => {
|
|
|
1065
1386
|
el.closest('details.commit-selector')?.removeAttribute('open')
|
|
1066
1387
|
return go({ kind: 'at', keyHex: el.dataset.keyhex, address: +el.dataset.addr })
|
|
1067
1388
|
}
|
|
1389
|
+
case 'expand-tree': {
|
|
1390
|
+
const k = `${el.dataset.keyhex}:${el.dataset.addr}`
|
|
1391
|
+
forceExpanded.add(k)
|
|
1392
|
+
forceCollapsed.delete(k)
|
|
1393
|
+
return fire()
|
|
1394
|
+
}
|
|
1395
|
+
case 'collapse-tree': {
|
|
1396
|
+
const k = `${el.dataset.keyhex}:${el.dataset.addr}`
|
|
1397
|
+
forceCollapsed.add(k)
|
|
1398
|
+
forceExpanded.delete(k)
|
|
1399
|
+
return fire()
|
|
1400
|
+
}
|
|
1068
1401
|
}
|
|
1069
1402
|
})
|
|
1070
1403
|
|
|
@@ -1078,8 +1411,12 @@ function syncByteStrips () {
|
|
|
1078
1411
|
for (const container of appEl.querySelectorAll('.byte-strip-container')) {
|
|
1079
1412
|
const visible = container.clientWidth || 1
|
|
1080
1413
|
const atRight = container.scrollLeft + visible >= container.scrollWidth - 8
|
|
1081
|
-
|
|
1082
|
-
|
|
1414
|
+
// Auto-pin to HEAD only when the strip is freshly mounted (scroll
|
|
1415
|
+
// hasn't been touched yet, so scrollLeft === 0). For recycled strips
|
|
1416
|
+
// mount restores the previous scrollLeft, so this branch correctly
|
|
1417
|
+
// skips and the user's position is preserved. Live updates still
|
|
1418
|
+
// pin if the user is already at the right edge (atRight).
|
|
1419
|
+
if (container.scrollLeft === 0 || atRight) {
|
|
1083
1420
|
container.scrollLeft = container.scrollWidth
|
|
1084
1421
|
}
|
|
1085
1422
|
}
|
|
@@ -1126,11 +1463,11 @@ appEl.addEventListener('pointerup', endDrag)
|
|
|
1126
1463
|
appEl.addEventListener('pointercancel', endDrag)
|
|
1127
1464
|
|
|
1128
1465
|
// Cross-highlight: hovering any element with data-addr highlights the
|
|
1129
|
-
// matching chunk in the byte-map
|
|
1130
|
-
//
|
|
1131
|
-
//
|
|
1132
|
-
// the
|
|
1133
|
-
//
|
|
1466
|
+
// matching chunk in the byte-map, populates the chunk inspector below
|
|
1467
|
+
// the strip with codec/addr/length, and (if the hovered chunk is a
|
|
1468
|
+
// signature) lights up its covered byte range as an overlay band on
|
|
1469
|
+
// the strip. If the hover came from somewhere other than the strip
|
|
1470
|
+
// itself, smooth-scroll the matching chunk into view in the strip.
|
|
1134
1471
|
appEl.addEventListener('mouseover', e => {
|
|
1135
1472
|
const el = e.target.closest('[data-addr]')
|
|
1136
1473
|
if (!el) return
|
|
@@ -1144,10 +1481,46 @@ appEl.addEventListener('mouseover', e => {
|
|
|
1144
1481
|
}
|
|
1145
1482
|
})
|
|
1146
1483
|
}
|
|
1484
|
+
// Look up the chunk's data on the strip rect for sig-coverage overlay.
|
|
1485
|
+
const stripRect = matches[0]?.closest('.byte-strip-container') ? matches[0]
|
|
1486
|
+
: appEl.querySelector(`.byte-strip .chunk[data-addr="${addr}"]`)
|
|
1487
|
+
if (stripRect) {
|
|
1488
|
+
const fromX = stripRect.getAttribute('data-sig-from-x')
|
|
1489
|
+
const toX = stripRect.getAttribute('data-sig-to-x')
|
|
1490
|
+
if (fromX != null && toX != null) {
|
|
1491
|
+
const overlay = stripRect.closest('.byte-strip').querySelector('.sig-coverage')
|
|
1492
|
+
if (overlay) {
|
|
1493
|
+
overlay.setAttribute('x', fromX)
|
|
1494
|
+
overlay.setAttribute('width', String(parseFloat(toX) - parseFloat(fromX)))
|
|
1495
|
+
overlay.classList.add('active')
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
// Live preview: if the hovered chunk is on the byte strip, set
|
|
1500
|
+
// hoveredAddress so the page content below renders for that chunk.
|
|
1501
|
+
// Click to actually navigate. Only fire if the address changed —
|
|
1502
|
+
// moving within the same chunk shouldn't re-render.
|
|
1503
|
+
const onStrip = el.closest('.byte-strip-container')
|
|
1504
|
+
const newHover = onStrip ? +addr : null
|
|
1505
|
+
if (newHover !== hoveredAddress) {
|
|
1506
|
+
hoveredAddress = newHover
|
|
1507
|
+
hoverFire()
|
|
1508
|
+
}
|
|
1147
1509
|
})
|
|
1148
1510
|
appEl.addEventListener('mouseout', e => {
|
|
1149
1511
|
const el = e.target.closest('[data-addr]')
|
|
1150
1512
|
if (!el) return
|
|
1151
|
-
appEl.querySelectorAll('.byte-map .chunk.hovered')
|
|
1152
|
-
|
|
1513
|
+
appEl.querySelectorAll('.byte-map .chunk.hovered').forEach(c => c.classList.remove('hovered'))
|
|
1514
|
+
appEl.querySelectorAll('.sig-coverage.active').forEach(o => o.classList.remove('active'))
|
|
1515
|
+
// Clear hoveredAddress unless the cursor is moving to ANOTHER chunk
|
|
1516
|
+
// on the strip. The previous check ("still inside .byte-strip-container")
|
|
1517
|
+
// treated the direction labels and any blank-space as "still hovering,"
|
|
1518
|
+
// which left the page stuck on the previously hovered chunk's content.
|
|
1519
|
+
// Requiring .chunk[data-addr] specifically means moving off a chunk
|
|
1520
|
+
// anywhere — out of the strip OR to its non-chunk regions — reverts.
|
|
1521
|
+
const goingToChunk = e.relatedTarget?.closest?.('.byte-strip-container .chunk[data-addr]')
|
|
1522
|
+
if (!goingToChunk && hoveredAddress !== null) {
|
|
1523
|
+
hoveredAddress = null
|
|
1524
|
+
hoverFire()
|
|
1525
|
+
}
|
|
1153
1526
|
})
|