Radish alpha
r
rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5
Radicle web interface
Radicle
Git
Add delegate and node policies information in sidebar
Open did:key:z6MkkfM3...sVz5 opened 1 year ago
17 files changed +461 -141 29d34096 c47fab26
modified src/components/IconSmall.svelte
@@ -53,6 +53,7 @@
    | "online"
    | "patch"
    | "plus"
+
    | "quorum"
    | "repo"
    | "seedling"
    | "settings"
@@ -349,11 +350,9 @@
      d="M8.10975 3.42707C8.04691 3.37209 7.95308 3.37209 7.89025 3.42707L2.99592 7.70961C2.7881 7.89145 2.47222 7.87039 2.29038 7.66258C2.10853 7.45476 2.12959 7.13888 2.33741 6.95703L7.23174 2.67449C7.6716 2.28961 8.32839 2.28961 8.76825 2.67449L13.6626 6.95703C13.8704 7.13888 13.8915 7.45476 13.7096 7.66258C13.5278 7.87039 13.2119 7.89145 13.0041 7.70961L8.10975 3.42707ZM4.73737 7.50501C5.01074 7.54407 5.20069 7.79733 5.16164 8.0707L4.71239 11.2155C4.64067 11.7175 5.03022 12.1667 5.53734 12.1667H6.16666V10.6667C6.16666 9.65413 6.98748 8.83332 8 8.83332C9.01252 8.83332 9.83333 9.65413 9.83333 10.6667V12.1667H10.4627C10.9698 12.1667 11.3593 11.7175 11.2876 11.2155L10.8384 8.0707C10.7993 7.79733 10.9893 7.54407 11.2626 7.50501C11.536 7.46596 11.7893 7.65591 11.8283 7.92928L12.2776 11.074C12.4353 12.1785 11.5783 13.1667 10.4627 13.1667H5.53734C4.42167 13.1667 3.56466 12.1785 3.72244 11.074L4.17169 7.92928C4.21074 7.65591 4.46401 7.46596 4.73737 7.50501ZM8.83333 12.1667V10.6667C8.83333 10.2064 8.46023 9.83332 8 9.83332C7.53976 9.83332 7.16666 10.2064 7.16666 10.6667V12.1667H8.83333Z" />
  {:else if name === "info"}
    <path
-
      d="M9 10.8419C9 11.3865 8.54467 11.8419 8 11.8419C7.45533 11.8419 7 11.3865 7 10.8419C7 10.2972 7.45533 9.84188 8 9.84188C8.54467 9.84188 9 10.2972 9 10.8419Z" />
+
      d="M2.5 8C2.5 5.3534 4.52821 2.5 8 2.5C11.4718 2.5 13.5 5.3534 13.5 8C13.5 9.23995 12.9967 10.5534 12.2001 11.5551C11.3998 12.5615 10.3633 13.1837 9.33333 13.1837C8.68303 13.1837 8.33754 12.925 8.14609 12.5639C7.93269 12.1615 7.87359 11.5555 8.00151 10.8385L8.49223 8.08782C8.51822 7.94214 8.47835 7.79244 8.38335 7.67899C8.28834 7.56554 8.14797 7.5 8 7.5H6.3125C6.03636 7.5 5.8125 7.72386 5.8125 8C5.8125 8.27614 6.03636 8.5 6.3125 8.5H7.4029L7.01705 10.6628C6.86857 11.4951 6.90375 12.3556 7.26261 13.0324C7.64341 13.7505 8.34815 14.1837 9.33333 14.1837C10.7695 14.1837 12.0664 13.33 12.9828 12.1775C13.9031 11.0203 14.5 9.49194 14.5 8C14.5 4.90438 12.122 1.5 8 1.5C3.87804 1.5 1.5 4.90438 1.5 8C1.5 10.5817 2.91858 12.8362 5.38742 13.9396C5.63953 14.0523 5.93524 13.9393 6.04792 13.6872C6.16059 13.435 6.04756 13.1393 5.79545 13.0267C3.68759 12.0846 2.5 10.1883 2.5 8Z" />
    <path
-
      fill-rule="evenodd"
-
      clip-rule="evenodd"
-
      d="M8 2.5C5.3534 2.5 2.5 4.52821 2.5 8C2.5 11.4718 5.3534 13.5 8 13.5C10.6466 13.5 13.5 11.4718 13.5 8C13.5 7.72386 13.7239 7.5 14 7.5C14.2761 7.5 14.5 7.72386 14.5 8C14.5 12.122 11.0956 14.5 8 14.5C4.90438 14.5 1.5 12.122 1.5 8C1.5 3.87804 4.90438 1.5 8 1.5C9.52918 1.5 10.6747 1.89561 11.4304 2.53262C12.1907 3.17354 12.5274 4.03904 12.4446 4.87775C12.3622 5.71281 11.8678 6.47921 11.0557 6.93153C10.3805 7.30768 9.51064 7.45531 8.5 7.28936V8.35417C8.5 8.63031 8.27614 8.85417 8 8.85417C7.72386 8.85417 7.5 8.63031 7.5 8.35417V6.66667C7.5 6.51033 7.57312 6.363 7.69763 6.26845C7.82214 6.17391 7.9837 6.14305 8.13429 6.18504C9.22008 6.48779 10.0333 6.35637 10.5691 6.05792C11.1056 5.75907 11.4005 5.27625 11.4495 4.77954C11.4981 4.28648 11.3084 3.73766 10.7859 3.2972C10.2588 2.85283 9.36507 2.5 8 2.5Z" />
+
      d="M7.33334 5.66666C7.33334 6.21132 7.78868 6.66666 8.33334 6.66666C8.87801 6.66666 9.33334 6.21132 9.33334 5.66666C9.33334 5.12199 8.87801 4.66666 8.33334 4.66666C7.78868 4.66666 7.33334 5.12199 7.33334 5.66666Z" />
  {:else if name === "issue"}
    <path
      fill-rule="evenodd"
