diff --git a/web/src/lib/components/profile/VouchButton.stories.svelte b/web/src/lib/components/profile/VouchButton.stories.svelte
new file mode 100644
index 00000000..fc5316f3
--- /dev/null
+++ b/web/src/lib/components/profile/VouchButton.stories.svelte
@@ -0,0 +1,162 @@
+
+
+
+
+ {#snippet template(args)}
+
+
+
+
+
+ {/snippet}
+
+
+
+
+ {#snippet template(args)}
+
+
+
+
+
+ {/snippet}
+
+
+
+
+ {#snippet template(args)}
+
+
+
+
+
+ {/snippet}
+
+
+
+
+ {#snippet template(args)}
+
+
+ {#each [{ label: "Bordered", bordered: true, insetShadow: true }, { label: "Borderless", bordered: false, insetShadow: true }] as look (look.label)}
+
+ {/each}
+
+
+ {/snippet}
+
+
+
+
+ {#snippet template(args)}
+
+
+
+
+
+ {/snippet}
+
+
+
+
+ {#snippet template(args)}
+
+
+
+
+
+ {/snippet}
+
+
+
+
+ {#snippet template(args)}
+
+
+
+
+
+ {/snippet}
+
+
+
+
+ {#snippet template(args)}
+
+
+
+
+
+ {/snippet}
+
+
+
+
+ {#snippet template(args)}
+
+
+
+
+ Nothing renders above — you can't vouch for yourself.
+
+
+
+ {/snippet}
+
diff --git a/web/src/lib/components/profile/VouchButton.svelte b/web/src/lib/components/profile/VouchButton.svelte
new file mode 100644
index 00000000..7c044a95
--- /dev/null
+++ b/web/src/lib/components/profile/VouchButton.svelte
@@ -0,0 +1,227 @@
+
+
+{#if !isSelf}
+ {#if !signedIn}
+
+ Vouch
+
+ {:else}
+
+ {trigger.text}
+
+
+
+
+
Vouch for {label}
+
+ {#if current}
+ You {current.kind === "vouch" ? "vouched for" : "denounced"}
+ {label}. You can change your decision below.
+ {:else}
+ Vouching builds a web-of-trust across Tangled. Vouch for users you have had positive
+ interactions with.
+
+ Read more
+
+ {/if}
+
+
+
+ {#if networkVouches.length > 0}
+
+ {/if}
+
+
+ {#each choices as option (option.value)}
+ {@const Icon = option.icon}
+ {@const active = choice === option.value}
+ (choice = option.value)}
+ disabled={save.loading}
+ aria-pressed={active}
+ class={[
+ "flex flex-col items-center gap-1 rounded-sm border p-2 transition-colors disabled:cursor-not-allowed disabled:opacity-60",
+ active
+ ? option.value === "vouch"
+ ? "border-border-success bg-background-success-subtle text-foreground-success"
+ : option.value === "denounce"
+ ? "border-border-danger bg-background-danger-subtle text-foreground-danger"
+ : "border-border-strong bg-background-subtle text-foreground-default"
+ : "border-border-default hover:bg-background-subtle"
+ ]}
+ >
+
+ {option.text}
+
+ {/each}
+
+
+ {#if choice !== "none"}
+
+ {/if}
+
+
+
+
+
+ Cancel
+
+ save.run()}
+ >
+ {current ? "Update" : "Save"}
+
+
+
+ {/if}
+{/if}
diff --git a/web/src/lib/components/profile/pages.ts b/web/src/lib/components/profile/pages.ts
index 526f5e4d..d8f09f3b 100644
--- a/web/src/lib/components/profile/pages.ts
+++ b/web/src/lib/components/profile/pages.ts
@@ -170,14 +170,14 @@ const resolveStars = async (
const enriched =
repoDids.length > 0
? await enrich<{ items: ListItem[] }>(ctx, {
- xrpc: "sh.tangled.repo.getReposByRepoDids",
- params: { dids: repoDids },
- enrich: [
- ...targetAll(starDescriptors(viewerDid), ["items[].value.repoDid"]),
- target(REPO_OWNER_DOCS, ["items[].uri"])
- ],
- ...(viewerDid ? { viewer: viewerDid } : {})
- })
+ xrpc: "sh.tangled.repo.getReposByRepoDids",
+ params: { dids: repoDids },
+ enrich: [
+ ...targetAll(starDescriptors(viewerDid), ["items[].value.repoDid"]),
+ target(REPO_OWNER_DOCS, ["items[].uri"])
+ ],
+ ...(viewerDid ? { viewer: viewerDid } : {})
+ })
: { output: { items: [] }, data: {} as Sidecar };
const reposByDid = new Map(
enriched.output.items.map((item) => [(item.value as RepoRecord).repoDid, item])
@@ -376,19 +376,19 @@ export const fetchPinned = async (
const [byDid, byUri] = await Promise.all([
dids.length > 0
? enrich<{ items: ListItem[] }>(ctx, {
- xrpc: "sh.tangled.repo.getReposByRepoDids",
- params: { dids },
- enrich: targetAll(descriptors, ["items[].value.repoDid"]),
- ...(viewerDid ? { viewer: viewerDid } : {})
- })
+ xrpc: "sh.tangled.repo.getReposByRepoDids",
+ params: { dids },
+ enrich: targetAll(descriptors, ["items[].value.repoDid"]),
+ ...(viewerDid ? { viewer: viewerDid } : {})
+ })
: empty,
uris.length > 0
? enrich<{ items: ListItem[] }>(ctx, {
- xrpc: "sh.tangled.repo.getRepos",
- params: { repos: uris },
- enrich: targetAll(descriptors, ["items[].value.repoDid"]),
- ...(viewerDid ? { viewer: viewerDid } : {})
- })
+ xrpc: "sh.tangled.repo.getRepos",
+ params: { repos: uris },
+ enrich: targetAll(descriptors, ["items[].value.repoDid"]),
+ ...(viewerDid ? { viewer: viewerDid } : {})
+ })
: empty
]);
const cards = new Map