Radish alpha
r
rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5
Radicle web interface
Radicle
Git
Replace repo sidebar with horizontal tab bar
Julien Donck committed 1 month ago
commit c6c3cc2807549af17012fecc4550b70b1c79c429
parent 61c4e4e
31 files changed +653 -895
modified src/App/MobileFooter.svelte
@@ -29,7 +29,7 @@
  <Link
    style="width: 100%; display: flex; align-items: center; justify-content: center;"
    route={{ resource: "nodes", params: undefined }}>
-
    <Icon name="logo" />
+
    <Icon name="seed" />
  </Link>

  <slot />
modified src/components/File.svelte
@@ -16,24 +16,20 @@
    display: flex;
    height: 3rem;
    align-items: center;
-
    padding: 0 0.5rem 0 1rem;
-
    border: 1px solid var(--color-border-subtle);
-
    border-top-left-radius: var(--border-radius-md);
-
    border-top-right-radius: var(--border-radius-md);
+
    padding: 0 1rem;
    background-color: var(--color-surface-canvas);
    z-index: 2;
  }

+
  .header-border {
+
    border-bottom: 1px solid var(--color-border-subtle);
+
  }
+

  .sticky {
    position: sticky;
    top: 0;
  }

-
  .collapsed {
-
    border-radius: var(--border-radius-md);
-
    border: 1px solid var(--color-border-subtle);
-
  }
-

  .left {
    display: flex;
    gap: 0.5rem;
@@ -51,27 +47,19 @@
  .container {
    position: relative;
    overflow-x: auto;
-
    border: 1px solid var(--color-border-subtle);
-
    border-top: 0;
-
    border-bottom-left-radius: var(--border-radius-md);
-
    border-bottom-right-radius: var(--border-radius-md);
  }
  @media (max-width: 719.98px) {
    .header {
-
      border-radius: 0;
-
      border-left: 0;
-
      border-right: 0;
      padding: 0 1rem 0 1rem;
    }
-
    .container {
-
      border-radius: 0;
-
      border-left: 0;
-
      border-right: 0;
-
    }
  }
</style>

-
<div bind:this={header} class="header" class:collapsed={!expanded} class:sticky>
+
<div
+
  bind:this={header}
+
  class="header"
+
  class:sticky
+
  class:header-border={expanded}>
  <div class="left">
    {#if collapsable}
      <ExpandButton
modified src/components/List.svelte
@@ -4,7 +4,6 @@

<style>
  .list {
-
    border-top: 1px solid var(--color-border-subtle);
    border-bottom: 1px solid var(--color-border-subtle);
  }
  .list-item:not(:last-child) {
modified src/components/RepoCard.svelte
@@ -110,7 +110,7 @@
    color: var(--color-text-on-brand);
  }

-
  @container (max-width: 20rem) {
+
  @container (max-width: 30rem) {
    .activity {
      display: none;
    }
modified src/views/repos/Changeset.svelte
@@ -58,6 +58,11 @@
    background-color: var(--color-surface-base);
    padding: 1rem;
  }
+
  .file {
+
    border: 1px solid var(--color-border-subtle);
+
    border-radius: var(--border-radius-md);
+
    overflow: clip;
+
  }
  .summary {
    font: var(--txt-body-m-regular);
  }
@@ -99,7 +104,7 @@
  <Observer let:filesVisibility let:observer>
    {#each diff.files as file}
      {@const path = "path" in file ? file.path : file.newPath}
-
      <div use:intersection={observer} id={"observer:" + path}>
+
      <div class="file" use:intersection={observer} id={"observer:" + path}>
        {#if "diff" in file}
          <FileDiff
            {repoId}
modified src/views/repos/Commit.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { BaseUrl, Commit, Repo, SeedingPolicy } from "@http-client";
+
  import type { BaseUrl, Commit, Repo } from "@http-client";

  import dompurify from "dompurify";
  import escape from "lodash/escape";
@@ -14,10 +14,8 @@
  import Layout from "./Layout.svelte";
  import Link from "@app/components/Link.svelte";
  import Separator from "./Separator.svelte";
-
  import Share from "./Share.svelte";

  export let baseUrl: BaseUrl;
-
  export let seedingPolicy: SeedingPolicy;
  export let commit: Commit;
  export let repo: Repo;
  export let nodeAvatarUrl: string | undefined;
@@ -70,7 +68,7 @@
  }
</style>

-
<Layout {nodeAvatarUrl} {seedingPolicy} {baseUrl} {repo}>
+
<Layout {nodeAvatarUrl} {baseUrl} {repo}>
  <svelte:fragment slot="breadcrumb">
    <Separator />
    <Link
@@ -117,7 +115,6 @@
                </Button>
              </a>
            {/if}
-
            <Share />
          </div>
        </span>
        <CommitAuthorship {header}>
modified src/views/repos/Header.svelte
@@ -5,20 +5,65 @@
<script lang="ts">
  import type { BaseUrl, Repo } from "@http-client";

-
  import Link from "@app/components/Link.svelte";
+
  import config from "@app/lib/config";
+
  import debounce from "lodash/debounce";
+
  import { routeToPath } from "@app/lib/router";
+
  import { toClipboard } from "@app/lib/utils";
+

  import Button from "@app/components/Button.svelte";
  import Icon from "@app/components/Icon.svelte";
+
  import Link from "@app/components/Link.svelte";
+
  import SeedButton from "@app/views/repos/Header/SeedButton.svelte";

  export let baseUrl: BaseUrl;
  export let activeTab: ActiveTab = undefined;
  export let repo: Repo;
+

+
  let shareIcon: "link" | "checkmark" = "link";
+

+
  const restoreIcon = debounce(() => {
+
    shareIcon = "link";
+
  }, 1000);
+

+
  function tabRoute(): string {
+
    if (activeTab === "issues") {
+
      return routeToPath({
+
        resource: "repo.issues",
+
        repo: repo.rid,
+
        node: baseUrl,
+
      });
+
    } else if (activeTab === "patches") {
+
      return routeToPath({
+
        resource: "repo.patches",
+
        repo: repo.rid,
+
        node: baseUrl,
+
      });
+
    } else {
+
      return routeToPath({
+
        resource: "repo.source",
+
        repo: repo.rid,
+
        node: baseUrl,
+
        path: "/",
+
      });
+
    }
+
  }
+

+
  async function copyLink() {
+
    const origin = new URL(config.nodes.fallbackPublicExplorer).origin;
+
    await toClipboard(origin.concat(tabRoute()));
+
    shareIcon = "checkmark";
+
    restoreIcon();
+
  }
</script>

<style>
  .container {
    display: flex;
-
    flex-direction: column;
-
    gap: 0.5rem;
+
    flex-direction: row;
+
    align-items: center;
+
    gap: 0.25rem;
+
    padding: 1rem;
+
    border-bottom: 1px solid var(--color-border-subtle);
  }

  .counter {
@@ -41,8 +86,16 @@
  .title-counter {
    display: flex;
    gap: 0.5rem;
-
    justify-content: space-between;
-
    width: 100%;
+
  }
+

+
  .spacer {
+
    flex: 1;
+
  }
+

+
  .actions {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.5rem;
  }
</style>

@@ -54,11 +107,7 @@
      node: baseUrl,
      path: "/",
    }}>
-
    <Button
-
      size="large"
-
      styleWidth="100%"
-
      styleJustifyContent="flex-start"
-
      variant={activeTab === "source" ? "gray" : "background"}>
+
    <Button variant={activeTab === "source" ? "gray" : "background"}>
      <Icon name="chevron-left-right" />
      Source
    </Button>
@@ -69,12 +118,7 @@
      repo: repo.rid,
      node: baseUrl,
    }}>
-
    <Button
-
      let:hover
-
      size="large"
-
      styleJustifyContent="flex-start"
-
      styleWidth="100%"
-
      variant={activeTab === "issues" ? "gray" : "background"}>
+
    <Button let:hover variant={activeTab === "issues" ? "gray" : "background"}>
      <Icon name="issue" />
      <div class="title-counter">
        Issues
@@ -94,12 +138,7 @@
      repo: repo.rid,
      node: baseUrl,
    }}>
-
    <Button
-
      let:hover
-
      size="large"
-
      styleWidth="100%"
-
      styleJustifyContent="flex-start"
-
      variant={activeTab === "patches" ? "gray" : "background"}>
+
    <Button let:hover variant={activeTab === "patches" ? "gray" : "background"}>
      <Icon name="patch" />
      <div class="title-counter">
        Patches
@@ -112,4 +151,17 @@
      </div>
    </Button>
  </Link>
+

+
  <div class="spacer"></div>
+

+
  <div class="actions">
+
    {#if activeTab !== "issues" && activeTab !== "patches"}
+
      <Button variant="outline" size="regular" on:click={copyLink}>
+
        <Icon name={shareIcon} />
+
        <span class="global-hide-on-small-desktop-down">Copy link</span>
+
      </Button>
+
    {/if}
+
    <slot name="actions" />
+
    <SeedButton seedCount={repo.seeding} repoId={repo.rid} />
+
  </div>
</div>
modified src/views/repos/Header/CloneButton.svelte
@@ -11,11 +11,23 @@
  import Popover from "@app/components/Popover.svelte";
  import Radio from "@app/components/Radio.svelte";

+
  import { baseUrlToString } from "@app/lib/utils";
+

  export let baseUrl: BaseUrl;
  export let id: string;
  export let name: string;
  export let currentRefname: string;
-
  export let enabledArchiveDownload: boolean;
+

+
  let enabledArchiveDownload = false;
+

+
  void fetch(
+
    `${baseUrlToString(baseUrl)}/raw/${id}/archive/${currentRefname}`,
+
    {
+
      method: "HEAD",
+
    },
+
  ).then(response => {
+
    enabledArchiveDownload = response.ok;
+
  });

  let activeTab: "radicle" | "git" | "archive" = "radicle";

modified src/views/repos/Header/SeedButton.svelte
@@ -27,8 +27,8 @@
    padding: 0 0.25rem;
  }
  .not-seeding {
-
    background-color: var(--color-surface-brand-secondary);
-
    color: var(--color-text-on-brand);
+
    background-color: var(--color-surface-mid);
+
    color: var(--color-text-secondary);
  }
  .disabled {
    background-color: var(--color-surface-mid);
@@ -44,7 +44,7 @@
    on:click={() => {
      toggle();
    }}
-
    variant="secondary-toggle-off">
+
    variant="gray">
    <Icon name="seed" />
    <span class="title-counter">
      <span class="global-hide-on-mobile-down">Seed</span>
modified src/views/repos/History.svelte
@@ -15,6 +15,7 @@
  import { groupCommits } from "@app/lib/commit";

  import Button from "@app/components/Button.svelte";
+
  import CloneButton from "@app/views/repos/Header/CloneButton.svelte";
  import CommitTeaser from "./Commit/CommitTeaser.svelte";
  import ErrorMessage from "@app/components/ErrorMessage.svelte";
  import Header from "./Source/Header.svelte";
@@ -36,7 +37,21 @@
  export let tree: Tree;
  export let nodeAvatarUrl: string | undefined;

+
  $: currentRefname = formatQualifiedRefname(
+
    revision || repo.payloads["xyz.radicle.project"].data.defaultBranch,
+
    peer,
+
  );
+

  const api = new HttpdClient(baseUrl);
+
  let totalCommits: number | undefined = undefined;
+

+
  function fetchTotalCommits(rid: string, sha: string) {
+
    void api.repo.getTreeStatsBySha(rid, sha).then(stats => {
+
      totalCommits = stats.commits;
+
    });
+
  }
+

+
  $: fetchTotalCommits(repo.rid, commit);

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  let error: any;
@@ -80,18 +95,13 @@
    justify-content: center;
  }
  .group-header {
-
    margin-left: 1rem;
-
    margin-top: 3rem;
-
    margin-bottom: 1rem;
+
    margin: 1rem 0 0.5rem 1rem;
    font: var(--txt-body-m-regular);
    color: var(--color-text-tertiary);
  }