@@ -499,6 +498,43 @@
      fill-rule="evenodd"
      clip-rule="evenodd"
      d="M13.1667 8C13.1667 8.27614 12.9428 8.5 12.6667 8.5L3.33334 8.5C3.0572 8.5 2.83334 8.27614 2.83334 8C2.83334 7.72386 3.0572 7.5 3.33334 7.5L12.6667 7.5C12.9428 7.5 13.1667 7.72386 13.1667 8Z" />
+
  {:else if name === "quorum"}
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M12 4C12.5523 4 13 3.55228 13 3C13 2.44772 12.5523 2 12 2C11.4477 2 11 2.44772 11 3C11 3.55228 11.4477 4 12 4ZM12 5C13.1046 5 14 4.10457 14 3C14 1.89543 13.1046 1 12 1C10.8954 1 10 1.89543 10 3C10 4.10457 10.8954 5 12 5Z" />
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M4 14C4.55228 14 5 13.5523 5 13C5 12.4477 4.55228 12 4 12C3.44772 12 3 12.4477 3 13C3 13.5523 3.44772 14 4 14ZM4 15C5.10457 15 6 14.1046 6 13C6 11.8954 5.10457 11 4 11C2.89543 11 2 11.8954 2 13C2 14.1046 2.89543 15 4 15Z" />
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M4 4C4.55228 4 5 3.55228 5 3C5 2.44772 4.55228 2 4 2C3.44772 2 3 2.44772 3 3C3 3.55228 3.44772 4 4 4ZM4 5C5.10457 5 6 4.10457 6 3C6 1.89543 5.10457 1 4 1C2.89543 1 2 1.89543 2 3C2 4.10457 2.89543 5 4 5Z" />
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M12 14C12.5523 14 13 13.5523 13 13C13 12.4477 12.5523 12 12 12C11.4477 12 11 12.4477 11 13C11 13.5523 11.4477 14 12 14ZM12 15C13.1046 15 14 14.1046 14 13C14 11.8954 13.1046 11 12 11C10.8954 11 10 11.8954 10 13C10 14.1046 10.8954 15 12 15Z" />
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M8 3.5C8.15738 3.5 8.30557 3.5741 8.4 3.7L11.4 7.7C11.5333 7.87778 11.5333 8.12222 11.4 8.3L8.4 12.3C8.30557 12.4259 8.15738 12.5 8 12.5C7.84262 12.5 7.69443 12.4259 7.6 12.3L4.6 8.3C4.46667 8.12222 4.46667 7.87778 4.6 7.7L7.6 3.7C7.69443 3.5741 7.84262 3.5 8 3.5ZM5.625 8L8 11.1667L10.375 8L8 4.83333L5.625 8Z" />
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M4.49592 11.5631L6.0103 9.64167L6.79568 10.2607L5.2813 12.1821L4.49592 11.5631Z" />
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M9.09293 5.6512L10.6073 3.7298L11.3927 4.34882L9.87831 6.27021L9.09293 5.6512Z" />
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M11.3927 11.5631L9.87831 9.64167L9.09293 10.2607L10.6073 12.1821L11.3927 11.5631Z" />
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M6.79568 5.6512L5.28129 3.7298L4.49591 4.34882L6.0103 6.27021L6.79568 5.6512Z" />
  {:else if name === "repo"}
    <path
      fill-rule="evenodd"
modified src/components/ScopePolicyExplainer.svelte
@@ -1,17 +1,41 @@
<script lang="ts">
  import type { Scope, Policy } from "@httpd-client";

+
  import { capitalize } from "lodash";
+

  export let scope: Scope;
  export let policy: Policy;
</script>

