Radish alpha
r
rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5
Radicle web interface
Radicle
Git
Tweak copyable IDs
Merged rudolfs opened 1 year ago

check check-visual check-unit-test check-http-client-unit-test check-http-server check-e2e check-build check-http πŸ‘‰ Preview πŸ‘‰ Workflow runs πŸ‘‰ Branch on GitHub

24 files changed +225 -269 adc6c6c2 β†’ 6d87823c
modified src/components/Comment.svelte
@@ -9,6 +9,7 @@
  import ExtendedTextarea from "@app/components/ExtendedTextarea.svelte";
  import IconButton from "@app/components/IconButton.svelte";
  import IconSmall from "@app/components/IconSmall.svelte";
+
  import Id from "@app/components/Id.svelte";
  import Markdown from "@app/components/Markdown.svelte";
  import NodeId from "@app/components/NodeId.svelte";
  import ReactionSelector from "@app/components/ReactionSelector.svelte";
@@ -136,15 +137,10 @@
    {/if}
    <div class="card-header" class:card-header-no-icon={isReply}>
      <slot class="icon" name="icon" />
-
      <NodeId
-
        stylePopoverPositionLeft="-13px"
-
        nodeId={authorId}
-
        alias={authorAlias} />
+
      <NodeId nodeId={authorId} alias={authorAlias} />
      <slot name="caption">{caption}</slot>
      {#if id}
-
        <span class="global-oid">
-
          {utils.formatObjectId(id)}
-
        </span>
+
        <Id {id} />
      {/if}
      <span class="timestamp" title={utils.absoluteTimestamp(timestamp)}>
        {utils.formatTimestamp(timestamp)}
deleted src/components/CopyableId.svelte
@@ -1,42 +0,0 @@
-
<script lang="ts">
-
  import type { SvelteComponent } from "svelte";
-
  import Clipboard from "@app/components/Clipboard.svelte";
-

-
  export let id: string;
-
  export let style: "commit" | "oid";
-

-
  let clipboard: SvelteComponent;
-
</script>
-

-
<style>
-
  .id {
-
    cursor: pointer;
-
    color: var(--color-foreground-emphasized);
-
    overflow-wrap: anywhere;
-
    display: flex;
-
    align-items: center;
-
    gap: 0.125rem;
-
    width: fit-content;
-
  }
-
  .id:hover {
-
    color: var(--color-foreground-emphasized-hover);
-
  }
-
</style>
-

-
<!-- svelte-ignore a11y-click-events-have-key-events -->
-
<div
-
  role="button"
-
  tabindex="0"
-
  on:click={() => {
-
    clipboard.copy();
-
  }}
-
  class="id">
-
  <span
-
    class="txt-overflow"
-
    class:global-commit={style === "commit"}
-
    class:global-oid={style === "oid"}
-
    style="color: inherit">
-
    <slot>{id}</slot>
-
  </span>
-
  <Clipboard bind:this={clipboard} text={id} />
-
</div>
modified src/components/HoverPopover.svelte
@@ -1,32 +1,26 @@
<script lang="ts">
  import debounce from "lodash/debounce";

-
  // eslint-disable-next-line @typescript-eslint/no-empty-function
-
  export let onShow: () => void = () => {};
-
  export let stylePopoverPositionLeft: string | undefined = undefined;
-
  export let stylePopoverPositionRight: string | undefined = undefined;
-
  export let stylePopoverPositionTop: string | undefined = undefined;
  export let stylePopoverPositionBottom: string | undefined = undefined;
-
  export let stylePopoverPadding: string | undefined = "1rem";
+
  export let stylePopoverPositionLeft: string | undefined = undefined;

  let visible: boolean = false;

  const setVisible = debounce((value: boolean) => {
    visible = value;
-
    if (visible) {
-
      onShow();
-
    }
-
  }, 500);
+
  }, 50);
</script>