-
  .group-header:first-child {
-
    margin-top: 0;
-
  }
</style>

-
<Layout {nodeAvatarUrl} {seedingPolicy} {baseUrl} {repo} activeTab="source">
+
<Layout {nodeAvatarUrl} {baseUrl} {repo} activeTab="source">
  <svelte:fragment slot="breadcrumb">
    <Separator />
    <Link
@@ -103,16 +113,19 @@
      Commits
    </Link>
  </svelte:fragment>
-
  <RepoNameHeader
-
    {repo}
-
    currentRefname={formatQualifiedRefname(
-
      revision || repo.payloads["xyz.radicle.project"].data.defaultBranch,
-
      peer,
-
    )}
-
    {baseUrl}
-
    slot="header" />
-

-
  <div style:margin="1rem" slot="subheader">
+
  <svelte:fragment slot="actions">
+
    <CloneButton
+
      {baseUrl}
+
      {currentRefname}
+
      id={repo.rid}
+
      name={repo.payloads["xyz.radicle.project"].data.name} />
+
  </svelte:fragment>
+
  <RepoNameHeader {repo} {baseUrl} {seedingPolicy} slot="header" />
+

+
  <div
+
    style:padding="1rem"
+
    style:border-bottom="1px solid var(--color-border-subtle)"
+
    slot="subheader">
    <Header
      {baseRoute}
      {commit}
@@ -140,41 +153,24 @@
    {/each}
  </div>