-
{#if policy === "allow"}
-
  All discovered repositories will get seeded,
-
{:else if policy === "block"}
-
  Only repositories marked as such will get seeded,
-
{/if}
-
{#if scope === "all"}
-
  and all changes in those repositories, made by any peer, will be synced.
-
{:else if scope === "followed"}
-
  and only changes made by explicitly followed peers will be synced.
-
{/if}
+
<style>
+
  .section {
+
    display: flex;
+
    justify-content: space-between;
+
    align-items: center;
+
    padding: 0.5rem 0;
+
  }
+
</style>
+

+
<div class="section">
+
  Scope:
+
  <span class="txt-bold">{capitalize(scope)}</span>
+
</div>
+
<div class="txt-missing">
+
  {#if scope === "all"}
+
    All changes in seeded repositories, made by any peer, will be synced.
+
  {:else if scope === "followed"}
+
    Only changes made by explicitly followed peers will be synced.
+
  {/if}
+
</div>
+

+
<div class="section">
+
  Policy:
+
  <span class="txt-bold">{capitalize(policy)}</span>
+
</div>
+
<div class="txt-missing">
+
  {#if policy === "allow"}
+
    All discovered repositories will get seeded.
+
  {:else if policy === "block"}
+
    Only repositories marked as such will get seeded.
+
  {/if}
+
</div>
modified src/components/TextInput.svelte
@@ -145,6 +145,7 @@
    {placeholder}
    {disabled}
    bind:value
+
    autocomplete="off"
    on:input
    on:focus={handleFocusEvent}
    on:blur={handleFocusEvent}
modified src/views/nodes/View.svelte
@@ -1,6 +1,8 @@
<script lang="ts">
  import type { BaseUrl, NodeStats, Policy, Scope } from "@httpd-client";

+
  import { capitalize } from "lodash";
+

  import * as router from "@app/lib/router";
  import { api, httpdStore } from "@app/lib/httpd";
  import { baseUrlToString, isLocal, truncateId } from "@app/lib/utils";
@@ -10,9 +12,12 @@

  import AppLayout from "@app/App/AppLayout.svelte";
  import CopyableId from "@app/components/CopyableId.svelte";
+
  import IconButton from "@app/components/IconButton.svelte";
+
  import IconSmall from "@app/components/IconSmall.svelte";
  import Loading from "@app/components/Loading.svelte";
+
  import Popover from "@app/components/Popover.svelte";
  import ProjectCard from "@app/components/ProjectCard.svelte";
-
  import ScopePolicyPopover from "@app/views/nodes/ScopePolicyPopover.svelte";
+
  import ScopePolicyExplainer from "@app/components/ScopePolicyExplainer.svelte";

  export let baseUrl: BaseUrl;
  export let nid: string;
@@ -22,6 +27,8 @@
  export let policy: Policy | undefined = undefined;
  export let scope: Scope | undefined = undefined;

+
  $: shortScope =
+
    scope === "all" && policy === "allow" ? "permissive" : "restrictive";
  $: hostname = isLocal(baseUrl.hostname) ? "Local Node" : baseUrl.hostname;
  $: session =
    $httpdStore.state === "authenticated" && isLocal(api.baseUrl.hostname)
@@ -68,6 +75,11 @@
    font-family: var(--font-family-monospace);
    font-size: var(--font-size-small);
  }
+
  .seeding-policies {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.5rem;
+
  }

  .project-grid {
    display: grid;
@@ -138,21 +150,28 @@
        </div>
      </div>

-
      <div class="subtitle">
+
      <div class="subtitle" style:justify-content="space-between">
        <div class="txt-semibold">
          {isLocal(baseUrl.hostname) ? "Seeded" : "Pinned"} projects
        </div>
-
        <div class="global-hide-on-mobile-down" style:margin-left="auto">
+
        <div class="seeding-policies">
          {#if policy && scope}
-
            <ScopePolicyPopover {scope} {policy} popoverPositionRight="0" />
+
            <span class="txt-bold">Seeding Policies:</span>
+
            {capitalize(shortScope)}
+
            <div class="global-hide-on-mobile-down">
+
              <Popover
+
                popoverPositionBottom="0"
+
                popoverPositionLeft="-17rem"
+
                popoverPositionRight="2rem">
+
                <IconButton slot="toggle" let:toggle on:click={toggle}>
+
                  <IconSmall name="help" />
+
                </IconButton>
+
                <ScopePolicyExplainer slot="popover" {scope} {policy} />
+
              </Popover>
+
            </div>
          {/if}
        </div>
      </div>
-
      <div class="subtitle global-hide-on-small-desktop-up">
-
        {#if policy && scope}
-
          <ScopePolicyPopover {scope} {policy} popoverPositionRight="-4.5rem" />
-
        {/if}
-
      </div>

      <div style:margin-top="1rem" style:padding-bottom="2.5rem">
        {#await fetchProjectInfos( baseUrl, { show: isLocal(baseUrl.hostname) ? "all" : "pinned", perPage: stats.repos.total }, )}
modified src/views/projects/Commit.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { BaseUrl, Commit, Project } from "@httpd-client";
+
  import type { BaseUrl, Commit, Node, Project } from "@httpd-client";

  import { formatCommit } from "@app/lib/utils";

@@ -14,6 +14,7 @@
  import Share from "./Share.svelte";

  export let baseUrl: BaseUrl;
+
  export let node: Node;
  export let commit: Commit;
  export let project: Project;

@@ -46,7 +47,7 @@
  }
</style>

-
<Layout {baseUrl} {project}>
+
<Layout {node} {baseUrl} {project}>
  <div class="commit">
    <div class="header">
      <div style="display:flex; flex-direction: column; gap: 0.5rem;">
modified src/views/projects/History.svelte
@@ -4,6 +4,7 @@
    CommitHeader,
    Project,
    Remote,
+
    Node,
    Tree,
  } from "@httpd-client";
  import type { Route } from "@app/lib/router";
@@ -23,6 +24,7 @@
  import ProjectNameHeader from "./Source/ProjectNameHeader.svelte";

  export let baseUrl: BaseUrl;
+
  export let node: Node;
  export let commit: string;
  export let branches: string[];
  export let commitHeaders: CommitHeader[];
@@ -106,7 +108,7 @@
  }
</style>

-
<Layout {baseUrl} {project} activeTab="source">
+
<Layout {node} {baseUrl} {project} activeTab="source">
  <ProjectNameHeader {project} {baseUrl} {seeding} slot="header" />

  <div style:margin="1rem 0 1rem 1rem" slot="subheader">
modified src/views/projects/Issue.svelte
@@ -7,6 +7,7 @@
    Issue,
    IssueState,
    Project,
+
    Node,
  } from "@httpd-client";
  import type { Session } from "@app/lib/httpd";

@@ -48,6 +49,7 @@
  import ThreadComponent from "@app/components/Thread.svelte";

  export let baseUrl: BaseUrl;
+
  export let node: Node;
  export let issue: Issue;
  export let project: Project;
  export let rawPath: (commit?: string) => string;
@@ -482,7 +484,7 @@
  }
</style>

-
<Layout {baseUrl} {project} activeTab="issues" stylePaddingBottom="0">
+
<Layout {node} {baseUrl} {project} activeTab="issues" stylePaddingBottom="0">
  <div class="issue">
    <div class="main">
      <CobHeader>
modified src/views/projects/Issue/New.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { BaseUrl, Embed, Project, Reaction } from "@httpd-client";
+
  import type { BaseUrl, Embed, Node, Project, Reaction } from "@httpd-client";

  import * as modal from "@app/lib/modal";
  import * as router from "@app/lib/router";
@@ -17,6 +17,7 @@
  import TextInput from "@app/components/TextInput.svelte";

  export let baseUrl: BaseUrl;
+
  export let node: Node;
  export let project: Project;
  export let rawPath: (commit?: string) => string;

@@ -99,7 +100,7 @@
  }
</style>

-
<Layout {baseUrl} {project} activeTab="issues">
+
<Layout {node} {baseUrl} {project} activeTab="issues">
  {#if session}
    {@const session_ = session}
    <div class="form">
modified src/views/projects/Issues.svelte
@@ -1,5 +1,11 @@
<script lang="ts">
-
  import type { BaseUrl, Issue, IssueState, Project } from "@httpd-client";
+
  import type {
+
    BaseUrl,
+
    Issue,
+
    IssueState,
+
    Node,
+
    Project,
+
  } from "@httpd-client";

  import capitalize from "lodash/capitalize";
  import { HttpdClient } from "@httpd-client";
@@ -24,6 +30,7 @@
  import Share from "./Share.svelte";

  export let baseUrl: BaseUrl;
+
  export let node: Node;
  export let issues: Issue[];
  export let project: Project;
  export let state: IssueState["status"];
@@ -105,7 +112,7 @@
  }
</style>

-
<Layout {baseUrl} {project} activeTab="issues">
+
<Layout {node} {baseUrl} {project} activeTab="issues">
  <div slot="header" style:display="flex" style:padding="1rem">
    <Popover
      popoverPadding="0"
modified src/views/projects/Layout.svelte
@@ -1,6 +1,6 @@
<script lang="ts">
  import type { ActiveTab } from "./Header.svelte";
-
  import type { BaseUrl, Project } from "@httpd-client";
+
  import type { BaseUrl, Node, Project } from "@httpd-client";

  import AppHeader from "@app/App/Header.svelte";

@@ -11,6 +11,7 @@
  import Sidebar from "@app/views/projects/Sidebar.svelte";

  export let activeTab: ActiveTab | undefined = undefined;
+
  export let node: Node;
  export let baseUrl: BaseUrl;
  export let project: Project;
  export let stylePaddingBottom: string = "2.5rem";
@@ -65,11 +66,11 @@
  </div>

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

  <div class="sidebar global-hide-on-mobile-down global-hide-on-desktop-up">
-
    <Sidebar {activeTab} {baseUrl} {project} collapsedOnly />
+
    <Sidebar {node} {activeTab} {baseUrl} {project} collapsedOnly />
  </div>

  <div class="content" style:padding-bottom={stylePaddingBottom}>
modified src/views/projects/Patch.svelte
@@ -8,6 +8,7 @@
    PatchState,
    Revision,
    Diff,
+
    Node,
  } from "@httpd-client";

  interface Thread {
@@ -91,6 +92,7 @@
  import { closeFocused } from "@app/components/Popover.svelte";

  export let baseUrl: BaseUrl;
+
  export let node: Node;
  export let patch: Patch;
  export let stats: Diff["stats"];
  export let rawPath: (commit?: string) => string;
@@ -706,7 +708,7 @@
  }
