| |
color: var(--color-text-secondary);
|
| |
text-align: center;
|
| |
}
|
| + |
.reveal {
|
| + |
background: none;
|
| + |
border: none;
|
| + |
padding: 0;
|
| + |
cursor: pointer;
|
| + |
font: var(--txt-body-s-regular);
|
| + |
color: var(--color-text-secondary);
|
| + |
text-decoration: underline dotted;
|
| + |
}
|
| + |
.reveal:hover {
|
| + |
color: var(--color-text-primary);
|
| + |
}
|
| + |
.peer-list {
|
| + |
display: flex;
|
| + |
flex-direction: column;
|
| + |
gap: 0.375rem;
|
| + |
margin-top: 0.5rem;
|
| + |
padding: 0.5rem 0.75rem;
|
| + |
background-color: var(--color-surface-canvas);
|
| + |
border: 1px solid var(--color-border-subtle);
|
| + |
border-radius: var(--border-radius-sm);
|
| + |
}
|
| + |
.peer-row {
|
| + |
display: flex;
|
| + |
align-items: flex-start;
|
| + |
flex-wrap: wrap;
|
| + |
gap: 0.5rem;
|
| + |
font: var(--txt-body-s-regular);
|
| + |
}
|
| + |
.role-badge {
|
| + |
display: inline-flex;
|
| + |
align-items: center;
|
| + |
height: 1.25rem;
|
| + |
padding: 0 0.375rem;
|
| + |
border-radius: var(--border-radius-sm);
|
| + |
font: var(--txt-body-s-regular);
|
| + |
border: 1px solid var(--color-border-subtle);
|
| + |
color: var(--color-text-secondary);
|
| + |
}
|
| + |
.role-badge.author {
|
| + |
background-color: var(--color-feedback-success-bg);
|
| + |
border-color: var(--color-feedback-success-border);
|
| + |
color: var(--color-feedback-success-text);
|
| + |
}
|
| + |
.role-badge.delegate {
|
| + |
background-color: var(--color-feedback-warning-bg);
|
| + |
border-color: var(--color-feedback-warning-border);
|
| + |
color: var(--color-feedback-warning-text);
|
| + |
}
|
| + |
.peer-url-list {
|
| + |
margin: 0;
|
| + |
padding-left: 1rem;
|
| + |
width: 100%;
|
| + |
color: var(--color-text-secondary);
|
| + |
word-break: break-all;
|
| + |
}
|
| + |
.reason {
|
| + |
color: var(--color-text-primary);
|
| + |
margin-left: 0.25rem;
|
| + |
}
|
| + |
.redactions {
|
| + |
margin-top: 0.5rem;
|
| + |
padding: 0.5rem 0.75rem;
|
| + |
background-color: var(--color-feedback-error-bg);
|
| + |
border: 1px solid var(--color-feedback-error-border);
|
| + |
border-radius: var(--border-radius-sm);
|
| + |
color: var(--color-feedback-error-text);
|
| + |
}
|
| + |
.redactions-title {
|
| + |
font: var(--txt-body-s-semibold);
|
| + |
margin-bottom: 0.25rem;
|
| + |
}
|
| + |
.redaction-row {
|
| + |
display: flex;
|
| + |
flex-wrap: wrap;
|
| + |
align-items: center;
|
| + |
gap: 0.375rem;
|
| + |
font: var(--txt-body-s-regular);
|
| + |
}
|
| + |
.blur {
|
| + |
filter: blur(4px);
|
| + |
pointer-events: none;
|
| + |
user-select: none;
|
| + |
opacity: 0.6;
|
| + |
}
|
| |
</style>
|
| |
|
| |
<Layout>
|
| |
</div>
|
| |
|
| |
{#each release.artifacts as artifact (artifact.cid)}
|
| + |
{@const trusted = trustedRedactions(artifact)}
|
| |
<div class="artifact">
|
| - |
<div class="artifact-row">
|
| - |
<span class="name">{artifact.name}</span>
|
| - |
<span class="kind">[{artifact.kind}]</span>
|
| - |
{#if isAvailableLocally(artifact)}
|
| - |
<span class="pill available" title="Available in local store">
|
| - |
<Icon name="checkmark" />
|
| - |
Local
|
| - |
</span>
|
| - |
{/if}
|
| - |
<div class="actions">
|
| - |
<button
|
| - |
onclick={() => download(artifact)}
|
| - |
disabled={busy[artifact.cid] || artifact.locations.length === 0}
|
| - |
title={artifact.locations.length === 0
|
| - |
? "No locations to download from"
|
| - |
: "Download to disk"}>
|
| - |
Download
|
| - |
</button>
|
| - |
{#if isShared(artifact)}
|
| + |
{#if trusted.length > 0}
|
| + |
<div class="redactions">
|
| + |
<div class="redactions-title">
|
| + |
Redacted by {trusted.length === 1
|
| + |
? "the artifact author or a delegate"
|
| + |
: "delegates / the artifact author"}
|
| + |
</div>
|
| + |
{#each trusted as redaction (redaction.peer.did)}
|
| + |
<div class="redaction-row">
|
| + |
<NodeId {...authorForNodeId(redaction.peer)} />
|
| + |
<span
|
| + |
class="role-badge {peerRole(redaction.peer.did, artifact)}">
|
| + |
{peerRole(redaction.peer.did, artifact)}
|
| + |
</span>
|
| + |
<span class="reason">{redaction.reason}</span>
|
| + |
</div>
|
| + |
{/each}
|
| + |
</div>
|
| + |
{/if}
|
| + |
<div class:blur={trusted.length > 0}>
|
| + |
<div class="artifact-row">
|
| + |
<span class="name">{artifact.name}</span>
|
| + |
<span class="kind">[{artifact.kind}]</span>
|
| + |
{#if isAvailableLocally(artifact)}
|
| + |
<span class="pill available" title="Available in local store">
|
| + |
<Icon name="checkmark" />
|
| + |
Local
|
| + |
</span>
|
| + |
{/if}
|
| + |
<div class="actions">
|
| |
<button
|
| - |
onclick={() => unseed(artifact)}
|
| - |
disabled={busy[artifact.cid]}>
|
| - |
Unseed
|
| + |
onclick={() => download(artifact)}
|
| + |
disabled={busy[artifact.cid] ||
|
| + |
artifact.locations.length === 0}
|
| + |
title={artifact.locations.length === 0
|
| + |
? "No locations to download from"
|
| + |
: "Download to disk"}>
|
| + |
Download
|
| |
</button>
|
| - |
{:else}
|
| + |
{#if isShared(artifact)}
|
| + |
<button
|
| + |
onclick={() => unseed(artifact)}
|
| + |
disabled={busy[artifact.cid]}>
|
| + |
Unseed
|
| + |
</button>
|
| + |
{:else}
|
| + |
<button
|
| + |
onclick={() => seed(artifact)}
|
| + |
disabled={busy[artifact.cid]}>
|
| + |
Seed
|
| + |
</button>
|
| + |
{/if}
|
| |
<button
|
| - |
onclick={() => seed(artifact)}
|
| + |
onclick={() => attest(artifact)}
|
| |
disabled={busy[artifact.cid]}>
|
| - |
Seed
|
| + |
Attest
|
| |
</button>
|
| - |
{/if}
|
| + |
{#if canEditMetadata(artifact)}
|
| + |
<button
|
| + |
class="danger"
|
| + |
onclick={() => redact(artifact)}
|
| + |
disabled={busy[artifact.cid]}>
|
| + |
Redact
|
| + |
</button>
|
| + |
{/if}
|
| + |
</div>
|
| + |
</div>
|
| + |
<div style:margin-top="0.375rem">
|
| + |
<Id id={artifact.cid} clipboard={artifact.cid} shorten={false} />
|
| + |
</div>
|
| + |
<div class="meta-line">
|
| |
<button
|
| - |
onclick={() => attest(artifact)}
|
| - |
disabled={busy[artifact.cid]}>
|
| - |
Attest
|
| + |
class="reveal"
|
| + |
onclick={() =>
|
| + |
(revealAttestations[artifact.cid] =
|
| + |
!revealAttestations[artifact.cid])}
|
| + |
disabled={artifact.attestations.length === 0}>
|
| + |
{artifact.attestations.length} attestations
|
| |
</button>
|
| - |
{#if canEditMetadata(artifact)}
|
| - |
<button
|
| - |
class="danger"
|
| - |
onclick={() => redact(artifact)}
|
| - |
disabled={busy[artifact.cid]}>
|
| - |
Redact
|
| - |
</button>
|
| + |
<button
|
| + |
class="reveal"
|
| + |
onclick={() =>
|
| + |
(revealLocations[artifact.cid] =
|
| + |
!revealLocations[artifact.cid])}
|
| + |
disabled={artifact.locations.length === 0}>
|
| + |
{artifact.locations.length} locations
|
| + |
</button>
|
| + |
{#if artifact.redactions.length > trusted.length}
|
| + |
<span style:color="var(--color-feedback-error-text)">
|
| + |
{artifact.redactions.length - trusted.length} other redactions
|
| + |
</span>
|
| |
{/if}
|
| |
</div>
|
| - |
</div>
|
| - |
<div style:margin-top="0.375rem">
|
| - |
<Id id={artifact.cid} clipboard={artifact.cid} shorten={false} />
|
| - |
</div>
|
| - |
<div class="meta-line">
|
| - |
<span>{artifact.attestations.length} attestations</span>
|
| - |
<span>{artifact.locations.length} locations</span>
|
| - |
{#if artifact.redactions.length > 0}
|
| - |
<span style:color="var(--color-fill-error)">
|
| - |
{artifact.redactions.length} redactions
|
| - |
</span>
|
| + |
{#if revealAttestations[artifact.cid] && artifact.attestations.length > 0}
|
| + |
<div class="peer-list">
|
| + |
{#each artifact.attestations as att (att.did)}
|
| + |
<div class="peer-row">
|
| + |
<NodeId {...authorForNodeId(att)} />
|
| + |
{#if peerRole(att.did, artifact) !== "other"}
|
| + |
<span class="role-badge {peerRole(att.did, artifact)}">
|
| + |
{peerRole(att.did, artifact)}
|
| + |
</span>
|
| + |
{/if}
|
| + |
</div>
|
| + |
{/each}
|
| + |
</div>
|
| + |
{/if}
|
| + |
{#if revealLocations[artifact.cid] && artifact.locations.length > 0}
|
| + |
<div class="peer-list">
|
| + |
{#each artifact.locations as loc (loc.peer.did)}
|
| + |
<div class="peer-row">
|
| + |
<NodeId {...authorForNodeId(loc.peer)} />
|
| + |
{#if peerRole(loc.peer.did, artifact) !== "other"}
|
| + |
<span
|
| + |
class="role-badge {peerRole(loc.peer.did, artifact)}">
|
| + |
{peerRole(loc.peer.did, artifact)}
|
| + |
</span>
|
| + |
{/if}
|
| + |
<ul class="peer-url-list">
|
| + |
{#each loc.urls as url}
|
| + |
<li>{url}</li>
|
| + |
{/each}
|
| + |
</ul>
|
| + |
</div>
|
| + |
{/each}
|
| + |
</div>
|
| + |
{/if}
|
| + |
{#if artifact.redactions.length > trusted.length}
|
| + |
<div class="peer-list">
|
| + |
{#each artifact.redactions.filter(r => peerRole(r.peer.did, artifact) === "other") as redaction (redaction.peer.did)}
|
| + |
<div class="peer-row">
|
| + |
<NodeId {...authorForNodeId(redaction.peer)} />
|
| + |
<span class="reason">{redaction.reason}</span>
|
| + |
</div>
|
| + |
{/each}
|
| + |
</div>
|
| + |
{/if}
|
| + |
{#if artifact.metadata && Object.keys(artifact.metadata).length > 0}
|
| + |
<dl class="metadata">
|
| + |
{#each Object.entries(artifact.metadata) as [key, value] (key)}
|
| + |
<dt>{key}</dt>
|
| + |
<dd>
|
| + |
<span class="value">
|
| + |
{typeof value === "string"
|
| + |
? value
|
| + |
: JSON.stringify(value)}
|
| + |
</span>
|
| + |
{#if canEditMetadata(artifact)}
|
| + |
<button
|
| + |
class="remove-meta"
|
| + |
title="Remove"
|
| + |
onclick={() => removeMetadata(artifact, key)}
|
| + |
disabled={busy[artifact.cid]}>
|
| + |
×
|
| + |
</button>
|
| + |
{/if}
|
| + |
</dd>
|
| + |
{/each}
|
| + |
</dl>
|
| + |
{/if}
|
| + |
{#if canEditMetadata(artifact) && draft[artifact.cid]}
|
| + |
<form
|
| + |
class="add-meta"
|
| + |
onsubmit={e => {
|
| + |
e.preventDefault();
|
| + |
const d = draft[artifact.cid];
|
| + |
void setMetadata(artifact, d.key, d.value);
|
| + |
}}>
|
| + |
<input
|
| + |
type="text"
|
| + |
placeholder="key"
|
| + |
bind:value={draft[artifact.cid].key}
|
| + |
disabled={busy[artifact.cid]} />
|
| + |
<input
|
| + |
type="text"
|
| + |
placeholder="value (string or JSON)"
|
| + |
bind:value={draft[artifact.cid].value}
|
| + |
disabled={busy[artifact.cid]} />
|
| + |
<button type="submit" disabled={busy[artifact.cid]}>Add</button>
|
| + |
</form>
|
| + |
{/if}
|
| + |
{#if progress[artifact.cid]}
|
| + |
<div class="progress">{progressText(artifact.cid)}</div>
|
| |
{/if}
|
| |
</div>
|
| - |
{#if artifact.metadata && Object.keys(artifact.metadata).length > 0}
|
| - |
<dl class="metadata">
|
| - |
{#each Object.entries(artifact.metadata) as [key, value] (key)}
|
| - |
<dt>{key}</dt>
|
| - |
<dd>
|
| - |
<span class="value">
|
| - |
{typeof value === "string" ? value : JSON.stringify(value)}
|
| - |
</span>
|
| - |
{#if canEditMetadata(artifact)}
|
| - |
<button
|
| - |
class="remove-meta"
|
| - |
title="Remove"
|
| - |
onclick={() => removeMetadata(artifact, key)}
|
| - |
disabled={busy[artifact.cid]}>
|
| - |
×
|
| - |
</button>
|
| - |
{/if}
|
| - |
</dd>
|
| - |
{/each}
|
| - |
</dl>
|
| - |
{/if}
|
| - |
{#if canEditMetadata(artifact) && draft[artifact.cid]}
|
| - |
<form
|
| - |
class="add-meta"
|
| - |
onsubmit={e => {
|
| - |
e.preventDefault();
|
| - |
const d = draft[artifact.cid];
|
| - |
void setMetadata(artifact, d.key, d.value);
|
| - |
}}>
|
| - |
<input
|
| - |
type="text"
|
| - |
placeholder="key"
|
| - |
bind:value={draft[artifact.cid].key}
|
| - |
disabled={busy[artifact.cid]} />
|
| - |
<input
|
| - |
type="text"
|
| - |
placeholder="value (string or JSON)"
|
| - |
bind:value={draft[artifact.cid].value}
|
| - |
disabled={busy[artifact.cid]} />
|
| - |
<button type="submit" disabled={busy[artifact.cid]}>Add</button>
|
| - |
</form>
|
| - |
{/if}
|
| - |
{#if progress[artifact.cid]}
|
| - |
<div class="progress">{progressText(artifact.cid)}</div>
|
| - |
{/if}
|
| |
</div>
|
| |
{:else}
|
| |
<div class="empty">No artifacts in this release</div>
|