-
  {#await api.repo.getTreeStatsBySha(repo.rid, commit)}
+
  {#if totalCommits === undefined || loading || allCommitHeaders.length < totalCommits}
    <div class="more">
-
      <Loading small center />
+
      {#if totalCommits === undefined || loading}
+
        <Loading small={page !== 0} center />
+
      {:else if allCommitHeaders.length < totalCommits}
+
        <Button size="large" variant="outline" on:click={loadMore}>More</Button>
+
      {/if}
    </div>
-
  {:then stats}
-
    {#if loading || allCommitHeaders.length < stats.commits}
-
      <div class="more">
-
        {#if loading}
-
          <Loading small={page !== 0} center />
-
        {:else if allCommitHeaders.length < stats.commits}
-
          <Button size="large" variant="outline" on:click={loadMore}>
-
            More
-
          </Button>
-
        {/if}
-
      </div>
-
    {/if}
-

-
    {#if error}
-
      <div class="message">
-
        <ErrorMessage
-
          title="Couldn't load commits"
-
          description="Make sure you are able to connect to the seed <code>{baseUrlToString(
-
            api.baseUrl,
-
          )}</code>"
-
          {error} />
-
      </div>
-
    {/if}
-
  {:catch error}
+
  {/if}
+

+
  {#if error}
    <div class="message">
      <ErrorMessage
-
        title="Couldn't load repo stats"
+
        title="Couldn't load commits"
        description="Make sure you are able to connect to the seed <code>{baseUrlToString(
          api.baseUrl,
        )}</code>"
        {error} />
    </div>
-
  {/await}
+
  {/if}
</Layout>
modified src/views/repos/Issue.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { BaseUrl, Issue, Repo, SeedingPolicy } from "@http-client";
+
  import type { BaseUrl, Issue, Repo } from "@http-client";

  import capitalize from "lodash/capitalize";
  import uniqBy from "lodash/uniqBy";
@@ -24,7 +24,6 @@
  import ThreadComponent from "@app/components/Thread.svelte";

  export let baseUrl: BaseUrl;
-
  export let seedingPolicy: SeedingPolicy;
  export let issue: Issue;
  export let repo: Repo;
  export let rawPath: (commit?: string) => string;
@@ -122,7 +121,6 @@
  {baseUrl}
  {nodeAvatarUrl}
  {repo}
-
  {seedingPolicy}
  activeTab="issues"
  stylePaddingBottom="0">
  <svelte:fragment slot="breadcrumb">
modified src/views/repos/Issues.svelte
@@ -1,21 +1,11 @@
<script lang="ts">
-
  import type {
-
    BaseUrl,
-
    Issue,
-
    IssueState,
-
    Repo,
-
    SeedingPolicy,
-
  } from "@http-client";
+
  import type { BaseUrl, Issue, IssueState, Repo } from "@http-client";

-
  import capitalize from "lodash/capitalize";
  import { HttpdClient } from "@http-client";
  import { ISSUES_PER_PAGE } from "./router";
  import { baseUrlToString } from "@app/lib/utils";
-
  import { closeFocused } from "@app/components/Popover.svelte";

  import Button from "@app/components/Button.svelte";
-
  import DropdownList from "@app/components/DropdownList.svelte";
-
  import DropdownListItem from "@app/components/DropdownList/DropdownListItem.svelte";
  import ErrorMessage from "@app/components/ErrorMessage.svelte";
  import Icon from "@app/components/Icon.svelte";
  import IssueTeaser from "@app/views/repos/Issue/IssueTeaser.svelte";
@@ -24,12 +14,9 @@
  import List from "@app/components/List.svelte";
  import Loading from "@app/components/Loading.svelte";
  import Placeholder from "@app/components/Placeholder.svelte";
-
  import Popover from "@app/components/Popover.svelte";
  import Separator from "./Separator.svelte";
-
  import Share from "./Share.svelte";

  export let baseUrl: BaseUrl;
-
  export let seedingPolicy: SeedingPolicy;
  export let issues: Issue[];
  export let repo: Repo;
  export let status: IssueState["status"];
@@ -65,17 +52,6 @@
    }
  }

-
  const stateOptions: IssueState["status"][] = ["open", "closed"];
-
  const stateColor: Record<IssueState["status"], string> = {
-
    open: "var(--color-text-open)",
-
    closed: "var(--color-text-merged)",
-
  };
-

-
  const stateBackground: Record<IssueState["status"], string> = {
-
    open: "var(--color-surface-open)",
-
    closed: "var(--color-surface-merged)",
-
  };
-

  $: showMoreButton =
    !loading &&
    !error &&
@@ -85,8 +61,9 @@
<style>
  .header {
    display: flex;
-
    justify-content: space-between;
+
    gap: 0.25rem;
    padding: 1rem;
+
    border-bottom: 1px solid var(--color-border-subtle);
  }
  .more {
    margin-top: 2rem;
@@ -95,21 +72,22 @@
    align-items: center;
    justify-content: center;
  }
-
  .dropdown-button-counter {
-
    border-radius: var(--border-radius-sm);
-
    background-color: var(--color-surface-alpha-subtle);
-
    color: var(--color-text-primary);
-
    padding: 0 0.25rem;
-
  }
-
  .dropdown-list-counter {
+
  .counter {
    border-radius: var(--border-radius-sm);
    background-color: var(--color-surface-mid);
    color: var(--color-text-tertiary);
    padding: 0 0.25rem;
+
    min-width: 1.5rem;
+
    text-align: center;
  }
  .selected {
    background-color: var(--color-surface-alpha-subtle);
-
    color: var(--color-text-tertiary);
+
    color: var(--color-text-primary);
+
  }
+
  .title-counter {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.5rem;
  }
  .placeholder {
    height: calc(100% - 4rem);
@@ -124,7 +102,7 @@
  }
</style>

-
<Layout {nodeAvatarUrl} {seedingPolicy} {baseUrl} {repo} activeTab="issues">
+
<Layout {nodeAvatarUrl} {baseUrl} {repo} activeTab="issues">
  <svelte:fragment slot="breadcrumb">
    <Separator />
    <Link
@@ -137,65 +115,40 @@
    </Link>
  </svelte:fragment>
  <div slot="header" class="header">
-
    <Popover
-
      popoverPadding="0"
-
      popoverPositionTop="2.5rem"
-
      popoverBorderRadius="var(--border-radius-md)">
-
      <Button
-
        let:expanded
-
        slot="toggle"
-
        let:toggle
-
        on:click={toggle}
-
        ariaLabel="filter-dropdown"
-
        title="Filter issues by state">
-
        <div
-
          style:color={stateColor[status]}
-
          style:background={stateBackground[status]}
-
          style:padding="0.25rem 0.25rem"
-
          style:border-radius="var(--border-radius-sm)">
-
          <Icon name={status === "closed" ? "issue-closed" : "issue"} />
+
    <Link
+
      route={{
+
        resource: "repo.issues",
+
        repo: repo.rid,
+
        node: baseUrl,
+
        status: "open",
+
      }}>
+
      <Button variant={status === "open" ? "gray" : "background"}>
+
        <Icon name="issue" />
+
        <div class="title-counter">
+
          Open
+
          <span class="counter" class:selected={status === "open"}>
+
            {repo.payloads["xyz.radicle.project"].meta.issues.open}
+
          </span>
        </div>
-
        {capitalize(status)}
-
        <div class="dropdown-button-counter">
-
          {repo.payloads["xyz.radicle.project"].meta.issues[status]}
+
      </Button>
+
    </Link>
+
    <Link
+
      route={{
+
        resource: "repo.issues",
+
        repo: repo.rid,
+
        node: baseUrl,
+
        status: "closed",
+
      }}>
+
      <Button variant={status === "closed" ? "gray" : "background"}>
+
        <Icon name="issue-closed" />
+
        <div class="title-counter">
+
          Closed
+
          <span class="counter" class:selected={status === "closed"}>
+
            {repo.payloads["xyz.radicle.project"].meta.issues.closed}
+
          </span>
        </div>
-
        <Icon name={expanded ? "chevron-up" : "chevron-down"} />
      </Button>
-

-
      <DropdownList slot="popover" items={stateOptions}>
-
        <Link
-
          on:afterNavigate={() => closeFocused()}
-
          slot="item"
-
          let:item
-
          route={{
-
            resource: "repo.issues",
-
            repo: repo.rid,
-
            node: baseUrl,
-
            status: item,
-
          }}>
-
          <DropdownListItem selected={item === status}>
-
            <div
-
              style:color={stateColor[item]}
-
              style:background={stateBackground[item]}
-
              style:padding="0.25rem 0.25rem"
-
              style:border-radius="var(--border-radius-sm)">
-
              <Icon name={item === "closed" ? "issue-closed" : "issue"} />
-
            </div>
-
            <div
-
              style="display: flex; gap: 1rem;justify-content: space-between; width: 100%;">
-
              {capitalize(item)}
-
              <div
-
                class="dropdown-list-counter"
-
                class:selected={item === status}>
-
                {repo.payloads["xyz.radicle.project"].meta.issues[item]}
-
              </div>
-
            </div>
-
          </DropdownListItem>
-
        </Link>
-
      </DropdownList>
-
    </Popover>
-

-
    <Share />
+
    </Link>
  </div>

  <List items={allIssues}>
modified src/views/repos/Layout.svelte
@@ -1,18 +1,17 @@
<script lang="ts">
  import type { ActiveTab } from "./Header.svelte";
-
  import type { BaseUrl, Repo, SeedingPolicy } from "@http-client";
+
  import type { BaseUrl, Repo } from "@http-client";

  import Button from "@app/components/Button.svelte";
  import Header from "@app/components/Header.svelte";
  import Icon from "@app/components/Icon.svelte";
  import Link from "@app/components/Link.svelte";
  import MobileFooter from "@app/App/MobileFooter.svelte";
+
  import RepoHeader from "./Header.svelte";
  import Separator from "./Separator.svelte";
-
  import Sidebar from "@app/views/repos/Sidebar.svelte";
  import UserAvatar from "@app/components/UserAvatar.svelte";

  export let activeTab: ActiveTab | undefined = undefined;
-
  export let seedingPolicy: SeedingPolicy;
  export let baseUrl: BaseUrl;
  export let repo: Repo;
  export let stylePaddingBottom: string = "2.5rem";
@@ -21,23 +20,18 @@

<style>
  .layout {
-
    display: grid;
-
    grid-template: auto 1fr auto / auto 1fr auto;
+
    display: flex;
+
    flex-direction: column;
    height: 100%;
  }

-
  .desktop-header {
-
    grid-column: 1 / 4;
-
  }
-

-
  .sidebar {
-
    grid-column: 1 / 2;
-
    border-right: 1px solid var(--color-border-subtle);
-
  }
-

  .content {
-
    grid-column: 2 / 3;
    overflow: scroll;
+
    flex: 1;
+
  }
+

+
  .tab-bar {
+
    display: block;
  }

  .mobile-footer {
@@ -68,6 +62,9 @@
    .desktop-header {
      display: none;
    }
+
    .tab-bar {
+
      display: none;
+
    }
    .content {
      overflow-y: scroll;
      overflow-x: hidden;
@@ -75,7 +72,6 @@
    .mobile-footer {
      margin-top: auto;
      display: grid;
-
      grid-column: 1 / 4;
    }
  }
</style>
@@ -133,12 +129,12 @@
    </Header>
  </div>

-
  <div class="sidebar global-hide-on-medium-desktop-down">
-
    <Sidebar {seedingPolicy} {activeTab} {baseUrl} {repo} />
-
  </div>
-

-
  <div class="sidebar global-hide-on-mobile-down global-hide-on-desktop-up">
-
    <Sidebar {seedingPolicy} {activeTab} {baseUrl} {repo} collapsedOnly />
+
  <div class="tab-bar">
+
    <RepoHeader {activeTab} {baseUrl} {repo}>
+
      <svelte:fragment slot="actions">
+
        <slot name="actions" />
+
      </svelte:fragment>
+
    </RepoHeader>
  </div>

  <div class="content" style:padding-bottom={stylePaddingBottom}>
modified src/views/repos/Patch.svelte
@@ -7,7 +7,6 @@
    Repo,
    Revision,
    Diff,
-
    SeedingPolicy,
  } from "@http-client";

  interface Thread {
@@ -71,7 +70,7 @@
  import Markdown from "@app/components/Markdown.svelte";
  import NodeId from "@app/components/NodeId.svelte";
  import Placeholder from "@app/components/Placeholder.svelte";
-
  import Radio from "@app/components/Radio.svelte";
+

  import Reactions from "@app/components/Reactions.svelte";
  import Reviews from "@app/views/repos/Cob/Reviews.svelte";
  import RevisionComponent from "@app/views/repos/Cob/Revision.svelte";
@@ -80,7 +79,6 @@
  import Share from "@app/views/repos/Share.svelte";

  export let baseUrl: BaseUrl;
-
  export let seedingPolicy: SeedingPolicy;
  export let patch: Patch;
  export let stats: Diff["stats"];
  export let rawPath: (commit?: string) => string;
@@ -267,18 +265,14 @@
    height: 100%;
  }
  .tabs {
-
    font: var(--txt-body-s-regular);
    display: flex;
    align-items: center;
-
    justify-content: left;
+
    gap: 0.25rem;
    flex-wrap: wrap;
-
    position: relative;
-
    margin-top: 1rem;
-
    box-shadow: inset 0 -1px 0 var(--color-border-subtle);
-
  }
-
  .tabs-spacer {
-
    width: 1rem;
-
    height: 100%;
+
    padding: 1rem;
+
    border-top: 1px solid var(--color-border-subtle);
+
    border-bottom: 1px solid var(--color-border-subtle);
+
    background-color: var(--color-surface-base);
  }
  .author-metadata {
    color: var(--color-text-tertiary);
@@ -304,7 +298,6 @@
  {baseUrl}
  {repo}
  {nodeAvatarUrl}
-
  {seedingPolicy}
  activeTab="patches"
  stylePaddingBottom="0">
  <svelte:fragment slot="breadcrumb">
@@ -419,42 +412,31 @@
      </CobHeader>

      <div class="tabs">
-
        <div class="tabs-spacer"></div>
-
        <Radio styleGap="0.375rem">
-
          {#each Object.entries(tabs) as [name, { route, icon }]}
-
            <Link {route}>
-
              <Button
-
                size="large"
-
                variant={name === view.name ||
-
                (view.name === "diff" && name === "changes")
-
                  ? "tab-active"
-
                  : "tab"}>
-
                <Icon name={icon} />
-
                {capitalize(name)}
-
              </Button>
-
            </Link>
-
          {/each}
-
        </Radio>
+
        {#each Object.entries(tabs) as [name, { route, icon }]}
+
          <Link {route}>
+
            <Button
+
              variant={name === view.name ||
+
              (view.name === "diff" && name === "changes")
+
                ? "gray"
+
                : "background"}>
+
              <Icon name={icon} />
+
              {capitalize(name)}
+
            </Button>
+
          </Link>
+
        {/each}

        {#if view.name === "changes"}
-
          <div
-
            class="global-hide-on-mobile-down"
-
            style="margin-left: auto; margin-top: -0.5rem;">
+
          <div class="global-hide-on-mobile-down" style="margin-left: auto;">
            <RevisionSelector {view} {baseUrl} {patch} {repo} />
          </div>
        {/if}
        {#if view.name === "diff"}
-
          <div
-
            class="global-hide-on-mobile-down"
-
            style="margin-left: auto; margin-top: -0.5rem;">
-
            <div style:margin-left="auto">
-
              <CompareButton
-
                fromCommit={view.fromCommit}
-
                toCommit={view.toCommit} />
-
            </div>
+
          <div class="global-hide-on-mobile-down" style="margin-left: auto;">
+
            <CompareButton
+
              fromCommit={view.fromCommit}
+
              toCommit={view.toCommit} />
          </div>
        {/if}
-
        <div class="tabs-spacer"></div>
      </div>
      <div class="bottom">
        {#if view.name === "changes"}
modified src/views/repos/Patches.svelte
@@ -1,21 +1,12 @@
<script lang="ts">
-
  import type {
-
    BaseUrl,
-
    Patch,
-
    PatchState,
-
    Repo,
-
    SeedingPolicy,
-
  } from "@http-client";
+
  import type { BaseUrl, Patch, PatchState, Repo } from "@http-client";

  import { HttpdClient } from "@http-client";
-
  import capitalize from "lodash/capitalize";

  import { PATCHES_PER_PAGE } from "./router";
  import { baseUrlToString } from "@app/lib/utils";

  import Button from "@app/components/Button.svelte";
-
  import DropdownList from "@app/components/DropdownList.svelte";
-
  import DropdownListItem from "@app/components/DropdownList/DropdownListItem.svelte";
  import ErrorMessage from "@app/components/ErrorMessage.svelte";
  import Icon from "@app/components/Icon.svelte";
  import Layout from "./Layout.svelte";
@@ -24,12 +15,9 @@
  import Loading from "@app/components/Loading.svelte";
  import PatchTeaser from "./Patch/PatchTeaser.svelte";
  import Placeholder from "@app/components/Placeholder.svelte";
-
  import Popover, { closeFocused } from "@app/components/Popover.svelte";
  import Separator from "./Separator.svelte";
-
  import Share from "./Share.svelte";

  export let baseUrl: BaseUrl;
-
  export let seedingPolicy: SeedingPolicy;
  export let patches: Patch[];
  export let repo: Repo;
  export let status: PatchState["status"];
@@ -65,27 +53,6 @@
    }
  }

-
  const stateOptions: PatchState["status"][] = [
-
    "draft",
-
    "open",
-
    "archived",
-
    "merged",
-
  ];
-

-
  const stateColor: Record<PatchState["status"], string> = {
-
    draft: "var(--color-text-draft)",
-
    open: "var(--color-text-open)",
-
    archived: "var(--color-text-archived)",
-
    merged: "var(--color-text-merged)",
-
  };
-

-
  const stateBackground: Record<PatchState["status"], string> = {
-
    draft: "var(--color-surface-draft)",
-
    open: "var(--color-surface-open)",
-
    archived: "var(--color-surface-archived)",
-
    merged: "var(--color-surface-merged)",
-
  };
-

  $: showMoreButton =
    !loading &&
    !error &&
@@ -96,8 +63,9 @@
<style>
  .header {
    display: flex;
-
    justify-content: space-between;
+
    gap: 0.25rem;
    padding: 1rem;
+
    border-bottom: 1px solid var(--color-border-subtle);
  }
  .more {
    margin-top: 2rem;
@@ -106,21 +74,22 @@
    align-items: center;
    justify-content: center;
  }
-
  .dropdown-button-counter {
-
    border-radius: var(--border-radius-sm);
-
    background-color: var(--color-surface-alpha-subtle);
-
    color: var(--color-text-primary);
-
    padding: 0 0.25rem;
-
  }
-
  .dropdown-list-counter {
+
  .counter {
    border-radius: var(--border-radius-sm);
    background-color: var(--color-surface-mid);
    color: var(--color-text-tertiary);
    padding: 0 0.25rem;
+
    min-width: 1.5rem;
+
    text-align: center;
  }
  .selected {
    background-color: var(--color-surface-alpha-subtle);
-
    color: var(--color-text-tertiary);
+
    color: var(--color-text-primary);
+
  }
+
  .title-counter {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.5rem;
  }
  .placeholder {
    height: calc(100% - 4rem);
@@ -132,10 +101,13 @@
    .placeholder {
      height: calc(100vh - 10rem);
    }
+
    .title-counter:not(.active) {
+
      display: none;
+
    }
  }
</style>

-
<Layout {nodeAvatarUrl} {seedingPolicy} {baseUrl} {repo} activeTab="patches">
+
<Layout {nodeAvatarUrl} {baseUrl} {repo} activeTab="patches">
  <svelte:fragment slot="breadcrumb">
    <Separator />
    <Link
@@ -148,78 +120,74 @@
    </Link>
  </svelte:fragment>
  <div slot="header" class="header">
-
    <Popover
-
      popoverPadding="0"
-
      popoverPositionTop="2.5rem"
-
      popoverBorderRadius="var(--border-radius-md)">
-
      <Button
-
        let:expanded
-
        slot="toggle"
-
        let:toggle
-
        on:click={toggle}
-
        ariaLabel="filter-dropdown"
-
        title="Filter patches by state">
-
        <div
-
          style:color={stateColor[status]}
-
          style:background={stateBackground[status]}
-
          style:padding="0.25rem 0.25rem"
-
          style:border-radius="var(--border-radius-sm)">
-
          <Icon
-
            name={status === "draft"
-
              ? "patch-draft"
-
              : status === "merged"
-
                ? "patch-merged"
-
                : status === "archived"
-
                  ? "patch-archived"
-
                  : "patch"} />
+
    <Link
+
      route={{
+
        resource: "repo.patches",
+
        repo: repo.rid,
+
        node: baseUrl,
+
        search: "status=open",
+
      }}>
+
      <Button variant={status === "open" ? "gray" : "background"}>
+
        <Icon name="patch" />
+
        <div class="title-counter" class:active={status === "open"}>
+
          Open
+
          <span class="counter" class:selected={status === "open"}>
+
            {repo.payloads["xyz.radicle.project"].meta.patches.open}
+
          </span>
+
        </div>
+
      </Button>
+
    </Link>
+
    <Link
+
      route={{
+
        resource: "repo.patches",
+
        repo: repo.rid,
+
        node: baseUrl,
+
        search: "status=draft",
+
      }}>
+
      <Button variant={status === "draft" ? "gray" : "background"}>
+
        <Icon name="patch-draft" />
+
        <div class="title-counter" class:active={status === "draft"}>
+
          Draft
+
          <span class="counter" class:selected={status === "draft"}>
+
            {repo.payloads["xyz.radicle.project"].meta.patches.draft}
+
          </span>
+
        </div>
+
      </Button>
+
    </Link>
+
    <Link
+
      route={{
+
        resource: "repo.patches",
+
        repo: repo.rid,
+
        node: baseUrl,
+
        search: "status=archived",
+
      }}>
+
      <Button variant={status === "archived" ? "gray" : "background"}>
+
        <Icon name="patch-archived" />
+
        <div class="title-counter" class:active={status === "archived"}>
+
          Archived
+
          <span class="counter" class:selected={status === "archived"}>
+
            {repo.payloads["xyz.radicle.project"].meta.patches.archived}
+
          </span>
        </div>
-
        {capitalize(status)}
-
        <div class="dropdown-button-counter">
-
          {repo.payloads["xyz.radicle.project"].meta.patches[status]}
+
      </Button>
+
    </Link>
+
    <Link
+
      route={{
+
        resource: "repo.patches",
+
        repo: repo.rid,
+
        node: baseUrl,
+
        search: "status=merged",
+
      }}>
+
      <Button variant={status === "merged" ? "gray" : "background"}>
+
        <Icon name="patch-merged" />
+
        <div class="title-counter" class:active={status === "merged"}>
+
          Merged
+
          <span class="counter" class:selected={status === "merged"}>
+
            {repo.payloads["xyz.radicle.project"].meta.patches.merged}
+
          </span>
        </div>
-
        <Icon name={expanded ? "chevron-up" : "chevron-down"} />
      </Button>
-
      <DropdownList slot="popover" items={stateOptions}>
-
        <Link
-
          slot="item"
-
          let:item
-
          on:afterNavigate={() => closeFocused()}
-
          route={{
-
            resource: "repo.patches",
-
            repo: repo.rid,
-
            node: baseUrl,
-
            search: `status=${item}`,
-
          }}>
-
          <DropdownListItem selected={item === status}>
-
            <div
-
              style:color={stateColor[item]}
-
              style:background={stateBackground[item]}
-
              style:padding="0.25rem 0.25rem"
-
              style:border-radius="var(--border-radius-sm)">
-
              <Icon
-
                name={item === "draft"
-
                  ? "patch-draft"
-
                  : item === "merged"
-
                    ? "patch-merged"
-
                    : item === "archived"
-
                      ? "patch-archived"
-
                      : "patch"} />
-
            </div>
-
            <div
-
              style="display: flex; gap: 1rem;justify-content: space-between; width: 100%;">
-
              {capitalize(item)}
-
              <div
-
                class="dropdown-list-counter"
-
                class:selected={item === status}>
-
                {repo.payloads["xyz.radicle.project"].meta.patches[item]}
-
              </div>
-
            </div>
-
          </DropdownListItem>
-
        </Link>
-
      </DropdownList>
-
    </Popover>
-

-
    <Share />
+
    </Link>
  </div>

  <List items={allPatches}>
deleted src/views/repos/Sidebar.svelte
@@ -1,267 +0,0 @@
-
<script lang="ts">
-
  import type { ActiveTab } from "./Header.svelte";
-
  import type { BaseUrl, Repo, SeedingPolicy } from "@http-client";
-

-
  import Button from "@app/components/Button.svelte";
-
  import ContextRepo from "@app/views/repos/Sidebar/ContextRepo.svelte";
-
  import Icon from "@app/components/Icon.svelte";
-
  import Link from "@app/components/Link.svelte";
-
  import Popover from "@app/components/Popover.svelte";
-

-
  const SIDEBAR_STATE_KEY = "sidebarState";
-

-
  export let activeTab: ActiveTab | undefined = undefined;
-
  export let seedingPolicy: SeedingPolicy;
-
  export let baseUrl: BaseUrl;
-
  export let repo: Repo;
-
  export let collapsedOnly = false;
-

-
  let expanded = collapsedOnly ? false : loadSidebarState();
-

-
  export function storeSidebarState(expanded: boolean): void {
-
    if (localStorage) {
-
      localStorage.setItem(
-
        SIDEBAR_STATE_KEY,
-
        expanded ? "expanded" : "collapsed",
-
      );
-
    } else {
-
      console.warn(
-
        "localStorage isn't available, not able to persist the sidebar state without it.",
-
      );
-
    }
-
  }
-

-
  function loadSidebarState(): boolean {
-
    const storedSidebarState = localStorage
-
      ? localStorage.getItem(SIDEBAR_STATE_KEY)
-
      : null;
-

-
    if (storedSidebarState === null) {
-
      return true;
-
    } else {
-
      return storedSidebarState === "expanded" ? true : false;
-
    }
-
  }
-

-
  function toggleSidebar() {
-
    expanded = !expanded;
-
    storeSidebarState(expanded);
-
  }
-
</script>
-

-
<style>
-
  .sidebar {
-
    padding: 1rem;
-
    height: 100%;
-
    display: flex;
-
    flex-direction: column;
-
    justify-content: space-between;
-
    transition: width 150ms ease-in-out;
-
    width: 4.5rem;
-
  }
-
  .sidebar.expanded {
-
    width: 22.5rem;
-
  }
-
  .repo-navigation {
-
    display: flex;
-
    flex-direction: column;
-
    gap: 0.25rem;
-
    flex: 1;
-
  }
-

-
  .counter {
-
    border-radius: var(--border-radius-sm);
-
    background-color: var(--color-surface-mid);
-
    color: var(--color-text-tertiary);
-
    padding: 0 0.25rem;
-
  }
-
  .selected {
-
    background-color: var(--color-surface-alpha-subtle);
-
    color: var(--color-text-primary);
-
  }
-
  .hover {
-
    background-color: var(--color-surface-strong);
-
    color: var(--color-text-primary);
-
  }
-
  .title-counter {
-
    display: flex;
-
    overflow: hidden;
-
    gap: 0.5rem;
-
    justify-content: space-between;
-
    width: 100%;
-
    opacity: 0;
-
    transition: opacity 150ms ease-in-out;
-
  }
-
  .title-counter.expanded {
-
    opacity: 1;
-
  }
-
  .sidebar-footer {
-
    display: flex;
-
    justify-content: space-between;
-
    width: 100%;
-
  }
-
  .repo {
-
    z-index: 10;
-
    opacity: 0;
-
    height: 0;
-
    overflow: hidden;
-
  }
-
  .box {
-
    padding: 1rem;
-
    margin-bottom: 0.5rem;
-
    background-color: var(--color-surface-subtle);
-
    border: 1px solid var(--color-border-subtle);
-
    font: var(--txt-body-m-regular);
-
    border-radius: var(--border-radius-md);
-
  }
-
  .repo.expanded {
-
    opacity: 1;
-
    height: initial;
-
    overflow: initial;
-
    transition: opacity 150ms;
-
    transition-delay: 150ms;
-
  }
-
  .vertical-buttons {
-
    opacity: 1;
-
    height: fit-content;
-
    display: flex;
-
    flex-direction: column-reverse;
-
    transition: opacity 150ms ease-in-out;
-
    transition-delay: 60ms;
-
    margin-bottom: 0.5rem;
-
  }
-
  .vertical-buttons.expanded {
-
    opacity: 0;
-
    height: 0;
-
    overflow: hidden;
-
  }
-
  .icon {
-
    transform: rotate(180deg);
-
    transition: transform 150ms ease-in-out;
-
  }
-
  .icon.expanded {
-
    transform: rotate(0deg);
-
  }
-
  .bottom {
-
    display: flex;
-
    flex-direction: column;
-
  }
-
</style>
-

-
<div class="sidebar" class:expanded>
-
  <!-- Top Navigation Items -->
-
  <div class="repo-navigation">
-
    <Link
-
      title="Source"
-
      route={{
-
        resource: "repo.source",
-
        repo: repo.rid,
-
        node: baseUrl,
-
        path: "/",
-
      }}>
-
      <Button
-
        stylePadding="0.5rem 0.75rem"
-
        size="large"
-
        styleWidth="100%"
-
        styleJustifyContent="flex-start"
-
        variant={activeTab === "source" ? "gray" : "background"}>
-
        <Icon name="chevron-left-right" />
-
        <span class="title-counter" class:expanded>Source</span>
-
      </Button>
-
    </Link>
-
    <Link
-
      title={`${repo.payloads["xyz.radicle.project"].meta.issues.open} Issues`}
-
      route={{
-
        resource: "repo.issues",
-
        repo: repo.rid,
-
        node: baseUrl,
-
      }}>
-
      <Button
-
        stylePadding="0.5rem 0.75rem"
-
        let:hover
-
        size="large"
-
        styleJustifyContent="flex-start"
-
        styleWidth="100%"
-
        variant={activeTab === "issues" ? "gray" : "background"}>
-
        <Icon name="issue" />
-
        <div class="title-counter" class:expanded>
-
          Issues
-
          <span
-
            class="counter"
-
            class:selected={activeTab === "issues"}
-
            class:hover={hover && activeTab !== "issues"}>
-
            {repo.payloads["xyz.radicle.project"].meta.issues.open}
-
          </span>
-
        </div>
-
      </Button>
-
    </Link>
-

-
    <Link
-
      title={`${repo.payloads["xyz.radicle.project"].meta.patches.open} Patches`}
-
      route={{
-
        resource: "repo.patches",
-
        repo: repo.rid,
-
        node: baseUrl,
-
      }}>
-
      <Button
-
        stylePadding="0.5rem 0.75rem"
-
        let:hover
-
        size="large"
-
        styleWidth="100%"
-
        styleJustifyContent="flex-start"
-
        variant={activeTab === "patches" ? "gray" : "background"}>
-
        <Icon name="patch" />
-
        <div class="title-counter" class:expanded>
-
          Patches
-
          <span
-
            class="counter"
-
            class:hover={hover && activeTab !== "patches"}
-
            class:selected={activeTab === "patches"}>
-
            {repo.payloads["xyz.radicle.project"].meta.patches.open}
-
          </span>
-
        </div>
-
      </Button>
-
    </Link>
-
  </div>
-
  <!-- Context and other information section -->
-
  <div class="bottom">
-
    <div class="repo box" class:expanded>
-
      <ContextRepo
-
        {baseUrl}
-
        repoThreshold={repo.threshold}
-
        repoDelegates={repo.delegates}
-
        {seedingPolicy} />
-
    </div>
-
    <div class="vertical-buttons" class:expanded style:gap="0.5rem">
-
      <Popover popoverPositionBottom="0" popoverPositionLeft="3rem">
-
        <Button
-
          stylePadding="0 0.75rem"
-
          variant="background"
-
          title="Info"
-
          slot="toggle"
-
          let:toggle
-
          on:click={toggle}>
-
          <Icon name="guide" />
-
        </Button>
-

-
        <div slot="popover" class="txt-body-m-regular" style:width="18rem">
-
          <ContextRepo
-
            {baseUrl}
-
            repoThreshold={repo.threshold}
-
            repoDelegates={repo.delegates}
-
            {seedingPolicy} />
-
        </div>
-
      </Popover>
-
    </div>
-
    <!-- Footer -->
-
    {#if !collapsedOnly}
-
      <div class="sidebar-footer" style:flex-direction="row">
-
        <Button title="Collapse" on:click={toggleSidebar} variant="background">
-
          <div class="icon" class:expanded>
-
            <Icon name="chevron-left" />
-
          </div>
-
        </Button>
-
      </div>
-
    {/if}
-
  </div>
-
</div>
modified src/views/repos/Sidebar/ContextRepo.svelte
@@ -2,53 +2,78 @@
  import type { BaseUrl, Repo, SeedingPolicy } from "@http-client";

  import capitalize from "lodash/capitalize";
-

-
  import IconButton from "@app/components/IconButton.svelte";
-
  import Icon from "@app/components/Icon.svelte";
+
  import HoverPopover from "@app/components/HoverPopover.svelte";
+
  import Link from "@app/components/Link.svelte";
  import NodeId from "@app/components/NodeId.svelte";
+
  import UserAvatar from "@app/components/UserAvatar.svelte";

  export let baseUrl: BaseUrl;
+

  export let repoThreshold: number;
  export let repoDelegates: Repo["delegates"];
  export let seedingPolicy: SeedingPolicy;
-

-
  let delegateExpanded = false;
-
  let policyExpanded = false;
</script>

<style>
-
  .item-header {
-
    gap: 2rem;
+
  .context-repo {
+
    display: flex;
+
    flex-direction: column;
+
    align-items: flex-start;
+
    gap: 0.5rem;
+
  }
+
  .row {
+
    display: flex;
+
    flex-direction: row;
+
    align-items: center;
+
    gap: 0.5rem;
+
  }
+
  .label {
+
    color: var(--color-text-tertiary);
+
  }
+
  .value {
+
    color: var(--color-text-primary);
+
  }
+
  .avatars {
    display: flex;
+
    flex-direction: row;
    align-items: center;
-
    justify-content: space-between;
-
    margin: 0.2rem 0;
+
    gap: 0.25rem;
  }
-
  .item-header:first-child {
-
    margin-top: 0;
+
  .avatars :global(.popover) {
+
    padding: 0.25rem 0.5rem;
  }
-
  .item-header:last-child {
-
    margin-bottom: 0;
+
  .avatar-popover {
+
    white-space: nowrap;
  }
-
  .nid {
-
    height: 21.5px;
-
    margin: 0.5rem 0;
+
  .description {
+
    font: var(--txt-body-s-medium);
+
    color: var(--color-text-quaternary);
  }
</style>

-
<div class="item-header">
-
  <span>Delegates</span>
-
  <div class="global-flex-item">
-
    <span class="txt-body-m-semibold">
+
<div class="context-repo">
+
  <div class="row">
+
    <span class="label txt-body-m-medium">Delegates</span>
+
    <span class="value txt-body-m-medium">
      {repoThreshold}/{repoDelegates.length}
    </span>
-
    <IconButton on:click={() => (delegateExpanded = !delegateExpanded)}>
-
      <Icon name={delegateExpanded ? "chevron-up" : "chevron-down"} />
-
    </IconButton>
+
    <div class="avatars">
+
      {#each repoDelegates as delegate}
+
        <HoverPopover>
+
          <Link
+
            slot="toggle"
+
            style="display: flex"
+
            route={{ resource: "users", did: delegate.id, baseUrl }}>
+
            <UserAvatar nodeId={delegate.id} styleWidth="1rem" />
+
          </Link>
+
          <div slot="popover" class="avatar-popover">
+
            <NodeId {baseUrl} nodeId={delegate.id} alias={delegate.alias} />
+
          </div>
+
        </HoverPopover>
+
      {/each}
+
    </div>
  </div>
-
</div>
-
{#if delegateExpanded}
-
  <div style:color="var(--color-text-tertiary)" style:margin-bottom="1rem">
+
  <div class="description">
    {#if repoDelegates.length === 1}
      Any changes accepted by the sole delegate will be included in the
      canonical branch.
@@ -57,29 +82,15 @@
      to be included in the canonical branch.
    {/if}
  </div>
-
  <div class="delegates">
-
    {#each repoDelegates as delegate}
-
      <div class="nid">
-
        <NodeId {baseUrl} nodeId={delegate.id} alias={delegate.alias} />
-
      </div>
-
    {/each}
-
  </div>
-
{/if}
-
<div class="item-header">
-
  <span style:text-wrap="nowrap">Seeding Scope</span>
-
  <div class="global-flex-item">
-
    <span class="txt-body-m-semibold">
+
  <div class="row">
+
    <span class="label txt-body-m-medium">Seeding Scope</span>
+
    <span class="value txt-body-m-medium">
      {capitalize(
        "scope" in seedingPolicy ? seedingPolicy.scope : "not defined",
      )}
    </span>
-
    <IconButton on:click={() => (policyExpanded = !policyExpanded)}>
-
      <Icon name={policyExpanded ? "chevron-up" : "chevron-down"} />
-
    </IconButton>
  </div>
-
</div>
-
{#if policyExpanded}
-
  <div style:color="var(--color-text-tertiary)">
+
  <div class="description">
    {#if seedingPolicy.policy === "block"}
      Seeding scope only has an effect when a repository is seeded. This repo
      isn't seeded by the seed node.
@@ -89,4 +100,4 @@
      This repository tracks only peers followed by the seed node.
    {/if}
  </div>
-
{/if}
+
</div>
modified src/views/repos/Source.svelte
@@ -10,17 +10,18 @@

  import { HttpdClient } from "@http-client";

+
  import { formatQualifiedRefname } from "@app/lib/utils";
+

+
  import BlobComponent from "./Source/Blob.svelte";
  import Button from "@app/components/Button.svelte";
+
  import CloneButton from "@app/views/repos/Header/CloneButton.svelte";
+
  import FilePath from "@app/components/FilePath.svelte";
  import Header from "./Source/Header.svelte";
  import Layout from "./Layout.svelte";
  import Placeholder from "@app/components/Placeholder.svelte";
-

-
  import BlobComponent from "./Source/Blob.svelte";
-
  import FilePath from "@app/components/FilePath.svelte";
  import RepoNameHeader from "./Source/RepoNameHeader.svelte";
  import Separator from "./Separator.svelte";
  import TreeComponent from "./Source/Tree.svelte";
-
  import { formatQualifiedRefname } from "@app/lib/utils";

  export let baseUrl: BaseUrl;
  export let blobResult: BlobResult;
@@ -52,6 +53,11 @@
    });
  };

+
  $: currentRefname = formatQualifiedRefname(
+
    revision || repo.payloads["xyz.radicle.project"].data.defaultBranch,
+
    peer,
+
  );
+

  $: baseRoute = {
    resource: "repo.source",
    node: baseUrl,
@@ -68,13 +74,14 @@
  .container {
    display: flex;
    width: inherit;
-
    padding: 0 1rem 1rem 1rem;
+
    padding: 0;
  }

  .column-left {
    display: flex;
    flex-direction: column;
    padding-right: 0.5rem;
+
    border-right: 1px solid var(--color-border-subtle);
  }

  .column-right {
@@ -82,8 +89,6 @@
    flex-direction: column;
    width: 100%;
    padding-bottom: 2.5rem;
-
    max-width: 75rem;
-
    margin: 0 auto;
    /* To allow pre elements to shrink when overflowing */
    min-width: 0;
  }
@@ -122,7 +127,6 @@
  {baseUrl}
  {nodeAvatarUrl}
  {repo}
-
  {seedingPolicy}
  activeTab="source"
  stylePaddingBottom="0">
  <svelte:fragment slot="breadcrumb">
@@ -131,16 +135,19 @@
      <FilePath filenameWithPath={path} />
    {/if}
  </svelte:fragment>
-
  <RepoNameHeader
-
    {repo}
-
    currentRefname={formatQualifiedRefname(
-
      revision || repo.payloads["xyz.radicle.project"].data.defaultBranch,
-
      peer,
-
    )}
-
    {baseUrl}
-
    slot="header" />
-

-
  <div style:margin="1rem" slot="subheader">
+
  <svelte:fragment slot="actions">
+
    <CloneButton
+
      {baseUrl}
+
      {currentRefname}
+
      id={repo.rid}
+
      name={repo.payloads["xyz.radicle.project"].data.name} />
+
  </svelte:fragment>
+
  <RepoNameHeader {repo} {baseUrl} {seedingPolicy} slot="header" />
+

+
  <div
+
    style:padding="1rem"
+
    style:border-bottom="1px solid var(--color-border-subtle)"
+
    slot="subheader">
    <Header
      filesLinkActive={true}
      historyLinkActive={false}
modified src/views/repos/Source/Blob.svelte
@@ -142,6 +142,8 @@
  .markdown-wrapper {
    padding: 2rem;
    background-color: var(--color-surface-canvas);
+
    max-width: 75rem;
+
    margin: 0 auto;
  }
  @media (max-width: 719.98px) {
    .markdown-wrapper {
modified src/views/repos/Source/Header.svelte
@@ -1,3 +1,8 @@
+
<script lang="ts" context="module">
+
  // Cache commit counts across component remounts (tab navigation).
+
  const commitCountCache: Record<string, number> = {};
+
</script>
+

<script lang="ts">
  import type { RepoRoute } from "../router";
  import type { BaseUrl, Repo, Remote, Tree } from "@http-client";
@@ -9,7 +14,7 @@
  import CommitButton from "../components/CommitButton.svelte";
  import Icon from "@app/components/Icon.svelte";
  import Link from "@app/components/Link.svelte";
-
  import Loading from "@app/components/Loading.svelte";
+

  import PeerBranchSelector from "./PeerBranchSelector.svelte";

  export let commit: string;
@@ -27,6 +32,22 @@
  export let tree: Tree;

  const api = new HttpdClient(node);
+
  let commitCount: number | undefined = commitCountCache[commit];
+

+
  function fetchCommitCount(rid: string, sha: string) {
+
    const cached = commitCountCache[sha];
+
    if (cached !== undefined) {
+
      commitCount = cached;
+
    } else {
+
      void api.repo.getTreeStatsBySha(rid, sha).then(stats => {
+
        commitCountCache[sha] = stats.commits;
+
        commitCount = stats.commits;
+
      });
+
    }
+
  }
+

+
  $: fetchCommitCount(repo.rid, commit);
+

  let selectedBranch: string | undefined;
  let commitButtonVariant: ComponentProps<CommitButton>["variant"] | undefined =
    undefined;
@@ -56,33 +77,24 @@
</script>

<style>
-
  .top-header {
+
  .header {
+
    font: var(--txt-body-s-regular);
    display: flex;
+
    gap: 0.375rem;
    align-items: center;
    justify-content: left;
-
    row-gap: 0.5rem;
-
    gap: 1px;
    flex-wrap: wrap;
-
    margin-bottom: 2rem;
  }
-

-
  .header {
-
    font: var(--txt-body-s-regular);
+
  .branch-commit {
    display: flex;
-
    gap: 0.375rem;
    align-items: center;
-
    justify-content: left;
    flex-wrap: wrap;
-
    position: relative;
  }
-
  .header::after {
-
    content: "";
-
    position: absolute;
-
    left: -1rem;
-
    bottom: 0;
-
    border-bottom: 1px solid var(--color-border-subtle);
-
    width: calc(100% + 1rem);
-
    z-index: -1;
+
  .mobile-branch {
+
    display: flex;
+
    align-items: center;
+
    flex-wrap: wrap;
+
    margin-bottom: 0.5rem;
  }

  .counter {
@@ -90,6 +102,8 @@
    background-color: var(--color-surface-mid);
    color: var(--color-text-tertiary);
    padding: 0 0.25rem;
+
    min-width: 1.5rem;
+
    text-align: center;
  }

  .title-counter {
@@ -104,7 +118,7 @@
  }
</style>

-
<div class="top-header">
+
<div class="mobile-branch global-hide-on-small-desktop-up" style:gap="1px">
  {#if selectedBranch}
    <PeerBranchSelector
      {peers}
@@ -114,25 +128,22 @@
      {repo}
      {selectedBranch} />
  {/if}
-
  <div class="global-flex-item txt-overflow" style:gap="1px">
-
    <CommitButton
-
      variant={commitButtonVariant}
-
      styleMinWidth="0"
-
      styleWidth="100%"
-
      hideSummaryOnMobile={false}
-
      repoId={repo.rid}
-
      commit={lastCommit}
-
      baseUrl={node} />
-
    {#if !onCanonical}
-
      <Link route={baseRoute}>
-
        <Button
-
          variant="not-selected"
-
          styleBorderRadius="0 var(--border-radius-sm) var(--border-radius-sm) 0">
-
          <Icon name="close" />
-
        </Button>
-
      </Link>
-
    {/if}
-
  </div>
+
  <CommitButton
+
    variant={commitButtonVariant}
+
    styleMinWidth="0"
+
    hideSummaryOnMobile
+
    repoId={repo.rid}
+
    commit={lastCommit}
+
    baseUrl={node} />
+
  {#if !onCanonical}
+
    <Link route={baseRoute}>
+
      <Button
+
        variant="not-selected"
+
        styleBorderRadius="0 var(--border-radius-sm) var(--border-radius-sm) 0">
+
        <Icon name="close" />
+
      </Button>
+
    </Link>
+
  {/if}
</div>

<div class="header">
@@ -145,7 +156,7 @@
        peer,
        revision,
      }}>
-
      <Button size="large" variant={filesLinkActive ? "tab-active" : "tab"}>
+
      <Button variant={filesLinkActive ? "gray" : "background"}>
        <Icon name="document" />Files
      </Button>
    </Link>
@@ -158,19 +169,45 @@
        peer,
        revision,
      }}>
-
      <Button size="large" variant={historyLinkActive ? "tab-active" : "tab"}>
+
      <Button variant={historyLinkActive ? "gray" : "background"}>
        <Icon name="commit" />
        <div class="title-counter">
          Commits
-
          {#await api.repo.getTreeStatsBySha(repo.rid, commit)}
-
            <Loading small center noDelay grayscale />
-
          {:then stats}
+
          {#if commitCount !== undefined}
            <div class="counter" class:selected={historyLinkActive}>
-
              {stats.commits}
+
              {commitCount}
            </div>
-
          {/await}
+
          {/if}
        </div>
      </Button>
    </Link>
  </div>
+

+
  <div class="branch-commit global-hide-on-mobile-down" style:gap="1px">
+
    {#if selectedBranch}
+
      <PeerBranchSelector
+
        {peers}
+
        {peer}
+
        {baseRoute}
+
        {onCanonical}
+
        {repo}
+
        {selectedBranch} />
+
    {/if}
+
    <CommitButton
+
      variant={commitButtonVariant}
+
      styleMinWidth="0"
+
      hideSummaryOnMobile
+
      repoId={repo.rid}
+
      commit={lastCommit}
+
      baseUrl={node} />
+
    {#if !onCanonical}
+
      <Link route={baseRoute}>
+
        <Button
+
          variant="not-selected"
+
          styleBorderRadius="0 var(--border-radius-sm) var(--border-radius-sm) 0">
+
          <Icon name="close" />
+
        </Button>
+
      </Link>
+
    {/if}
+
  </div>
</div>
modified src/views/repos/Source/PeerBranchSelector.svelte
@@ -180,7 +180,7 @@
              <DropdownListItem
                selected={selectedPeer?.id === peer?.id &&
                  selectedBranch === revision}
-
                style={`${subgridStyle} gap: inherit;`}>
+
                style={subgridStyle}>
                <div class="global-flex-item">
                  <Icon name="branch" />
                  <span class="txt-overflow">
@@ -231,7 +231,7 @@
            </Link>
          {:else}
            <div
-
              style="gap: inherit; padding: 0.5rem 0.375rem;"
+
              style="padding: 0.5rem 0.375rem;"
              class="subgrid-item txt-body-m-regular"
              style:color="var(--color-text-tertiary)">
              No entries found
@@ -245,9 +245,7 @@
              searchInput = "";
              toggle();
            }}>
-
            <DropdownListItem
-
              selected={onCanonical}
-
              style={`${subgridStyle} gap: inherit;`}>
+
            <DropdownListItem selected={onCanonical} style={subgridStyle}>
              <div class="global-flex-item">
                <Icon name="branch" />
                {repo.payloads["xyz.radicle.project"].data.defaultBranch}
modified src/views/repos/Source/PeerBranchSelector/Peer.svelte
@@ -68,7 +68,7 @@
            peer: peer.remote.id,
            revision: name,
          })}
-
        style={`${subgridStyle} padding-left: 2.3rem; gap: inherit;`}>
+
        style={`${subgridStyle} padding-left: 2.3rem;`}>
        <div class="global-flex-item">
          <Icon name="branch" />
          <span class="txt-overflow">
modified src/views/repos/Source/RepoNameHeader.svelte
@@ -1,32 +1,20 @@
<script lang="ts">
-
  import type { BaseUrl, Repo } from "@http-client";
+
  import type { BaseUrl, Repo, SeedingPolicy } from "@http-client";

  import dompurify from "dompurify";
  import { markdown } from "@app/lib/markdown";
-
  import { baseUrlToString, twemoji } from "@app/lib/utils";
+
  import { formatRepositoryId, twemoji } from "@app/lib/utils";

  import Badge from "@app/components/Badge.svelte";
-
  import CloneButton from "@app/views/repos/Header/CloneButton.svelte";
+
  import ContextRepo from "@app/views/repos/Sidebar/ContextRepo.svelte";
  import Icon from "@app/components/Icon.svelte";
  import Id from "@app/components/Id.svelte";
  import Link from "@app/components/Link.svelte";
-
  import SeedButton from "@app/views/repos/Header/SeedButton.svelte";
-
  import Share from "@app/views/repos/Share.svelte";
+
  import RepoAvatar from "@app/components/RepoAvatar.svelte";

  export let repo: Repo;
  export let baseUrl: BaseUrl;
-
  export let currentRefname: string;
-

-
  let enabledArchiveDownload = false;
-

-
  void fetch(
-
    `${baseUrlToString(baseUrl)}/raw/${repo.rid}/archive/${currentRefname}`,
-
    {
-
      method: "HEAD",
-
    },
-
  ).then(response => {
-
    enabledArchiveDownload = response.ok;
-
  });
+
  export let seedingPolicy: SeedingPolicy;

  function render(content: string): string {
    return dompurify.sanitize(
@@ -38,6 +26,28 @@
</script>

<style>
+
  .header-layout {
+
    display: flex;
+
    gap: 1rem;
+
    border-bottom: 1px solid var(--color-border-subtle);
+
  }
+
  .avatar {
+
    flex-shrink: 0;
+
    line-height: 0;
+
  }
+
  .meta {
+
    flex: 1;
+
    min-width: 0;
+
    padding: 1rem 0;
+
  }
+
  .info {
+
    flex: 1;
+
    min-width: 0;
+
    padding: 1rem;
+
    border-left: 1px solid var(--color-border-subtle);
+
    display: flex;
+
    align-items: center;
+
  }
  .title {
    align-items: center;
    gap: 0.5rem;
@@ -47,77 +57,100 @@
    justify-content: left;
    text-align: left;
    text-overflow: ellipsis;
-
    padding: 1rem 1rem 0 1rem;
-
  }
-
  .description {
-
    padding: 0 1rem 1rem 1rem;
  }
  .repo-name:hover {
    color: inherit;
  }
+
  .description {
+
    margin-top: 0.5rem;
+
    font: var(--txt-body-m-regular);
+
    color: var(--color-text-tertiary);
+
    display: -webkit-box;
+
    -webkit-line-clamp: 3;
+
    line-clamp: 3;
+
    -webkit-box-orient: vertical;
+
    overflow: hidden;
+
  }
  .description :global(a) {
    border-bottom: 1px solid var(--color-text-tertiary);
  }
  .description :global(a:hover) {
    border-bottom: 1px solid var(--color-text-primary);
  }
-
  .id {
-
    padding-left: 1rem;
-
  }
-
  .title-container {
-
    display: flex;
-
    flex-direction: column;
-
    gap: 0rem;
-
    margin-bottom: 1rem;
+

+
  @media (max-width: 719.98px) {
+
    .header-layout {
+
      flex-wrap: wrap;
+
      padding: 1rem;
+
    }
+
    .avatar :global(img) {
+
      width: 4rem !important;
+
    }
+
    .meta {
+
      flex-basis: 0;
+
      padding: 0;
+
    }
+
    .info {
+
      flex-basis: 100%;
+
      padding: 0;
+
      border-left: none;
+
      border-top: 1px solid var(--color-border-subtle);
+
      padding-top: 0.5rem;
+
    }
+
    .mobile-description {
+
      flex-basis: 100%;
+
    }
  }
</style>

-
<div class="title-container">
-
  <div class="title">
-
    <span class="txt-overflow">
-
      <Link
-
        route={{
-
          resource: "repo.source",
-
          repo: repo.rid,
-
          node: baseUrl,
-
        }}>
-
        <span class="repo-name">
-
          {project.data.name}
-
        </span>
-
      </Link>
-
    </span>
-
    {#if repo.visibility.type === "private"}
-
      <Badge variant="private" size="tiny">
-
        <Icon name="lock" />
-
        Private
-
      </Badge>
-
    {/if}
-
    <div style="margin-left: auto; display: flex; gap: 0.5rem;">
-
      <Share />
-
      <div
-
        style:display="flex"
-
        style:gap="0.5rem"
-
        class="global-hide-on-mobile-down">
-
        <CloneButton
-
          {enabledArchiveDownload}
-
          {baseUrl}
-
          {currentRefname}
-
          id={repo.rid}
-
          name={project.data.name} />
-
        <SeedButton seedCount={repo.seeding} repoId={repo.rid} />
-
      </div>
-
      <div
-
        style:display="flex"
-
        style:gap="0.5rem"
-
        class="global-hide-on-small-desktop-up">
-
        <SeedButton disabled seedCount={repo.seeding} repoId={repo.rid} />
-
      </div>
+
<div class="header-layout">
+
  <div class="avatar">
+
    <RepoAvatar name={project.data.name} rid={repo.rid} styleWidth="10rem" />
+
  </div>
+
  <div class="meta">
+
    <div class="title">
+
      <span class="txt-overflow">
+
        <Link
+
          route={{
+
            resource: "repo.source",
+
            repo: repo.rid,
+
            node: baseUrl,
+
          }}>
+
          <span class="repo-name">
+
            {project.data.name}
+
          </span>
+
        </Link>
+
      </span>
+
      {#if repo.visibility.type === "private"}
+
        <Badge variant="private" size="tiny">
+
          <Icon name="lock" />
+
          Private
+
        </Badge>
+
      {/if}
+
    </div>
+
    <div>
+
      <Id shorten={false} id={repo.rid} ariaLabel="repo-id">
+
        {formatRepositoryId(repo.rid)}
+
      </Id>
+
    </div>
+
    <div
+
      class="description global-hide-on-mobile-down"
+
      title={project.data.description}
+
      use:twemoji>
+
      {@html render(project.data.description)}
    </div>
  </div>
-
  <div class="id">
-
    <Id shorten={false} id={repo.rid} ariaLabel="repo-id" />
+
  <div
+
    class="description mobile-description global-hide-on-small-desktop-up"
+
    title={project.data.description}
+
    use:twemoji>
+
    {@html render(project.data.description)}
+
  </div>
+
  <div class="info">
+
    <ContextRepo
+
      {baseUrl}
+
      repoThreshold={repo.threshold}
+
      repoDelegates={repo.delegates}
+
      {seedingPolicy} />
  </div>
-
</div>
-
<div class="description" use:twemoji>
-
  {@html render(project.data.description)}
</div>
modified src/views/repos/router.ts
@@ -145,7 +145,6 @@ export type RepoLoadedRoute =
      resource: "repo.commit";
      params: {
        baseUrl: BaseUrl;
-
        seedingPolicy: SeedingPolicy;
        repo: Repo;
        commit: Commit;
        nodeAvatarUrl: string | undefined;
@@ -155,7 +154,6 @@ export type RepoLoadedRoute =
      resource: "repo.issue";
      params: {
        baseUrl: BaseUrl;
-
        seedingPolicy: SeedingPolicy;
        repo: Repo;
        rawPath: (commit?: string) => string;
        issue: Issue;
@@ -166,7 +164,6 @@ export type RepoLoadedRoute =
      resource: "repo.issues";
      params: {
        baseUrl: BaseUrl;
-
        seedingPolicy: SeedingPolicy;
        repo: Repo;
        issues: Issue[];
        status: IssueState["status"];
@@ -177,7 +174,6 @@ export type RepoLoadedRoute =
      resource: "repo.patches";
      params: {
        baseUrl: BaseUrl;
-
        seedingPolicy: SeedingPolicy;
        repo: Repo;
        patches: Patch[];
        status: PatchState["status"];
@@ -188,7 +184,6 @@ export type RepoLoadedRoute =
      resource: "repo.patch";
      params: {
        baseUrl: BaseUrl;
-
        seedingPolicy: SeedingPolicy;
        repo: Repo;
        rawPath: (commit?: string) => string;
        patch: Patch;
@@ -270,10 +265,9 @@ export async function loadRepoRoute(
    } else if (route.resource === "repo.history") {
      return await loadHistoryView(route, previousLoaded);
    } else if (route.resource === "repo.commit") {
-
      const [repo, commit, seedingPolicy, node] = await Promise.all([
+
      const [repo, commit, node] = await Promise.all([
        api.repo.getByRid(route.repo),
        api.repo.getCommitBySha(route.repo, route.commit),
-
        api.getPolicyByRid(route.repo),
        api.getNode(),
      ]);

@@ -281,7 +275,6 @@ export async function loadRepoRoute(
        resource: "repo.commit",
        params: {
          baseUrl: route.node,
-
          seedingPolicy,
          repo,
          commit,
          nodeAvatarUrl: node.avatarUrl,
@@ -318,14 +311,13 @@ async function loadPatchesView(
  const searchParams = new URLSearchParams(route.search || "");
  const status = (searchParams.get("status") as PatchState["status"]) || "open";

-
  const [repo, patches, seedingPolicy, node] = await Promise.all([
+
  const [repo, patches, node] = await Promise.all([
    api.repo.getByRid(route.repo),
    api.repo.getAllPatches(route.repo, {
      status,
      page: 0,
      perPage: PATCHES_PER_PAGE,
    }),
-
    api.getPolicyByRid(route.repo),
    api.getNode(),
  ]);

@@ -333,7 +325,6 @@ async function loadPatchesView(
    resource: "repo.patches",
    params: {
      baseUrl: route.node,
-
      seedingPolicy,
      patches,
      status,
      repo,
@@ -348,14 +339,13 @@ async function loadIssuesView(
  const api = new HttpdClient(route.node);
  const status = route.status || "open";

-
  const [repo, issues, seedingPolicy, node] = await Promise.all([
+
  const [repo, issues, node] = await Promise.all([
    api.repo.getByRid(route.repo),
    api.repo.getAllIssues(route.repo, {
      status,
      page: 0,
      perPage: ISSUES_PER_PAGE,
    }),
-
    api.getPolicyByRid(route.repo),
    api.getNode(),
  ]);

@@ -363,7 +353,6 @@ async function loadIssuesView(
    resource: "repo.issues",
    params: {
      baseUrl: route.node,
-
      seedingPolicy,
      issues,
      status,
      repo,
@@ -623,17 +612,15 @@ async function loadIssueView(route: RepoIssueRoute): Promise<RepoLoadedRoute> {
      route.repo
    }${commit ? `/${commit}` : ""}`;

-
  const [repo, issue, seedingPolicy, node] = await Promise.all([
+
  const [repo, issue, node] = await Promise.all([
    api.repo.getByRid(route.repo),
    api.repo.getIssueById(route.repo, route.issue),
-
    api.getPolicyByRid(route.repo),
    api.getNode(),
  ]);
  return {
    resource: "repo.issue",
    params: {
      baseUrl: route.node,
-
      seedingPolicy,
      repo,
      rawPath,
      issue,
@@ -655,7 +642,6 @@ async function loadPatchView(
  let repoPromise: Promise<Repo>;
  let patchPromise: Promise<Patch>;
  let nodePromise: Promise<Partial<Node>>;
-
  let seedingPolicyPromise: Promise<SeedingPolicy>;

  if (
    previousLoaded.resource === "repo.patch" &&
@@ -664,20 +650,17 @@ async function loadPatchView(
  ) {
    repoPromise = Promise.resolve(previousLoaded.params.repo);
    patchPromise = Promise.resolve(previousLoaded.params.patch);
-
    seedingPolicyPromise = Promise.resolve(previousLoaded.params.seedingPolicy);
    nodePromise = Promise.resolve({
      avatarUrl: previousLoaded.params.nodeAvatarUrl,
    });
  } else {
    repoPromise = api.repo.getByRid(route.repo);
    patchPromise = api.repo.getPatchById(route.repo, route.patch);
-
    seedingPolicyPromise = api.getPolicyByRid(route.repo);
    nodePromise = api.getNode();
  }
-
  const [repo, patch, seedingPolicy, { avatarUrl }] = await Promise.all([
+
  const [repo, patch, { avatarUrl }] = await Promise.all([
    repoPromise,
    patchPromise,
-
    seedingPolicyPromise,
    nodePromise,
  ]);

@@ -740,7 +723,6 @@ async function loadPatchView(
    resource: "repo.patch",
    params: {
      baseUrl: route.node,
-
      seedingPolicy,
      repo,
      rawPath,
      patch,
modified tests/e2e/clipboard.spec.ts
@@ -69,8 +69,7 @@ test("copy to clipboard", async ({ page, browserName, context }) => {
    await page.getByRole("button", { name: "59a0821", exact: true }).click();
    await expectClipboard("59a0821edc73630bce540596cffc7854da557365");

-
    await page.getByLabel("filter-dropdown").click();
-
    await page.getByRole("button", { name: "Draft" }).click();
+
    await page.getByRole("link", { name: "Draft" }).click();
    await page.getByRole("button", { name: "783d33c", exact: true }).click();
    await expectClipboard("783d33c5b14e13234d4d7affa98bd0b52d1b1ea3");
  }
modified tests/e2e/repo.spec.ts
@@ -21,10 +21,10 @@ test("navigate to repo", async ({ page }) => {
  // Header.
  {
    const name = page.getByRole("link", { name: "source-browsing" }).nth(1);
-
    const id = page.getByText(sourceBrowsingRid);
-
    const description = page.getByText(
-
      "Git repository for source browsing tests",
-
    );
+
    const id = page.getByLabel("repo-id");
+
    const description = page
+
      .getByText("Git repository for source browsing tests")
+
      .first();

    await expect(name).toBeVisible();
    await expect(id).toBeVisible();
@@ -33,7 +33,9 @@ test("navigate to repo", async ({ page }) => {

  // Repo menu shows default selected branch and commit and contributor counts.
  {
-
    await expect(page.getByTitle("Change branch")).toBeVisible();
+
    await expect(
+
      page.locator('[title="Change branch"]:visible').first(),
+
    ).toBeVisible();
    await expect(
      page
        .getByRole("button", {
@@ -66,7 +68,7 @@ test("repo description", async ({ page, peer }) => {
  await page.goto(peer.ridUrl(rid));
  await page.waitForLoadState("networkidle");
  await expect(
-
    page.getByText("Radicle Heartwood Protocol & Stack"),
+
    page.getByText("Radicle Heartwood Protocol & Stack").first(),
  ).toBeVisible();
});

@@ -83,7 +85,9 @@ test("show source tree at specific revision", async ({ page }) => {
    })
    .click();

-
  await expect(page.getByTitle("Current HEAD")).toContainText("335dd6d");
+
  await expect(page.getByTitle("Current HEAD").first()).toContainText(
+
    "335dd6d",
+
  );
  await expect(page.locator(".source-tree")).toHaveText("bin src");
  await expect(
    page.getByRole("link", {
@@ -317,11 +321,15 @@ test("peer and branch switching", async ({ page }) => {
  // Alice's peer.
  {
    await changeBranch("alice", `main ${shortAliceHead}`, page);
-
    await expect(page.getByTitle("Change branch")).toHaveText(/alice/);
+
    await expect(
+
      page.locator('[title="Change branch"]:visible').first(),
+
    ).toHaveText(/alice/);

    // Default `main` branch.
    {
-
      await expect(page.getByTitle("Change branch")).toHaveText(/main/);
+
      await expect(
+
        page.locator('[title="Change branch"]:visible').first(),
+
      ).toHaveText(/main/);
      await expect(
        page
          .getByRole("button", {
@@ -339,8 +347,11 @@ test("peer and branch switching", async ({ page }) => {
    // Feature branch with a slash in the name.
    {
      await changeBranch("alice", "feature/branch", page);
-
      await page.getByTitle("Change branch").click();
-
      await page.getByText("feature/branch").click();
+
      await page.locator('[title="Change branch"]:visible').first().click();
+
      await page
+
        .getByRole("button", { name: "feature/branch" })
+
        .first()
+
        .click();

      await expect(
        page.getByRole("button", { name: "feature/branch" }),
@@ -379,10 +390,16 @@ test("peer and branch switching", async ({ page }) => {
  {
    await page.getByRole("link", { name: "source-browsing" }).nth(1).click();

-
    await expect(page.getByTitle("Change branch")).not.toContainText("alice");
-
    await expect(page.getByTitle("Change branch")).not.toContainText("bob");
+
    await expect(
+
      page.locator('[title="Change branch"]:visible').first(),
+
    ).not.toContainText("alice");
+
    await expect(
+
      page.locator('[title="Change branch"]:visible').first(),
+
    ).not.toContainText("bob");

-
    await expect(page.getByTitle("Change branch")).toBeVisible();
+
    await expect(
+
      page.locator('[title="Change branch"]:visible').first(),
+
    ).toBeVisible();
    await expect(
      page
        .getByRole("button", {
@@ -437,7 +454,7 @@ test("only one modal can be open at a time", async ({ page }) => {
  await expect(page.getByText("Use the Radicle CLI")).not.toBeVisible();
  await expect(page.getByText("bob")).not.toBeVisible();

-
  await page.getByTitle("Change branch").click();
+
  await page.locator('[title="Change branch"]:visible').first().click();
  await expect(page.getByText("Code font")).not.toBeVisible();
  await expect(page.getByText("Use the Radicle CLI")).not.toBeVisible();
  await expect(page.getByText("bob")).toBeVisible();
modified tests/e2e/repo/commit.spec.ts
@@ -14,7 +14,7 @@ test("navigation from commit list", async ({ page }) => {
  await page.goto(sourceBrowsingUrl);
  await changeBranch("bob", `main ${shortBobHead}`, page);

-
  await page.getByText("Update readme").first().click();
+
  await page.getByRole("link", { name: "Update readme" }).first().click();
  await expect(page).toHaveURL(commitUrl);
});

@@ -103,13 +103,10 @@ test("navigation to source tree at specific revision", async ({ page }) => {
  // Go to source tree at this revision.
  await page.getByTitle("View file at this commit").click();
  const branchSelectorCommitButton = page.getByTitle("Current HEAD").first();
-
  await expect(
-
    branchSelectorCommitButton.getByText("Add a deeply nested directory tree"),
-
  ).toBeVisible();
+
  await expect(branchSelectorCommitButton).toContainText("0801ace");
  await expect(page).toHaveURL(
    `${sourceBrowsingUrl}/tree/0801aceeab500033f8d608778218657bd626ef73/deep/directory/hierarchy/is/entirely/possible/in/git/repositories/.gitkeep`,
  );
-
  await expect(branchSelectorCommitButton).toContainText("0801ace");
  await expect(page.locator(".source-tree >> text=.gitkeep")).toBeVisible();
  await expect(
    page
modified tests/e2e/repo/commits.spec.ts
@@ -23,9 +23,9 @@ test("peer and branch switching", async ({ page }) => {
  {
    await changeBranch("alice", `main ${shortAliceHead}`, page);

-
    await expect(page.getByTitle("Change branch")).toHaveText(
-
      "alice Delegate / main",
-
    );
+
    await expect(
+
      page.locator('[title="Change branch"]:visible').first(),
+
    ).toHaveText("alice Delegate / main");

    await expect(page.getByText("Thursday, November 17, 2022")).toBeVisible();
    await expect(page.locator(".list .teaser")).toHaveCount(
@@ -63,7 +63,9 @@ test("peer and branch switching", async ({ page }) => {
  {
    await changeBranch("bob", `main ${shortBobHead}`, page);

-
    await expect(page.getByTitle("Change branch")).toContainText("bob");
+
    await expect(
+
      page.locator('[title="Change branch"]:visible').first(),
+
    ).toContainText("bob");

    await expect(page.getByText("Wednesday, December 21, 2022")).toBeVisible();
    await expect(page.locator(".list").first().locator(".teaser")).toHaveCount(
@@ -158,7 +160,9 @@ test("relative timestamps", async ({ page }) => {
  await expect(page.getByText("Thursday, December 15,")).toBeVisible();

  await changeBranch("bob", `main ${shortBobHead}`, page);
-
  await expect(page.getByTitle("Change branch")).toHaveText(/bob/);
+
  await expect(
+
    page.locator('[title="Change branch"]:visible').first(),
+
  ).toHaveText(/bob/);
  const latestCommit = page.locator(".teaser").first();
  await expect(latestCommit).toContainText(
    `Bob Belcher committed ${shortBobHead} now`,
@@ -194,7 +198,9 @@ test("pushing changes while viewing history", async ({ page, peerManager }) => {
  await expect(page).toHaveURL(`${alice.uiUrl()}/${rid}/history`);
  await expect(page.getByRole("link", { name: "Commits 2" })).toBeVisible();

-
  await expect(page.getByTitle("Change branch")).toHaveText("main Canonical");
+
  await expect(
+
    page.locator('[title="Change branch"]:visible').first(),
+
  ).toHaveText("main Canonical");
  const branchSelectorCommitButton = page.getByTitle("Current HEAD").first();
  await expect(branchSelectorCommitButton).toHaveText("516fa74 first change");

modified tests/e2e/repo/issues.spec.ts
@@ -7,8 +7,7 @@ test("navigate issue listing", async ({ page }) => {
  await page.getByRole("link", { name: "Issues 1" }).click();
  await expect(page).toHaveURL(`${cobUrl}/issues`);

-
  await page.getByRole("button", { name: "filter-dropdown" }).first().click();
-
  await page.getByRole("link", { name: "Closed 2" }).click();
+
  await page.getByRole("link", { name: "Closed" }).click();
  await expect(page).toHaveURL(`${cobUrl}/issues?status=closed`);
});

@@ -39,12 +38,8 @@ test("issue counters", async ({ page, peer }) => {
    ],
    { cwd: repoFolder },
  );
-
  await page.getByRole("button", { name: "filter-dropdown" }).first().click();
-
  await page.locator(".dropdown-item").getByText("Open 1").click();
+
  await page.getByRole("link", { name: "Open" }).first().click();
  await expect(page.getByRole("button", { name: "Issues 2" })).toBeVisible();
-
  await expect(
-
    page.getByRole("button", { name: "filter-dropdown" }).first(),
-
  ).toHaveText("Open 2");
  await expect(page.locator(".issue-teaser")).toHaveCount(2);

  await page
modified tests/e2e/repo/patches.spec.ts
@@ -6,8 +6,7 @@ test("navigate patch listing", async ({ page }) => {
  await page.getByRole("link", { name: "Patches 2" }).click();
  await expect(page).toHaveURL(`${cobUrl}/patches`);

-
  await page.getByRole("button", { name: "filter-dropdown" }).first().click();
-
  await page.getByRole("link", { name: "Merged 1" }).click();
+
  await page.getByRole("link", { name: "Merged" }).click();
  await expect(page).toHaveURL(`${cobUrl}/patches?status=merged`);
  await expect(
    page.locator(".comments").filter({ hasText: "5" }),
@@ -40,11 +39,7 @@ test("patches counters", async ({ page, peer }) => {
  await peer.git(["push", "rad", "HEAD:refs/patches"], {
    cwd: repoFolder,
  });
-
  await page.getByRole("button", { name: "filter-dropdown" }).first().click();
-
  await page.locator(".dropdown-item").getByText("Open 1").click();
+
  await page.getByRole("link", { name: "Open" }).first().click();
  await expect(page.getByRole("button", { name: "Patches 2" })).toBeVisible();
-
  await expect(
-
    page.getByRole("button", { name: "filter-dropdown" }).first(),
-
  ).toHaveText("Open 2");
  await expect(page.locator(".patch-teaser")).toHaveCount(2);
});
modified tests/support/repo.ts
@@ -4,7 +4,7 @@ import type { RadiclePeer } from "@tests/support/peerManager";
import * as Path from "node:path";

export async function changeBranch(peer: string, branch: string, page: Page) {
-
  await page.getByTitle("Change branch").click();
+
  await page.locator('[title="Change branch"]:visible').first().click();
  const peerLocator = page.getByLabel("peer-item").filter({ hasText: peer });
  await peerLocator.getByTitle("Expand peer").click();
  await page.getByRole("button", { name: branch }).click();