</style>

-
<Layout {baseUrl} {project} activeTab="patches" stylePaddingBottom="0">
+
<Layout {node} {baseUrl} {project} activeTab="patches" stylePaddingBottom="0">
  <div class="patch">
    <div class="main">
      <CobHeader>
modified src/views/projects/Patches.svelte
@@ -1,5 +1,11 @@
<script lang="ts">
-
  import type { BaseUrl, Patch, PatchState, Project } from "@httpd-client";
+
  import type {
+
    BaseUrl,
+
    Node,
+
    Patch,
+
    PatchState,
+
    Project,
+
  } from "@httpd-client";

  import { HttpdClient } from "@httpd-client";
  import capitalize from "lodash/capitalize";
@@ -25,6 +31,7 @@
  import Command from "@app/components/Command.svelte";

  export let baseUrl: BaseUrl;
+
  export let node: Node;
  export let patches: Patch[];
  export let project: Project;
  export let state: PatchState["status"];
@@ -120,7 +127,7 @@
  }
</style>

-
<Layout {baseUrl} {project} activeTab="patches">
+
<Layout {node} {baseUrl} {project} activeTab="patches">
  <div slot="header" style:display="flex" style:padding="1rem">
    <Popover
      popoverPadding="0"
modified src/views/projects/Sidebar.svelte
@@ -1,15 +1,17 @@
<script lang="ts">
  import type { ActiveTab } from "./Header.svelte";
-
  import type { BaseUrl, Project } from "@httpd-client";
+
  import type { BaseUrl, Node, Project } from "@httpd-client";
+

+
  import { onMount } from "svelte";

  import { experimental } from "@app/lib/appearance";
-
  import { queryProject } from "@app/lib/projects";
  import { httpdStore, api } from "@app/lib/httpd";
  import { isLocal } from "@app/lib/utils";
-
  import { onMount } from "svelte";
+
  import { queryProject } from "@app/lib/projects";

  import Button from "@app/components/Button.svelte";
  import ContextHelp from "@app/views/projects/Sidebar/ContextHelp.svelte";
+
  import ContextRepo from "@app/views/projects/Sidebar/ContextRepo.svelte";
  import IconSmall from "@app/components/IconSmall.svelte";
  import Link from "@app/components/Link.svelte";
  import Loading from "@app/components/Loading.svelte";
@@ -21,6 +23,7 @@
  const SIDEBAR_STATE_KEY = "sidebarState";

  export let activeTab: ActiveTab | undefined = undefined;
+
  export let node: Node;
  export let baseUrl: BaseUrl;
  export let project: Project;
  export let collapsedOnly = false;
@@ -129,17 +132,17 @@
    justify-content: space-between;
    width: 100%;
  }
+
  .repo,
  .help {
    z-index: 10;
-
    opacity: 0;
-
  }