<style>
  .container {
    position: relative;
+
    display: inline-block;
  }
  .popover {
    background: var(--color-background-float);
    border-radius: var(--border-radius-regular);
    border: 1px solid var(--color-border-hint);
+
    padding: 1rem;
    box-shadow: var(--elevation-low);
    position: absolute;
    z-index: 10;
@@ -42,19 +36,11 @@
    <slot name="toggle" />

    {#if visible}
-
      <!-- If this component is used inside a button (see `NodeId`, for example)
-
       we don’t want clicks in the popover to trigger button actions. So we
-
       stop propagation of click events. -->
-
      <!-- svelte-ignore a11y-click-events-have-key-events -->
-
      <!-- svelte-ignore a11y-no-static-element-interactions -->
-
      <div style:position="absolute" on:click|stopPropagation>
+
      <div style:position="absolute">
        <div
          class="popover"
-
          style:padding={stylePopoverPadding}
          style:left={stylePopoverPositionLeft}
-
          style:right={stylePopoverPositionRight}
-
          style:bottom={stylePopoverPositionBottom}
-
          style:top={stylePopoverPositionTop}>
+
          style:bottom={stylePopoverPositionBottom}>
          <slot name="popover" />
        </div>
      </div>
added src/components/Id.svelte
@@ -0,0 +1,116 @@
+
<script lang="ts">
+
  import type { ComponentProps } from "svelte";
+

+
  import { debounce } from "lodash";
+

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

+
  import IconSmall from "./IconSmall.svelte";
+

+
  export let id: string;
+
  export let clipboard: string = id;
+
  export let shorten: boolean = true;
+
  export let style: "oid" | "commit" | "none" = "oid";
+
  export let subject: string | undefined = undefined;
+
  export let ariaLabel: string | undefined = undefined;
+

+
  let icon: ComponentProps<IconSmall>["name"] = "clipboard";
+
  const text = subject ? `Click to copy ${subject}` : "Click to copy";
+
  let tooltip = text;
+

+
  const restoreIcon = debounce(() => {
+
    icon = "clipboard";
+
    tooltip = text;
+
    visible = false;
+
  }, 1000);
+

+
  async function copy() {
+
    await toClipboard(clipboard);
+
    icon = "checkmark";
+
    tooltip = "Copied to clipboard";
+
    restoreIcon();
+
  }
+

+
  let visible: boolean = false;
+
  export let debounceTimeout = 50;
+

+
  const setVisible = debounce((value: boolean) => {
+
    visible = value;
+
  }, debounceTimeout);
+
</script>
+

+
<style>
+
  .container {
+
    position: relative;
+
    display: inline-block;
+
  }
+
  .popover {
+
    position: absolute;
+
    bottom: 1.5rem;
+
    left: 0;
+
    display: flex;
+
    align-items: center;
+
    flex-direction: row;
+
    gap: 0.5rem;
+
    justify-content: center;
+
    z-index: 10;
+
    background: var(--color-background-float);
+
    color: var(--color-foreground-default);
+
    border: 1px solid var(--color-border-hint);
+
    border-radius: var(--border-radius-small);
+
    box-shadow: var(--elevation-low);
+
    font-family: var(--font-family-sans-serif);
+
    font-size: var(--font-size-small);
+
    font-weight: var(--font-weight-regular);
+
    white-space: nowrap;
+
    padding: 0.25rem 0.5rem;
+
  }
+
  .target-commit:hover {
+
    color: var(--color-foreground-contrast);
+
  }
+
  .target-oid:hover {
+
    color: var(--color-foreground-emphasized-hover);
+
  }
+
</style>
+

+
<div class="container">
+
  <div
+
    role="button"
+
    tabindex="0"
+
    on:mouseenter={() => {
+
      setVisible(true);
+
    }}
+
    on:mouseleave={() => {
+
      setVisible(false);
+
    }}>
+
    <!-- svelte-ignore a11y-click-events-have-key-events -->
+
    <div
+
      class="target-{style} global-{style}"
+
      style:cursor="pointer"
+
      aria-label={ariaLabel}
+
      on:click={async () => {
+
        await copy();
+
        setVisible(true);
+
      }}
+
      role="button"
+
      tabindex="0">
+
      <slot>
+
        {#if shorten}
+
          {formatObjectId(id)}
+
        {:else}
+
          {id}
+
        {/if}
+
      </slot>
+
    </div>
+

+
    {#if visible}
+
      <div style:position="absolute">
+
        <div class="popover">
+
          <IconSmall name={icon} />
+
          {tooltip}
+
        </div>
+
      </div>
+
    {/if}
+
  </div>
+
</div>
modified src/components/NodeId.svelte
@@ -2,13 +2,10 @@
  import { formatNodeId } from "@app/lib/utils";

  import Avatar from "./Avatar.svelte";
-
  import HoverPopover from "./HoverPopover.svelte";
-
  import CopyableId from "./CopyableId.svelte";
+
  import Id from "./Id.svelte";

  export let nodeId: string;
  export let alias: string | undefined = undefined;
-
  export let large: boolean = false;
-
  export let stylePopoverPositionLeft = "-4.5rem";
</script>

<style>
@@ -22,32 +19,10 @@
    font-weight: var(--font-weight-semibold);
    font-size: var(--font-size-small);
  }
-
  .large {
-
    height: 1.25rem;
-
    gap: 0.5rem;
-
  }
-
  .popover-avatar {
-
    height: 1rem;
-
    display: flex;
-
    align-items: center;
-
    gap: 0.5rem;
-
    font-family: var(--font-family-monospace);
-
    font-weight: var(--font-weight-semibold);
-
    font-size: var(--font-size-small);
-
    white-space: nowrap;
-
  }
-
  .popover-container {
-
    display: flex;
-
    align-items: center;
-
    gap: 1rem;
-
  }
</style>

-
<HoverPopover
-
  {stylePopoverPositionLeft}
-
  stylePopoverPadding="0.5rem 0.5rem 0.5rem 0.75rem"
-
  stylePopoverPositionTop="-4.5rem">
-
  <div slot="toggle" class="avatar-alias" class:large>
+
<Id id={nodeId} subject={formatNodeId(nodeId)} style="none">
+
  <div class="avatar-alias">
    <Avatar {nodeId} />
    {#if alias}
      {alias}
@@ -55,19 +30,4 @@
      {formatNodeId(nodeId)}
    {/if}
  </div>
-

-
  <div slot="popover">
-
    <div class="popover-container">
-
      <div class="popover-avatar">
-
        <Avatar {nodeId} />
-
        {#if alias}
-
          {alias}
-
        {/if}
-
      </div>
-

-
      <CopyableId id={nodeId} style="oid">
-
        {formatNodeId(nodeId)}
-
      </CopyableId>
-
    </div>
-
  </div>
-
</HoverPopover>
+
</Id>
modified src/views/nodes/View.svelte
@@ -11,9 +11,9 @@
  import { isDelegate } from "@app/lib/roles";

  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 Id from "@app/components/Id.svelte";
  import Loading from "@app/components/Loading.svelte";
  import Popover from "@app/components/Popover.svelte";
  import ProjectCard from "@app/components/ProjectCard.svelte";
@@ -128,25 +128,27 @@
            {#each externalAddresses as address}
              <!-- If there are externalAddresses this is probably a remote node -->
              <!-- in that case, we show all the defined externalAddresses as a listing -->
-
              <CopyableId id={`${nid}@${address}`} style="oid">
-
                {truncateId(nid)}@{address}
-
              </CopyableId>
+
              <Id
+
                ariaLabel="node-id"
+
                shorten={false}
+
                id="{truncateId(nid)}@{address}"
+
                clipboard={`${nid}@${address}`} />
            {:else}
              <!-- else this is probably a local node -->
              <!-- So we show only the nid -->
-
              <CopyableId id={nid} style="oid">
-
                <div class="global-hide-on-small-desktop-up">
-
                  {truncateId(nid)}
-
                </div>
-
                <div class="global-hide-on-mobile-down">
-
                  {nid}
-
                </div>
-
              </CopyableId>
+
              <div class="global-hide-on-small-desktop-up">
+
                <Id ariaLabel="node-id" id={truncateId(nid)} shorten={false} />
+
              </div>
+
              <div class="global-hide-on-mobile-down">
+
                <Id ariaLabel="node-id" id={nid} shorten={false} />
+
              </div>
            {/each}
          </div>
-
          <div class="version">
-
            {version}
-
          </div>
+
          <Id ariaLabel="version" id={version} shorten={false} style="none">
+
            <div class="version">
+
              {version}
+
            </div>
+
          </Id>
        </div>
      </div>

modified src/views/projects/Cob/CobCommitTeaser.svelte
@@ -3,11 +3,11 @@

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

-
  import CommitLink from "@app/views/projects/components/CommitLink.svelte";
  import CompactCommitAuthorship from "@app/components/CompactCommitAuthorship.svelte";
  import ExpandButton from "@app/components/ExpandButton.svelte";
  import IconButton from "@app/components/IconButton.svelte";
  import IconSmall from "@app/components/IconSmall.svelte";
+
  import Id from "@app/components/Id.svelte";
  import InlineMarkdown from "@app/components/InlineMarkdown.svelte";
  import Link from "@app/components/Link.svelte";

@@ -88,7 +88,7 @@
    {/if}
    <div class="global-hide-on-small-desktop-up">
      <CompactCommitAuthorship {commit}>
-
        <CommitLink {baseUrl} {projectId} commitId={commit.id} />
+
        <Id id={commit.id} style="commit" />
      </CompactCommitAuthorship>
    </div>
  </div>
@@ -96,7 +96,7 @@
    <div style="display: flex; gap: 0.5rem; height: 21px; align-items: center;">
      <div class="global-hide-on-mobile-down">
        <CompactCommitAuthorship {commit}>
-
          <CommitLink {baseUrl} {projectId} commitId={commit.id} />
+
          <Id id={commit.id} style="commit" />
        </CompactCommitAuthorship>
      </div>
      <IconButton title="Browse repo at this commit">
modified src/views/projects/Cob/CobHeader.svelte
@@ -11,7 +11,6 @@
    flex-wrap: wrap;
    gap: 0.5rem;
    font-size: var(--font-size-small);
-
    font-family: var(--font-family-monospace);
  }
  .summary {
    display: flex;
modified src/views/projects/Cob/Revision.svelte
@@ -18,7 +18,6 @@

  import CobCommitTeaser from "@app/views/projects/Cob/CobCommitTeaser.svelte";
  import CommentComponent from "@app/components/Comment.svelte";
-
  import CommitLink from "@app/views/projects/components/CommitLink.svelte";
  import DiffStatBadge from "@app/components/DiffStatBadge.svelte";
  import DropdownList from "@app/components/DropdownList.svelte";
  import DropdownListItem from "@app/components/DropdownList/DropdownListItem.svelte";
@@ -35,6 +34,7 @@
  import ReactionSelector from "@app/components/ReactionSelector.svelte";
  import Reactions from "@app/components/Reactions.svelte";
  import Thread from "@app/components/Thread.svelte";
+
  import Id from "@app/components/Id.svelte";

  export let baseUrl: BaseUrl;
  export let initiallyExpanded: boolean = false;
@@ -300,7 +300,7 @@
        <ExpandButton {expanded} on:toggle={() => (expanded = !expanded)} />
        <span>
          Revision
-
          <span class="global-oid">{utils.formatObjectId(revisionId)}</span>
+
          <Id id={revisionId} />
        </span>
      </div>
      <div class="revision-data">
@@ -409,21 +409,16 @@
              style:padding="0 0.375rem">
              <IconSmall name="patch" />
            </div>
-
            <NodeId
-
              stylePopoverPositionLeft="-13px"
-
              nodeId={revisionAuthor.id}
-
              alias={revisionAuthor.alias} />
+
            <NodeId nodeId={revisionAuthor.id} alias={revisionAuthor.alias} />
            {#if patchId === revisionId}
              opened this patch on base
-
              <CommitLink {baseUrl} {projectId} commitId={revisionBase} />
+
              <Id id={revisionBase} style="commit" />
            {:else}
              updated to
-
              <span class="global-oid">
-
                {utils.formatObjectId(revisionId)}
-
              </span>
+
              <Id id={revisionId} />
              {#if previousRevBase && previousRevBase !== revisionBase}
                with base
-
                <CommitLink {baseUrl} {projectId} commitId={revisionBase} />
+
                <Id id={revisionBase} style="commit" />
              {/if}
            {/if}
            <span
@@ -556,20 +551,14 @@
              </div>

              <NodeId
-
                stylePopoverPositionLeft="-13px"
                nodeId={element.inner.author.id}
                alias={element.inner.author.alias}>
              </NodeId>

              merged revision
-
              <span class="global-oid">
-
                {utils.formatObjectId(element.inner.revision)}
-
              </span>
+
              <Id id={element.inner.revision} />
              at commit
-
              <CommitLink
-
                {baseUrl}
-
                {projectId}
-
                commitId={element.inner.commit} />
+
              <Id id={element.inner.commit} style="commit" />
              <span
                class="timestamp"
                title={utils.absoluteTimestamp(revisionTimestamp)}>
@@ -593,9 +582,7 @@
              body={review.summary ?? ""}>
              <div slot="caption">
                {formatVerdict(review.verdict)}
-
                <span class="global-oid">
-
                  {utils.formatObjectId(revisionId)}
-
                </span>
+
                <Id id={revisionId} />
              </div>
              <div slot="icon" style:color={verdictIconColor(review.verdict)}>
                {#if review.verdict === "accept"}
modified src/views/projects/Commit.svelte
@@ -1,17 +1,15 @@
<script lang="ts">
  import type { BaseUrl, Commit, Node, Project } from "@http-client";

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

  import Button from "@app/components/Button.svelte";
  import Changeset from "@app/views/projects/Changeset.svelte";
  import CommitAuthorship from "@app/views/projects/Commit/CommitAuthorship.svelte";
-
  import CopyableId from "@app/components/CopyableId.svelte";
  import IconSmall from "@app/components/IconSmall.svelte";
  import InlineMarkdown from "@app/components/InlineMarkdown.svelte";
  import Layout from "./Layout.svelte";
  import Link from "@app/components/Link.svelte";
  import Share from "./Share.svelte";
+
  import Id from "@app/components/Id.svelte";

  export let baseUrl: BaseUrl;
  export let node: Node;
@@ -73,9 +71,7 @@
          </div>
        </span>
        <CommitAuthorship {header}>
-
          <CopyableId id={header.id} style="commit">
-
            {formatCommit(header.id)}
-
          </CopyableId>
+
          <Id id={header.id} style="commit" ariaLabel="commit-id" />
        </CommitAuthorship>
      </div>
      {#if header.description}
modified src/views/projects/Commit/CommitTeaser.svelte
@@ -4,12 +4,12 @@
  import { twemoji } from "@app/lib/utils";

  import CommitAuthorship from "./CommitAuthorship.svelte";
-
  import CommitLink from "@app/views/projects/components/CommitLink.svelte";
  import ExpandButton from "@app/components/ExpandButton.svelte";
  import IconButton from "@app/components/IconButton.svelte";
  import IconSmall from "@app/components/IconSmall.svelte";
  import InlineMarkdown from "@app/components/InlineMarkdown.svelte";
  import Link from "@app/components/Link.svelte";
+
  import Id from "@app/components/Id.svelte";

  export let baseUrl: BaseUrl;
  export let commit: CommitHeader;
@@ -104,7 +104,7 @@
      </div>
    {/if}
    <CommitAuthorship header={commit}>
-
      <CommitLink {baseUrl} {projectId} commitId={commit.id} />
+
      <Id id={commit.id} style="commit" />
    </CommitAuthorship>
  </div>
  <div class="right">
modified src/views/projects/Issue.svelte
@@ -32,11 +32,11 @@
  import CobHeader from "@app/views/projects/Cob/CobHeader.svelte";
  import CobStateButton from "@app/views/projects/Cob/CobStateButton.svelte";
  import CommentToggleInput from "@app/components/CommentToggleInput.svelte";
-
  import CopyableId from "@app/components/CopyableId.svelte";
  import Embeds from "@app/views/projects/Cob/Embeds.svelte";
  import ErrorModal from "@app/modals/ErrorModal.svelte";
  import ExtendedTextarea from "@app/components/ExtendedTextarea.svelte";
  import IconSmall from "@app/components/IconSmall.svelte";
+
  import Id from "@app/components/Id.svelte";
  import InlineMarkdown from "@app/components/InlineMarkdown.svelte";
  import LabelInput from "./Cob/LabelInput.svelte";
  import Layout from "./Layout.svelte";
@@ -547,14 +547,9 @@
              {issue.state.reason}
            </Badge>
          {/if}
-
          <NodeId
-
            stylePopoverPositionLeft="-13px"
-
            nodeId={issue.author.id}
-
            alias={issue.author.alias} />
+
          <NodeId nodeId={issue.author.id} alias={issue.author.alias} />
          opened
-
          <CopyableId id={issue.id} style="oid">
-
            {utils.formatObjectId(issue.id)}
-
          </CopyableId>
+
          <Id id={issue.id} />
          <span title={utils.absoluteTimestamp(issue.discussion[0].timestamp)}>
            {utils.formatTimestamp(issue.discussion[0].timestamp)}
          </span>
modified src/views/projects/Issue/IssueTeaser.svelte
@@ -1,11 +1,7 @@
<script lang="ts">
  import type { BaseUrl, Issue } from "@http-client";

-
  import {
-
    absoluteTimestamp,
-
    formatObjectId,
-
    formatTimestamp,
-
  } from "@app/lib/utils";
+
  import { absoluteTimestamp, formatTimestamp } from "@app/lib/utils";

  import IconSmall from "@app/components/IconSmall.svelte";
  import InlineMarkdown from "@app/components/InlineMarkdown.svelte";
@@ -14,6 +10,7 @@

  import CommentCounter from "../CommentCounter.svelte";
  import Labels from "../Cob/Labels.svelte";
+
  import Id from "@app/components/Id.svelte";

  export let baseUrl: BaseUrl;
  export let issue: Issue;
@@ -123,12 +120,9 @@
      {/if}
      <div
        style="display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;">
-
        <NodeId
-
          stylePopoverPositionLeft="-13px"
-
          nodeId={issue.author.id}
-
          alias={issue.author.alias} />
+
        <NodeId nodeId={issue.author.id} alias={issue.author.alias} />
        opened
-
        <span class="global-oid">{formatObjectId(issue.id)}</span>
+
        <Id id={issue.id} />
        <span title={absoluteTimestamp(issue.discussion[0].timestamp)}>
          {formatTimestamp(issue.discussion[0].timestamp)}
        </span>
modified src/views/projects/Patch.svelte
@@ -52,12 +52,13 @@
  import * as role from "@app/lib/roles";
  import * as router from "@app/lib/router";
  import * as utils from "@app/lib/utils";
-
  import { experimental } from "@app/lib/appearance";
  import capitalize from "lodash/capitalize";
  import isEqual from "lodash/isEqual";
  import partial from "lodash/partial";
  import uniqBy from "lodash/uniqBy";
  import { HttpdClient } from "@http-client";
+
  import { closeFocused } from "@app/components/Popover.svelte";
+
  import { experimental } from "@app/lib/appearance";
  import { httpdStore } from "@app/lib/httpd";
  import { parseEmbedIntoMap } from "@app/lib/file";

@@ -68,12 +69,12 @@
  import CobStateButton from "@app/views/projects/Cob/CobStateButton.svelte";
  import CommentToggleInput from "@app/components/CommentToggleInput.svelte";
  import CompareButton from "@app/views/projects/Patch/CompareButton.svelte";
-
  import CopyableId from "@app/components/CopyableId.svelte";
  import DiffStatBadge from "@app/components/DiffStatBadge.svelte";
  import Embeds from "@app/views/projects/Cob/Embeds.svelte";
  import ErrorModal from "@app/modals/ErrorModal.svelte";
  import ExtendedTextarea from "@app/components/ExtendedTextarea.svelte";
  import IconSmall from "@app/components/IconSmall.svelte";
+
  import Id from "@app/components/Id.svelte";
  import InlineMarkdown from "@app/components/InlineMarkdown.svelte";
  import LabelInput from "@app/views/projects/Cob/LabelInput.svelte";
  import Layout from "@app/views/projects/Layout.svelte";
@@ -89,7 +90,6 @@
  import RevisionSelector from "@app/views/projects/Patch/RevisionSelector.svelte";
  import Share from "@app/views/projects/Share.svelte";
  import TextInput from "@app/components/TextInput.svelte";
-
  import { closeFocused } from "@app/components/Popover.svelte";

  export let baseUrl: BaseUrl;
  export let node: Node;
@@ -775,14 +775,9 @@
              insertions={stats.insertions}
              deletions={stats.deletions} />
          </Link>
-
          <NodeId
-
            stylePopoverPositionLeft="-13px"
-
            nodeId={patch.author.id}
-
            alias={patch.author.alias} />
+
          <NodeId nodeId={patch.author.id} alias={patch.author.alias} />
          opened
-
          <CopyableId id={patch.id} style="oid">
-
            {utils.formatObjectId(patch.id)}
-
          </CopyableId>
+
          <Id id={patch.id} />
          <span title={utils.absoluteTimestamp(patch.revisions[0].timestamp)}>
            {utils.formatTimestamp(patch.revisions[0].timestamp)}
          </span>
modified src/views/projects/Patch/PatchTeaser.svelte
@@ -2,20 +2,17 @@
  import type { BaseUrl } from "@http-client";
  import type { Patch } from "@http-client";

-
  import {
-
    absoluteTimestamp,
-
    formatObjectId,
-
    formatTimestamp,
-
  } from "@app/lib/utils";
+
  import { absoluteTimestamp, formatTimestamp } from "@app/lib/utils";

  import IconSmall from "@app/components/IconSmall.svelte";
  import InlineMarkdown from "@app/components/InlineMarkdown.svelte";
  import Link from "@app/components/Link.svelte";
  import NodeId from "@app/components/NodeId.svelte";

-
  import Labels from "../Cob/Labels.svelte";
-
  import DiffStatBadgeLoader from "../DiffStatBadgeLoader.svelte";
  import CommentCounter from "../CommentCounter.svelte";
+
  import DiffStatBadgeLoader from "../DiffStatBadgeLoader.svelte";
+
  import Id from "@app/components/Id.svelte";
+
  import Labels from "../Cob/Labels.svelte";

  export let projectId: string;
  export let baseUrl: BaseUrl;
@@ -137,17 +134,12 @@
        {/if}
        <div
          style="display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;">
-
          <NodeId
-
            stylePopoverPositionLeft="-13px"
-
            nodeId={patch.author.id}
-
            alias={patch.author.alias} />
+
          <NodeId nodeId={patch.author.id} alias={patch.author.alias} />
          {patch.revisions.length > 1 ? "updated" : "opened"}
-
          <span class="global-oid">{formatObjectId(patch.id)}</span>
+
          <Id id={patch.id} />
          {#if patch.revisions.length > 1}
            <span class="global-hide-on-mobile-down">
-
              to <span class="global-oid">
-
                {formatObjectId(patch.revisions[patch.revisions.length - 1].id)}
-
              </span>
+
              to <Id id={patch.revisions[patch.revisions.length - 1].id} />
            </span>
          {/if}
          <span title={absoluteTimestamp(latestRevision.timestamp)}>
modified src/views/projects/Share.svelte
@@ -61,7 +61,12 @@
    <ShareButton slot="popover" />
  </Popover>
{:else}
-
  <Button variant="outline" size="regular" on:click={copy}>
+
  <Button
+
    variant="outline"
+
    size="regular"
+
    on:click={async () => {
+
      await copy();
+
    }}>
    <IconSmall name={shareIcon} />
    <span class="global-hide-on-small-desktop-down">Copy link</span>
  </Button>
modified src/views/projects/Sidebar/ContextRepo.svelte
@@ -1,13 +1,14 @@
<script lang="ts">
  import type { BaseUrl, Node, Project } from "@http-client";

+
  import { capitalize } from "lodash";
+
  import { isLocal } from "@app/lib/utils";
+

  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;
@@ -86,7 +87,7 @@
  <div class="nids">
    {#each project.delegates as { id: nodeId, alias }}
      <div style:width="fit-content">
-
        <NodeId {alias} {nodeId} stylePopoverPositionLeft="-0.8rem" />
+
        <NodeId {alias} {nodeId} />
      </div>
    {/each}
  </div>
@@ -119,11 +120,5 @@
          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/PeerSelector.svelte
@@ -6,15 +6,14 @@
  import { formatNodeId } from "@app/lib/utils";
  import { httpdStore } from "@app/lib/httpd";

-
  import NodeId from "@app/components/NodeId.svelte";
+
  import Avatar from "@app/components/Avatar.svelte";
  import Badge from "@app/components/Badge.svelte";
+
  import Button from "@app/components/Button.svelte";
  import DropdownList from "@app/components/DropdownList.svelte";
  import DropdownListItem from "@app/components/DropdownList/DropdownListItem.svelte";
-
  import Popover from "@app/components/Popover.svelte";
  import IconSmall from "@app/components/IconSmall.svelte";
  import Link from "@app/components/Link.svelte";
-
  import Button from "@app/components/Button.svelte";
-
  import Avatar from "@app/components/Avatar.svelte";
+
  import Popover from "@app/components/Popover.svelte";

  export let peers: Array<{ remote: Remote; selected: boolean; route: Route }>;
  export let project: Project;
@@ -48,22 +47,27 @@
    popoverPositionTop="2.5rem"
    popoverBorderRadius="var(--border-radius-small)">
    <Button
+
      ariaLabel="Change peer"
      slot="toggle"
      let:expanded
      let:toggle
      styleBorderRadius="var(--border-radius-tiny) 0 0 var(--border-radius-tiny)"
      on:click={toggle}
-
      title="Change peer"
+
      title={selectedPeer ? formatNodeId(selectedPeer.id) : "Change peer"}
      disabled={!peers}>
      {#if !selectedPeer}
        <IconSmall name="delegate" />
      {/if}

      {#if selectedPeer}
-
        <NodeId
-
          nodeId={selectedPeer.id}
-
          alias={selectedPeer.alias}
-
          stylePopoverPositionLeft="-0.75rem" />
+
        <div style:height="1rem">
+
          <Avatar nodeId={selectedPeer.id} />
+
        </div>
+
        <span
+
          style:font-family="var(--font-family-monospace)"
+
          class:no-alias={!selectedPeer.alias}>
+
          {selectedPeer.alias || formatNodeId(selectedPeer.id)}
+
        </span>
        {#if selectedPeer.delegate}
          <Badge size="tiny" variant="delegate">
            <IconSmall name="badge" />
modified src/views/projects/Source/ProjectNameHeader.svelte
@@ -5,8 +5,8 @@

  import Badge from "@app/components/Badge.svelte";
  import CloneButton from "@app/views/projects/Header/CloneButton.svelte";
-
  import CopyableId from "@app/components/CopyableId.svelte";
  import IconSmall from "@app/components/IconSmall.svelte";
+
  import Id from "@app/components/Id.svelte";
  import InlineMarkdown from "@app/components/InlineMarkdown.svelte";
  import Link from "@app/components/Link.svelte";
  import SeedButton from "@app/views/projects/Header/SeedButton.svelte";
@@ -90,7 +90,7 @@
    </div>
  </div>
  <div class="id">
-
    <CopyableId id={project.id} style="oid" />
+
    <Id shorten={false} id={project.id} ariaLabel="project-id" />
  </div>
</div>
<div class="description" use:twemoji>
deleted src/views/projects/components/CommitLink.svelte
@@ -1,22 +0,0 @@
-
<script lang="ts">
-
  import type { BaseUrl } from "@http-client";
-

-
  import { formatCommit } from "@app/lib/utils";
-
  import Link from "@app/components/Link.svelte";
-

-
  export let baseUrl: BaseUrl;
-
  export let projectId: string;
-
  export let commitId: string;
-
</script>
-

-
<Link
-
  route={{
-
    resource: "project.commit",
-
    node: baseUrl,
-
    project: projectId,
-
    commit: commitId,
-
  }}>
-
  <span class="global-commit">
-
    {formatCommit(commitId)}
-
  </span>
-
</Link>
modified tests/e2e/clipboard.spec.ts
@@ -33,7 +33,7 @@ test("copy to clipboard", async ({ page, browserName, context }) => {

  // Project ID.
  {
-
    await page.locator(".id > .clipboard").click();
+
    await page.getByLabel("project-id").click();
    const clipboardContent = await page.evaluate<string>(
      "navigator.clipboard.readText()",
    );
@@ -63,7 +63,7 @@ test("copy to clipboard", async ({ page, browserName, context }) => {
  await page.goto("/nodes/radicle.local");
  // Node address.
  {
-
    await page.locator(".clipboard").first().click();
+
    await page.getByRole("button", { name: "node-id" }).first().click();
    await expectClipboard(`${nodeRemote}`, page);
  }

modified tests/e2e/project.spec.ts
@@ -288,13 +288,13 @@ test("peer and branch switching", async ({ page }) => {

  // Alice's peer.
  {
-
    await page.getByTitle("Change peer").click();
+
    await page.getByLabel("Change peer").click();
    await page
      .getByRole("link", {
        name: "alice delegate",
      })
      .click();
-
    await expect(page.getByTitle("Change peer")).toHaveText("alice Delegate");
+
    await expect(page.getByLabel("Change peer")).toHaveText("alice Delegate");

    // Default `main` branch.
    {
@@ -356,8 +356,8 @@ test("peer and branch switching", async ({ page }) => {
  {
    await page.getByRole("link", { name: "source-browsing" }).nth(1).click();

-
    await expect(page.getByTitle("Change peer")).not.toContainText("alice");
-
    await expect(page.getByTitle("Change peer")).not.toContainText("bob");
+
    await expect(page.getByLabel("Change peer")).not.toContainText("alice");
+
    await expect(page.getByLabel("Change peer")).not.toContainText("bob");

    await expect(page.getByTitle("Change branch")).toBeVisible();
    await expect(
@@ -372,10 +372,10 @@ test("peer and branch switching", async ({ page }) => {

  // Bob's peer.
  {
-
    await page.getByTitle("Change peer").click();
+
    await page.getByLabel("Change peer").click();
    await page.getByRole("link", { name: "bob" }).click();
-
    await expect(page.getByTitle("Change peer")).toContainText("bob");
-
    await expect(page.getByTitle("Change peer")).not.toHaveText("delegate");
+
    await expect(page.getByLabel("Change peer")).toContainText("bob");
+
    await expect(page.getByLabel("Change peer")).not.toHaveText("delegate");

    // Default `main` branch.
    {
@@ -402,7 +402,7 @@ test("peer and branch switching", async ({ page }) => {
test("only one modal can be open at a time", async ({ page }) => {
  await page.goto(sourceBrowsingUrl);

-
  await page.getByTitle("Change peer").click();
+
  await page.getByLabel("Change peer").click();
  await page
    .getByRole("link", {
      name: "alice delegate",
@@ -427,7 +427,7 @@ test("only one modal can be open at a time", async ({ page }) => {
  await expect(page.getByText("bob")).not.toBeVisible();
  await expect(page.getByText("feature/branch")).toBeVisible();

-
  await page.getByTitle("Change peer").click();
+
  await page.getByLabel("Change peer").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/project/commit.spec.ts
@@ -13,7 +13,7 @@ const commitUrl = `${sourceBrowsingUrl}/commits/${bobHead}`;

test("navigation from commit list", async ({ page }) => {
  await page.goto(sourceBrowsingUrl);
-
  await page.getByTitle("Change peer").click();
+
  await page.getByLabel("Change peer").click();
  await page.getByRole("link", { name: "bob" }).click();
  await page
    .getByRole("link", { name: `Commits ${bobMainCommitCount}` })
@@ -43,9 +43,7 @@ test("modified file", async ({ page }) => {
  // Commit header.
  {
    await expect(page.getByText("Update readme")).toBeVisible();
-
    await expect(
-
      page.getByRole("button", { name: shortBobHead }),
-
    ).toBeVisible();
+
    await expect(page.getByLabel("commit-id")).toHaveText(shortBobHead);
  }

  // Diff header.
modified tests/e2e/project/commits.spec.ts
@@ -20,14 +20,14 @@ test("peer and branch switching", async ({ page }) => {

  // Alice's peer.
  {
-
    await page.getByTitle("Change peer").click();
+
    await page.getByLabel("Change peer").click();
    await page
      .getByRole("link", {
        name: "alice delegate",
      })
      .click();

-
    await expect(page.getByTitle("Change peer")).toHaveText("alice Delegate");
+
    await expect(page.getByLabel("Change peer")).toHaveText("alice Delegate");

    await expect(page.getByText("Thursday, November 17, 2022")).toBeVisible();
    await expect(page.locator(".list .teaser")).toHaveCount(
@@ -65,10 +65,10 @@ test("peer and branch switching", async ({ page }) => {

  // Bob's peer.
  {
-
    await page.getByTitle("Change peer").click();
+
    await page.getByLabel("Change peer").click();
    await page.getByRole("link", { name: "bob" }).click();

-
    await expect(page.getByTitle("Change peer")).toContainText("bob");
+
    await expect(page.getByLabel("Change peer")).toContainText("bob");

    await expect(page.getByText("Wednesday, December 21, 2022")).toBeVisible();
    await expect(page.locator(".list").first().locator(".teaser")).toHaveCount(
@@ -155,9 +155,9 @@ test("relative timestamps", async ({ page }) => {
    .getByRole("link", { name: `Commits ${aliceMainCommitCount}` })
    .click();

-
  await page.getByTitle("Change peer").click();
+
  await page.getByLabel("Change peer").click();
  await page.getByRole("link", { name: "bob" }).click();
-
  await expect(page.getByTitle("Change peer")).toHaveText("bob");
+
  await expect(page.getByLabel("Change peer")).toHaveText("bob");
  const latestCommit = page.locator(".teaser").first();
  await expect(latestCommit).toContainText(
    `Bob Belcher committed ${shortBobHead} now`,