-
  .help.expanded {
    opacity: 1;
-
    transition: opacity 150ms;
+
    transition:
+
      opacity 150ms,
+
      display 150ms allow-discrete;
    transition-delay: 150ms;
  }
  .help-box {
-
    width: 20.5rem;
+
    width: 100%;
    padding: 1rem;
    margin-bottom: 0.5rem;
    background-color: var(--color-background-float);
@@ -147,19 +150,20 @@
    font-size: var(--font-size-small);
    border-radius: var(--border-radius-small);
  }
+
  .repo-box {
+
    margin-bottom: 0.5rem;
+
  }
  .vertical-buttons {
    opacity: 1;
    height: fit-content;
    display: flex;
    flex-direction: column-reverse;
-
    transition: opacity 150ms ease-in-out;
+
    transition:
+
      opacity 150ms ease-in-out,
+
      display 150ms ease-in-out allow-discrete;
    transition-delay: 60ms;
    margin-bottom: 0.5rem;
  }
-
  .vertical-buttons.expanded {
-
    opacity: 0;
-
    height: 0;
-
  }
  .horizontal-buttons {
    display: flex;
    gap: 0.5rem;
@@ -170,6 +174,13 @@
    opacity: 1;
    transition: opacity 150ms ease-in-out;
  }
+
  .collapse-label {
+
    display: none;
+
  }
+
  .collapse-label.expanded {
+
    display: block;
+
    transition: opacity 30ms ease-in-out;
+
  }
  .icon {
    transform: rotate(180deg);
    transition: transform 150ms ease-in-out;
@@ -185,6 +196,7 @@
</style>

<div class="sidebar" class:expanded>
+
  <!-- Top Navigation Items -->
  <div class="project-navigation">
    <Link
      title="Source"
@@ -258,87 +270,113 @@
      </Button>
    </Link>
  </div>
+
  <!-- Context and other information section -->
  <div class="bottom">
-
    <div class="help" class:expanded>
-
      {#if !hideContextHelp && expanded && $experimental}
-
        {#if !localProject}
-
          <div
-
            style="display: flex; justify-content: center; align-items: center; height: 2rem;">
-
            <Loading small />
-
          </div>
-
        {:else}
-
          <div class="help-box">
-
            <ContextHelp
-
              {localProject}
-
              {baseUrl}
-
              projectId={project.id}
-
              hideLocalButton={isLocal(baseUrl.hostname) ||
-
                localProject !== "found"}
-
              disableLocalButton={$httpdStore.state !== "authenticated"} />
-
          </div>
+
    {#if expanded}
+
      <div class="help">
+
        {#if !hideContextHelp && $experimental}
+
          {#if !localProject}
+
            <div
+
              style="display: flex; justify-content: center; align-items: center; height: 2rem;">
+
              <Loading small />
+
            </div>
+
          {:else}
+
            <div class="help-box">
+
              <ContextHelp
+
                {localProject}
+
                {baseUrl}
+
                projectId={project.id}
+
                hideLocalButton={isLocal(baseUrl.hostname) ||
+
                  localProject !== "found"}
+
                disableLocalButton={$httpdStore.state !== "authenticated"} />
+
            </div>
+
          {/if}
        {/if}
-
      {/if}
-
    </div>
-
    <div class="vertical-buttons" class:expanded style:gap="0.5rem">
-
      <Popover popoverPositionBottom="0" popoverPositionLeft="3rem">
-
        <Button
-
          stylePadding="0 0.75rem"
-
          variant="background"
-
          title="Settings"
-
          slot="toggle"
-
          let:toggle
-
          on:click={toggle}>
-
          <IconSmall name="settings" />
-
        </Button>
+
      </div>
+
      <div class="repo">
+
        <div class="repo-box">
+
          <ContextRepo {project} {baseUrl} {node} />
+
        </div>
+
      </div>
+
    {:else}
+
      <div class="vertical-buttons" style:gap="0.5rem">
+
        <Popover popoverPositionBottom="0" popoverPositionLeft="3rem">
+
          <Button
+
            stylePadding="0 0.75rem"
+
            variant="background"
+
            title="Settings"
+
            slot="toggle"
+
            let:toggle
+
            on:click={toggle}>
+
            <IconSmall name="settings" />
+
          </Button>

-
        <Settings slot="popover" />
-
      </Popover>
+
          <Settings slot="popover" />
+
        </Popover>

-
      <Popover popoverPositionBottom="0" popoverPositionLeft="3rem">
-
        <Button
-
          stylePadding="0 0.75rem"
-
          variant="background"
-
          title="Help"
-
          slot="toggle"
-
          let:toggle
-
          on:click={toggle}>
-
          <IconSmall name="help" />
-
        </Button>
+
        <Popover popoverPositionBottom="0" popoverPositionLeft="3rem">
+
          <Button
+
            stylePadding="0 0.75rem"
+
            variant="background"
+
            title="Help"
+
            slot="toggle"
+
            let:toggle
+
            on:click={toggle}>
+
            <IconSmall name="help" />
+
          </Button>
+

+
          <Help slot="popover" />
+
        </Popover>

-
        <Help slot="popover" />
-
      </Popover>
+
        <Popover popoverPositionBottom="0" popoverPositionLeft="2rem">
+
          <Button
+
            stylePadding="0 0.75rem"
+
            variant="background"
+
            title="Info"
+
            slot="toggle"
+
            let:toggle
+
            on:click={toggle}>
+
            <IconSmall name="info" />
+
          </Button>

-
      {#if !hideContextHelp && $experimental}
-
        {#if !localProject}
-
          <div
-
            style="display: flex; justify-content: center; align-items: center; height: 2rem;">
-
            <Loading small condensed />
+
          <div slot="popover">
+
            <ContextRepo disablePopovers {node} {baseUrl} {project} />
          </div>
-
        {:else}
-
          <Popover popoverPositionBottom="0" popoverPositionLeft="3rem">
-
            <Button
-
              stylePadding="0 0.75rem"
-
              variant="background"
-
              title="Local node"
-
              slot="toggle"
-
              let:toggle
-
              on:click={toggle}>
-
              <IconSmall name="device" />
-
            </Button>
+
        </Popover>

-
            <ContextHelp
-
              {localProject}
-
              {baseUrl}
-
              popover
-
              projectId={project.id}
-
              hideLocalButton={isLocal(baseUrl.hostname) ||
-
                localProject !== "found"}
-
              disableLocalButton={$httpdStore.state !== "authenticated"}
-
              slot="popover" />
-
          </Popover>
+
        {#if !hideContextHelp && $experimental}
+
          {#if !localProject}
+
            <div
+
              style="display: flex; justify-content: center; align-items: center; height: 2rem;">
+
              <Loading small condensed />
+
            </div>
+
          {:else}
+
            <Popover popoverPositionBottom="0" popoverPositionLeft="3rem">
+
              <Button
+
                stylePadding="0 0.75rem"
+
                variant="background"
+
                title="Local node"
+
                slot="toggle"
+
                let:toggle
+
                on:click={toggle}>
+
                <IconSmall name="device" />
+
              </Button>
+

+
              <ContextHelp
+
                {localProject}
+
                {baseUrl}
+
                popover
+
                projectId={project.id}
+
                hideLocalButton={isLocal(baseUrl.hostname) ||
+
                  localProject !== "found"}
+
                disableLocalButton={$httpdStore.state !== "authenticated"}
+
                slot="popover" />
+
            </Popover>
+
          {/if}
        {/if}
-
      {/if}
-
    </div>
+
      </div>
+
    {/if}
+
    <!-- Footer -->
    {#if !collapsedOnly}
      <div class="sidebar-footer" style:flex-direction="row">
        <Button
@@ -348,8 +386,8 @@
          <div class="icon" class:expanded>
            <IconSmall name="chevron-left" />
          </div>
+
          <span class="collapse-label" class:expanded>Collapse</span>
        </Button>
-
        <div style:width="1.5rem" />
        <div class="horizontal-buttons" class:expanded>
          <Popover popoverPositionBottom="2.5rem" popoverPositionLeft="0">
            <Button
@@ -364,6 +402,8 @@

            <Settings slot="popover" />
          </Popover>
+
        </div>
+
        <div class="horizontal-buttons" class:expanded>
          <Popover popoverPositionBottom="2.5rem" popoverPositionLeft="0">
            <Button
              variant="background"
modified src/views/projects/Sidebar/ContextHelp.svelte
@@ -38,35 +38,49 @@
  }
  .title {
    padding-bottom: 0.75rem;
+
    text-wrap: nowrap;
+
    overflow: hidden;
+
  }
+
  .description {
+
    text-wrap: nowrap;
+
    overflow: hidden;
  }
</style>

<div class="help" class:popover>
  {#if $httpdStore.state === "stopped"}
    <div class="title txt-bold">Local node not connected</div>
-
    <div>Click the Connect button in the top right corner to get started.</div>
+
    <div class="description">
+
      Click the Connect button in the top right
+
      <br />
+
      corner to get started.
+
    </div>
  {:else if localProject === "notFound"}
    <div class="title txt-bold">Project not available locally</div>
-
    <div style:padding-bottom="0.5rem">
-
      This project hasn't been found on your local node. To get a local copy
-
      start seeding it using the following command.
+
    <div class="description" style:padding-bottom="0.5rem">
+
      This project hasn't been found on your local
+
      <br />
+
      node. To get a local copy start seeding it
+
      <br />
+
      using the following command.
    </div>
    <Command command={`rad seed ${projectId}`} />
  {:else if $httpdStore.state === "running" && localProject === "found"}
    <div class="title txt-bold">Not authenticated</div>
-
    <div>To make changes you need to authenticate yourself.</div>
-
    <div>
-
      Click the Authenticate button in the top right corner to get
-
      authenticated.
+
    <div class="description">To make changes you need to authenticate.</div>
+
    <div class="description">
+
      Click the Authenticate button in the top
+
      <br />
+
      right corner to get authenticated.
    </div>
  {:else if !isLocal(baseUrl.hostname) && localProject === "found"}
    <div class="title txt-bold">Read Only</div>
-
    <div>This is a read only preview hosted on</div>
+
    <div class="description">This is a read only preview hosted on</div>
    <ExternalLink href={baseUrl.hostname} />
  {/if}

  {#if !hideLocalButton}
-
    <div style:padding-top="1rem">
+
    <div class="txt-overflow" style:padding-top="1rem">
      <Link {route} disabled={disableLocalButton}>
        <Button size="large" styleWidth="100%" disabled={disableLocalButton}>
          <IconSmall name="device" />Make changes on your local node
added src/views/projects/Sidebar/ContextRepo.svelte
@@ -0,0 +1,129 @@
+
<script lang="ts">
+
  import type { BaseUrl, Node, Project } from "@httpd-client";
+

+
  import IconButton from "@app/components/IconButton.svelte";
+
  import IconSmall from "@app/components/IconSmall.svelte";
+
  import NodeId from "@app/components/NodeId.svelte";
+
  import Popover from "@app/components/Popover.svelte";
+
  import ScopePolicyExplainer from "@app/components/ScopePolicyExplainer.svelte";
+
  import { isLocal } from "@app/lib/utils";
+
  import { capitalize } from "lodash";
+

+
  export let disablePopovers: boolean = false;
+
  export let project: Project;
+
  export let baseUrl: BaseUrl;
+
  export let node: Node;
+

+
  let expandedNode = false;
+

+
  $: shortSeedingPolicy =
+
    node.config?.scope === "all" && node.config?.policy === "allow"
+
      ? "permissive"
+
      : "restrictive";
+
</script>
+

+
<style>
+
  .nids {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 1rem;
+
    padding: 0.5rem 0;
+
  }
+

+
  .node {
+
    display: flex;
+
    margin-top: 0.5rem;
+
    flex-direction: column;
+
    gap: 1rem;
+
    padding-top: 0.5rem;
+
  }
+
  .policies {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 0.5rem;
+
  }
+
  .item {
+
    display: flex;
+
    flex-wrap: nowrap;
+
    align-items: center;
+
    justify-content: space-between;
+
    gap: 0.5rem;
+
  }
+
  .no-wrap {
+
    text-wrap: nowrap;
+
  }
+
</style>
+

+
<div class="delegates txt-small">
+
  <div class="item" style:height="2rem">
+
    <div class="item">
+
      <IconSmall name="badge" />
+
      <span class="txt-bold">Delegates</span>
+
      <span class="txt-missing">{project.delegates.length}</span>
+
    </div>
+
    <div class="item">
+
      <IconSmall name="quorum" />
+
      <span class="txt-bold">
+
        {project.threshold}/{project.delegates.length}
+
      </span>
+
      {#if !disablePopovers}
+
        <Popover
+
          popoverPositionBottom="0"
+
          popoverPositionLeft="2rem"
+
          popoverPositionRight="-17rem">
+
          <IconButton slot="toggle" let:toggle on:click={toggle}>
+
            <IconSmall name="help" />
+
          </IconButton>
+

+
          <div slot="popover">
+
            {project.threshold} out of {project.delegates.length} delegates have
+
            to accept changes to be included in the canonical branch.
+
          </div>
+
        </Popover>
+
      {/if}
+
    </div>
+
  </div>
+
  <div class="nids">
+
    {#each project.delegates as { id: nodeId, alias }}
+
      <div style:width="fit-content">
+
        <NodeId {alias} {nodeId} stylePopoverPositionLeft="-0.8rem" />
+
      </div>
+
    {/each}
+
  </div>
+
</div>
+
<div class="txt-small node">
+
  <div class="item no-wrap txt-bold" style="justify-content: flex-start; ">
+
    {#if isLocal(baseUrl.hostname)}
+
      <IconSmall name="device" />Local Node
+
    {:else}
+
      <IconSmall name="seedling" />{baseUrl.hostname}
+
    {/if}
+
  </div>
+

+
  <div class="policies">
+
    <div class="item">
+
      <div class="item" style="justify-content: flex-start;">
+
        <IconButton on:click={() => (expandedNode = !expandedNode)}>
+
          <IconSmall name={`chevron-${expandedNode ? "down" : "right"}`} />
+
        </IconButton>
+
        <span class="no-wrap">Seeding Policy</span>
+
      </div>
+
      <div class="txt-bold">
+
        {capitalize(shortSeedingPolicy)}
+
      </div>
+
    </div>
+
    {#if expandedNode && node.config}
+
      <div style:padding-left="2.3rem">
+
        <ScopePolicyExplainer
+
          scope={node.config.scope}
+
          policy={node.config.policy} />
+
      </div>
+
    {/if}
+
    <div class="item txt-overflow" style:padding-top="0.5rem">
+
      Radicle version
+
      <span class="txt-missing txt-overflow txt-monospace" title={node.version}>
+
        {node.version}
+
      </span>
+
    </div>
+
  </div>
+
</div>
modified src/views/projects/Source.svelte
@@ -1,5 +1,5 @@
<script lang="ts">
-
  import type { BaseUrl, Project, Remote, Tree } from "@httpd-client";
+
  import type { BaseUrl, Node, Project, Remote, Tree } from "@httpd-client";
  import type { BlobResult } from "./router";
  import type { Route } from "@app/lib/router";

@@ -15,6 +15,7 @@
  import ProjectNameHeader from "./Source/ProjectNameHeader.svelte";

  export let baseUrl: BaseUrl;
+
  export let node: Node;
  export let commit: string;
  export let rawPath: (commit?: string) => string;
  export let blobResult: BlobResult;
@@ -130,7 +131,7 @@
  }
</style>

-
<Layout {baseUrl} {project} activeTab="source" stylePaddingBottom="0">
+
<Layout {node} {baseUrl} {project} activeTab="source" stylePaddingBottom="0">
  <ProjectNameHeader {project} {baseUrl} {seeding} slot="header" />

  <div style:margin="1rem 0 1rem 1rem" slot="subheader">
modified src/views/projects/router.ts
@@ -13,6 +13,7 @@ import type {
  DiffBlob,
  Issue,
  IssueState,
+
  Node,
  Patch,
  PatchState,
  Project,
@@ -113,6 +114,7 @@ export type ProjectLoadedRoute =
      resource: "project.source";
      params: {
        baseUrl: BaseUrl;
+
        node: Node;
        commit: string;
        project: Project;
        peers: Remote[];
@@ -130,6 +132,7 @@ export type ProjectLoadedRoute =
      resource: "project.history";
      params: {
        baseUrl: BaseUrl;
+
        node: Node;
        commit: string;
        project: Project;
        peers: Remote[];
@@ -145,6 +148,7 @@ export type ProjectLoadedRoute =
      resource: "project.commit";
      params: {
        baseUrl: BaseUrl;
+
        node: Node;
        project: Project;
        commit: Commit;
      };
@@ -153,6 +157,7 @@ export type ProjectLoadedRoute =
      resource: "project.issue";
      params: {
        baseUrl: BaseUrl;
+
        node: Node;
        project: Project;
        rawPath: (commit?: string) => string;
        issue: Issue;
@@ -162,6 +167,7 @@ export type ProjectLoadedRoute =
      resource: "project.issues";
      params: {
        baseUrl: BaseUrl;
+
        node: Node;
        project: Project;
        issues: Issue[];
        state: IssueState["status"];
@@ -171,6 +177,7 @@ export type ProjectLoadedRoute =
      resource: "project.newIssue";
      params: {
        baseUrl: BaseUrl;
+
        node: Node;
        project: Project;
        rawPath: (commit?: string) => string;
      };
@@ -179,6 +186,7 @@ export type ProjectLoadedRoute =
      resource: "project.patches";
      params: {
        baseUrl: BaseUrl;
+
        node: Node;
        project: Project;
        patches: Patch[];
        state: PatchState["status"];
@@ -188,6 +196,7 @@ export type ProjectLoadedRoute =
      resource: "project.patch";
      params: {
        baseUrl: BaseUrl;
+
        node: Node;
        project: Project;
        rawPath: (commit?: string) => string;
        patch: Patch;
@@ -270,6 +279,16 @@ export async function loadProjectRoute(
  previousLoaded: LoadedRoute,
): Promise<ProjectLoadedRoute | ErrorRoute | NotFoundRoute> {
  const api = new HttpdClient(route.node);
+
  let node: Node | undefined = undefined;
+
  if (
+
    "params" in previousLoaded &&
+
    "node" in previousLoaded.params &&
+
    route.node.hostname === previousLoaded.params.baseUrl.hostname
+
  ) {
+
    node = previousLoaded.params.node;
+
  } else {
+
    node = await api.getNode();
+
  }
  const rawPath = (commit?: string) =>
    `${route.node.scheme}://${route.node.hostname}:${route.node.port}/raw/${
      route.project
@@ -277,9 +296,9 @@ export async function loadProjectRoute(

  try {
    if (route.resource === "project.source") {
-
      return await loadTreeView(route, previousLoaded);
+
      return await loadTreeView(route, previousLoaded, node);
    } else if (route.resource === "project.history") {
-
      return await loadHistoryView(route);
+
      return await loadHistoryView(route, node);
    } else if (route.resource === "project.commit") {
      const [project, commit] = await Promise.all([
        api.project.getById(route.project),
@@ -290,28 +309,30 @@ export async function loadProjectRoute(
        resource: "project.commit",
        params: {
          baseUrl: route.node,
+
          node,
          project,
          commit,
        },
      };
    } else if (route.resource === "project.issue") {
-
      return await loadIssueView(route);
+
      return await loadIssueView(route, node);
    } else if (route.resource === "project.patch") {
-
      return await loadPatchView(route);
+
      return await loadPatchView(route, node);
    } else if (route.resource === "project.issues") {
-
      return await loadIssuesView(route);
+
      return await loadIssuesView(route, node);
    } else if (route.resource === "project.newIssue") {
      const project = await api.project.getById(route.project);
      return {
        resource: "project.newIssue",
        params: {
          baseUrl: route.node,
+
          node,
          project,
          rawPath,
        },
      };
    } else if (route.resource === "project.patches") {
-
      return await loadPatchesView(route);
+
      return await loadPatchesView(route, node);
    } else {
      return unreachable(route);
    }
@@ -330,6 +351,7 @@ export async function loadProjectRoute(

async function loadPatchesView(
  route: ProjectPatchesRoute,
+
  node: Node,
): Promise<ProjectLoadedRoute> {
  const api = new HttpdClient(route.node);
  const searchParams = new URLSearchParams(route.search || "");
@@ -348,6 +370,7 @@ async function loadPatchesView(
    resource: "project.patches",
    params: {
      baseUrl: route.node,
+
      node,
      patches,
      state,
      project,
@@ -357,6 +380,7 @@ async function loadPatchesView(

async function loadIssuesView(
  route: ProjectIssuesRoute,
+
  node: Node,
): Promise<ProjectLoadedRoute> {
  const api = new HttpdClient(route.node);
  const state = route.state || "open";
@@ -375,6 +399,7 @@ async function loadIssuesView(
    resource: "project.issues",
    params: {
      baseUrl: route.node,
+
      node,
      issues,
      state,
      project,
@@ -385,6 +410,7 @@ async function loadIssuesView(
async function loadTreeView(
  route: ProjectTreeRoute,
  previousLoaded: LoadedRoute,
+
  node: Node,
): Promise<ProjectLoadedRoute | NotFoundRoute> {
  const api = new HttpdClient(route.node);
  const rawPath = (commit?: string) =>
@@ -450,6 +476,7 @@ async function loadTreeView(
    resource: "project.source",
    params: {
      baseUrl: route.node,
+
      node,
      commit,
      project,
      peers: peers.filter(remote => Object.keys(remote.heads).length > 0),
@@ -516,6 +543,7 @@ async function loadBlob(
}
async function loadHistoryView(
  route: ProjectHistoryRoute,
+
  node: Node,
): Promise<ProjectLoadedRoute> {
  const api = new HttpdClient(route.node);

@@ -554,6 +582,7 @@ async function loadHistoryView(
    resource: "project.history",
    params: {
      baseUrl: route.node,
+
      node,
      commit: commitId,
      project,
      peers: peers.filter(remote => Object.keys(remote.heads).length > 0),
@@ -569,6 +598,7 @@ async function loadHistoryView(

async function loadIssueView(
  route: ProjectIssueRoute,
+
  node: Node,
): Promise<ProjectLoadedRoute> {
  const api = new HttpdClient(route.node);
  const rawPath = (commit?: string) =>
@@ -584,6 +614,7 @@ async function loadIssueView(
    resource: "project.issue",
    params: {
      baseUrl: route.node,
+
      node,
      project,
      rawPath,
      issue,
@@ -593,6 +624,7 @@ async function loadIssueView(

async function loadPatchView(
  route: ProjectPatchRoute,
+
  node: Node,
): Promise<ProjectLoadedRoute> {
  const api = new HttpdClient(route.node);
  const rawPath = (commit?: string) =>
@@ -658,6 +690,7 @@ async function loadPatchView(
    resource: "project.patch",
    params: {
      baseUrl: route.node,
+
      node,
      project,
      rawPath,
      patch,