Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Two column layout refactor
Rūdolfs Ošiņš committed 2 years ago
commit e194de4218866d202508d15ccd604390f9467e7c
parent b3e212ff5bea3bf07069812df89e4ae67db93e4e
30 files changed +697 -602
modified public/index.css
@@ -9,9 +9,6 @@
  --border-radius-regular: 8px;
  --border-radius-round: 10rem;

-
  --content-max-width: 1920px;
-
  --content-min-width: 400px;
-

  --scrollbar-width: 0.5rem;

  --button-regular-height: 2.5rem;
@@ -21,13 +18,11 @@

html {
  height: 100%;
-
  overflow-y: scroll;
  -ms-overflow-style: scrollbar;
  -webkit-tap-highlight-color: transparent;
}

body {
-
  min-width: var(--content-min-width);
  height: 100%;
  margin: 0;
  padding: 0;
@@ -64,10 +59,6 @@ body::-webkit-scrollbar-thumb {
  background-color: var(--color-fill-separator);
}

-
main {
-
  display: block;
-
}
-

a {
  color: inherit;
  text-decoration: none;
modified src/App/AppLayout.svelte
@@ -10,11 +10,28 @@
    flex-direction: column;
    height: 100%;
  }
+
  .content {
+
    height: 100%;
+
  }
+
  @media (max-width: 720px) {
+
    .app {
+
      display: grid;
+
      grid-template-rows: 1fr auto;
+
      height: 100%;
+
    }
+
    .content {
+
      overflow-y: scroll;
+
    }
+
  }
</style>

<div class="app">
-
  <Header />
-
  <slot />
+
  <div class="global-hide-on-mobile">
+
    <Header />
+
  </div>
+
  <div class="content">
+
    <slot />
+
  </div>
  <div style:margin-top="auto">
    <div class="global-hide-on-mobile">
      <Footer />
modified src/App/Footer.svelte
@@ -4,7 +4,7 @@
  import KeyHint from "@app/components/KeyHint.svelte";
  import Popover from "@app/components/Popover.svelte";
  import RadworksLogo from "@app/components/RadworksLogo.svelte";
-
  import ThemeSettings from "./Header/ThemeSettings.svelte";
+
  import Settings from "./Settings.svelte";
</script>

<style>
@@ -66,9 +66,7 @@
        Settings
      </IconButton>

-
      <div slot="popover" style:width="19rem">
-
        <ThemeSettings />
-
      </div>
+
      <Settings slot="popover" />
    </Popover>

    <a
modified src/App/Header.svelte
@@ -61,7 +61,7 @@
  }
</style>

-
<header class="global-hide-on-mobile">
+
<header>
  <div class="left">
    <Link
      style="display: flex; align-items: center;"
deleted src/App/Header/ThemeSettings.svelte
@@ -1,74 +0,0 @@
-
<script lang="ts">
-
  import {
-
    codeFont,
-
    codeFonts,
-
    storeCodeFont,
-
    storeTheme,
-
    theme,
-
  } from "@app/lib/appearance";
-

-
  import Icon from "@app/components/Icon.svelte";
-
  import Radio from "@app/components/Radio.svelte";
-
  import Button from "@app/components/Button.svelte";
-
</script>
-

-
<style>
-
  .container {
-
    display: flex;
-
    flex-direction: column;
-
    align-items: center;
-
    gap: 1.5rem;
-
    font-size: var(--font-size-small);
-
  }
-

-
  .item {
-
    display: flex;
-
    width: 100%;
-
    align-items: center;
-
  }
-

-
  .right {
-
    display: flex;
-
    margin-left: auto;
-
  }
-
</style>
-

-
<div class="container">
-
  <div class="item">
-
    <div>Theme</div>
-
    <div class="right">
-
      <Radio>
-
        <Button
-
          ariaLabel="Light Mode"
-
          styleBorderRadius="0"
-
          variant={$theme === "light" ? "gray-white" : "dim"}
-
          on:click={() => storeTheme("light")}>
-
          <Icon name="sun" />
-
        </Button>
-
        <Button
-
          ariaLabel="Dark Mode"
-
          styleBorderRadius="0"
-
          variant={$theme === "dark" ? "gray-white" : "dim"}
-
          on:click={() => storeTheme("dark")}>
-
          <Icon name="moon" />
-
        </Button>
-
      </Radio>
-
    </div>
-
  </div>
-
  <div class="item">
-
    <div>Code Font</div>
-
    <div class="right">
-
      <Radio>
-
        {#each codeFonts as font}
-
          <Button
-
            styleBorderRadius="0"
-
            styleFontFamily={font.fontFamily}
-
            on:click={() => storeCodeFont(font.storedName)}
-
            variant={$codeFont === font.storedName ? "gray-white" : "dim"}>
-
            {font.displayName}
-
          </Button>
-
        {/each}
-
      </Radio>
-
    </div>
-
  </div>
-
</div>
added src/App/Help.svelte
@@ -0,0 +1,56 @@
+
<script lang="ts">
+
  import ExternalLink from "@app/components/ExternalLink.svelte";
+
  import KeyHint from "@app/components/KeyHint.svelte";
+
  import RadworksLogo from "@app/components/RadworksLogo.svelte";
+

+
  export let hideShortcuts: boolean = false;
+
</script>
+

+
<style>
+
  .help {
+
    width: 18.5rem;
+
    font-size: var(--font-size-small);
+
    color: var(--color-foreground-dim);
+
    display: flex;
+
    flex-direction: column;
+
    gap: 1rem;
+
  }
+
  .item {
+
    display: flex;
+
    justify-content: space-between;
+
    width: 100%;
+
  }
+
  .divider {
+
    border-bottom: 1px solid var(--color-fill-separator);
+
  }
+
  .logo {
+
    color: var(--color-foreground-contrast);
+
  }
+
  .logo:hover {
+
    color: var(--color-fill-secondary);
+
  }
+
</style>
+

+
<div class="help">
+
  <div class="item">
+
    Supported by
+
    <a
+
      class="logo"
+
      target="_blank"
+
      rel="noreferrer"
+
      href="https://radworks.org">
+
      <RadworksLogo />
+
    </a>
+
  </div>
+
  <div class="item">
+
    About
+
    <ExternalLink href="https://radicle.xyz">radicle.xyz</ExternalLink>
+
  </div>
+

+
  {#if !hideShortcuts}
+
    <div class="divider" />
+
    <div class="item">
+
      Keyboard shortcuts <KeyHint>?</KeyHint>
+
    </div>
+
  {/if}
+
</div>
modified src/App/MobileFooter.svelte
@@ -1,11 +1,11 @@
<script lang="ts">
  import Button from "@app/components/Button.svelte";
-
  import ExternalLink from "@app/components/ExternalLink.svelte";
  import IconSmall from "@app/components/IconSmall.svelte";
  import Link from "@app/components/Link.svelte";
  import Popover from "@app/components/Popover.svelte";
-
  import RadworksLogo from "@app/components/RadworksLogo.svelte";
-
  import ThemeSettings from "./Header/ThemeSettings.svelte";
+

+
  import Help from "./Help.svelte";
+
  import Settings from "./Settings.svelte";
</script>

<style>
@@ -14,8 +14,6 @@
    display: flex;
    justify-content: space-between;
    padding: 0.5rem;
-
    position: fixed;
-
    bottom: 0;
    z-index: 1;
    gap: 0.5rem;
    border-top: 1px solid var(--color-fill-separator);
@@ -25,21 +23,6 @@
    border-bottom: 1px solid var(--color-fill-separator);
    margin: 1.5rem 0;
  }
-
  .help {
-
    font-size: var(--font-size-small);
-
    color: var(--color-foreground-dim);
-
    display: flex;
-
    flex-direction: column;
-
    gap: 1rem;
-
  }
-
  a:hover {
-
    color: var(--color-fill-secondary);
-
  }
-
  .help-item {
-
    display: flex;
-
    justify-content: space-between;
-
    width: 100%;
-
  }
</style>

<div class="mobile-footer">
@@ -68,25 +51,10 @@
        <IconSmall name="menu" />
      </Button>

-
      <div slot="popover" style:width="18.5rem">
-
        <div class="help">
-
          <div class="help-item">
-
            Supported by
-
            <a
-
              class="logo"
-
              target="_blank"
-
              rel="noreferrer"
-
              href="https://radworks.org">
-
              <RadworksLogo />
-
            </a>
-
          </div>
-
          <div class="help-item">
-
            About
-
            <ExternalLink href="https://radicle.xyz">radicle.xyz</ExternalLink>
-
          </div>
-
        </div>
+
      <div slot="popover">
+
        <Help hideShortcuts />
        <div class="divider" />
-
        <ThemeSettings />
+
        <Settings />
      </div>
    </Popover>
  </div>
added src/App/Settings.svelte
@@ -0,0 +1,75 @@
+
<script lang="ts">
+
  import {
+
    codeFont,
+
    codeFonts,
+
    storeCodeFont,
+
    storeTheme,
+
    theme,
+
  } from "@app/lib/appearance";
+

+
  import Icon from "@app/components/Icon.svelte";
+
  import Radio from "@app/components/Radio.svelte";
+
  import Button from "@app/components/Button.svelte";
+
</script>
+

+
<style>
+
  .settings {
+
    width: 18.5rem;
+
    display: flex;
+
    flex-direction: column;
+
    align-items: center;
+
    gap: 1.5rem;
+
    font-size: var(--font-size-small);
+
  }
+

+
  .item {
+
    display: flex;
+
    width: 100%;
+
    align-items: center;
+
  }
+

+
  .right {
+
    display: flex;
+
    margin-left: auto;
+
  }
+
</style>
+

+
<div class="settings">
+
  <div class="item">
+
    <div>Theme</div>
+
    <div class="right">
+
      <Radio>
+
        <Button
+
          ariaLabel="Light Mode"
+
          styleBorderRadius="0"
+
          variant={$theme === "light" ? "gray-white" : "dim"}
+
          on:click={() => storeTheme("light")}>
+
          <Icon name="sun" />
+
        </Button>
+
        <Button
+
          ariaLabel="Dark Mode"
+
          styleBorderRadius="0"
+
          variant={$theme === "dark" ? "gray-white" : "dim"}
+
          on:click={() => storeTheme("dark")}>
+
          <Icon name="moon" />
+
        </Button>
+
      </Radio>
+
    </div>
+
  </div>
+
  <div class="item">
+
    <div>Code Font</div>
+
    <div class="right">
+
      <Radio>
+
        {#each codeFonts as font}
+
          <Button
+
            styleBorderRadius="0"
+
            styleFontFamily={font.fontFamily}
+
            on:click={() => storeCodeFont(font.storedName)}
+
            variant={$codeFont === font.storedName ? "gray-white" : "dim"}>
+
            {font.displayName}
+
          </Button>
+
        {/each}
+
      </Radio>
+
    </div>
+
  </div>
+
</div>
modified src/components/File.svelte
@@ -24,7 +24,7 @@

  .sticky {
    position: sticky;
-
    top: 3.5rem;
+
    top: 0;
  }

  .collapsed {
@@ -55,11 +55,6 @@
    border-bottom-left-radius: var(--border-radius-small);
    border-bottom-right-radius: var(--border-radius-small);
  }
-
  @media (max-width: 720px) {
-
    .sticky {
-
      top: 0rem;
-
    }
-
  }
</style>

<div bind:this={header} class="header" class:collapsed={!expanded} class:sticky>
modified src/components/LoadError.svelte
@@ -40,7 +40,7 @@
      </div>
      <div class="help">
        If you need help resolving this issue, copy the error message
-
        <br />
+
        <br class="global-hide-on-mobile" />
        below and send it to us on
        <ExternalLink href="https://radicle.zulipchat.com">
          radicle.zulipchat.com
modified src/components/Markdown.svelte
@@ -201,6 +201,9 @@
  :global(html) {
    scroll-padding-top: 4rem;
  }
+
  .markdown {
+
    max-width: 1024px;
+
  }
  .front-matter {
    font-size: var(--font-size-tiny);
    font-family: var(--font-family-monospace);
modified src/views/home/Index.svelte
@@ -31,7 +31,6 @@
    flex-wrap: wrap;
    gap: 2.5rem;
    width: 100%;
-
    max-width: var(--content-max-width);
  }
  .project {
    width: 16rem;
@@ -48,7 +47,7 @@
      width: 100%;
    }
    .projects {
-
      margin-bottom: 4.5rem;
+
      margin-bottom: 1.5rem;
      gap: 1.5rem;
    }
    .wrapper {
modified src/views/nodes/View.svelte
@@ -90,6 +90,26 @@
    align-items: center;
    justify-content: center;
  }
+

+
  @media (max-width: 720px) {
+
    .projects {
+
      gap: 1.5rem;
+
    }
+
    .wrapper {
+
      width: 100%;
+
      padding: 1rem 1.5rem 1.5rem 1.5rem;
+
      gap: 2rem;
+
    }
+
    .layout {
+
      width: 100%;
+
      display: flex;
+
      justify-content: center;
+
      padding: 0;
+
    }
+
    .info {
+
      flex-direction: column;
+
    }
+
  }
</style>

<AppLayout>
@@ -127,13 +147,25 @@
              project: project.id,
              node: baseUrl,
            }}>
-
            <ProjectCard
-
              {activity}
-
              id={project.id}
-
              name={project.name}
-
              visibility={project.visibility?.type}
-
              description={project.description}
-
              head={project.head} />
+
            <div class="global-hide-on-mobile">
+
              <ProjectCard
+
                {activity}
+
                id={project.id}
+
                name={project.name}
+
                visibility={project.visibility?.type}
+
                description={project.description}
+
                head={project.head} />
+
            </div>
+
            <div class="global-hide-on-desktop">
+
              <ProjectCard
+
                compact
+
                {activity}
+
                id={project.id}
+
                name={project.name}
+
                visibility={project.visibility?.type}
+
                description={project.description}
+
                head={project.head} />
+
            </div>
          </Link>
        {/each}
      </div>
modified src/views/projects/Changeset/FileDiff.svelte
@@ -239,7 +239,7 @@
</script>

<style>
-
  main {
+
  .container {
    font-size: var(--font-size-small);
    background: var(--color-background-float);
    border-radius: 0 0 var(--border-radius-small) var(--border-radius-small);
@@ -420,7 +420,7 @@
    {/if}
  </svelte:fragment>

-
  <main>
+
  <div class="container">
    {#if fileDiff.type === "plain"}
      {#if fileDiff.hunks.length > 0}
        <table class="diff" data-file-diff-select>
@@ -494,5 +494,5 @@
        <Placeholder iconName="binary-file" caption="Binary file" inline />
      </div>
    {/if}
-
  </main>
+
  </div>
</File>
modified src/views/projects/Commit.svelte
@@ -16,6 +16,9 @@
</script>

<style>
+
  .commit {
+
    padding: 1rem;
+
  }
  .header {
    margin-bottom: 3rem;
    border-radius: var(--border-radius-small);
@@ -27,18 +30,20 @@
  }
</style>

-
<Layout {baseUrl} {project} styleContentMargin="0">
-
  <div class="header">
-
    <InlineMarkdown fontSize="large" content={header.summary} />
-
    <pre class="description txt-small">{header.description}</pre>
-
    <CommitAuthorship {header}>
-
      <span class="global-hash">{formatCommit(header.id)}</span>
-
    </CommitAuthorship>
+
<Layout {baseUrl} {project}>
+
  <div class="commit">
+
    <div class="header">
+
      <InlineMarkdown fontSize="large" content={header.summary} />
+
      <pre class="description txt-small">{header.description}</pre>
+
      <CommitAuthorship {header}>
+
        <span class="global-hash">{formatCommit(header.id)}</span>
+
      </CommitAuthorship>
+
    </div>
+
    <Changeset
+
      {baseUrl}
+
      projectId={project.id}
+
      files={commit.files}
+
      diff={commit.diff}
+
      revision={commit.commit.id} />
  </div>
-
  <Changeset
-
    {baseUrl}
-
    projectId={project.id}
-
    files={commit.files}
-
    diff={commit.diff}
-
    revision={commit.commit.id} />
</Layout>
modified src/views/projects/History.svelte
@@ -108,10 +108,10 @@
  }
</style>

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

-
  <div style:margin-top="1rem" style:margin-left="1rem" slot="subheader">
+
  <div style:margin="1rem 0 1rem 1rem" slot="subheader">
    <Header
      node={baseUrl}
      {project}
@@ -123,17 +123,19 @@
      historyLinkActive={true} />
  </div>

-
  {#each groupCommits(allCommitHeaders) as group (group.time)}
-
    <div class="group-header">{group.date}</div>
-
    <List items={group.commits}>
-
      <CommitTeaser
-
        slot="item"
-
        let:item
-
        projectId={project.id}
-
        {baseUrl}
-
        commit={item} />
-
    </List>
-
  {/each}
+
  <div>
+
    {#each groupCommits(allCommitHeaders) as group (group.time)}
+
      <div class="group-header">{group.date}</div>
+
      <List items={group.commits}>
+
        <CommitTeaser
+
          slot="item"
+
          let:item
+
          projectId={project.id}
+
          {baseUrl}
+
          commit={item} />
+
      </List>
+
    {/each}
+
  </div>

  {#if loading || allCommitHeaders.length < totalCommitCount}
    <div class="more">
modified src/views/projects/Issue.svelte
@@ -402,6 +402,7 @@
  .issue {
    display: flex;
    flex: 1;
+
    padding: 1rem;
  }
  .metadata {
    display: flex;
@@ -463,7 +464,7 @@
  }
</style>

-
<Layout {baseUrl} {project} activeTab="issues" styleContentMargin="0">
+
<Layout {baseUrl} {project} activeTab="issues">
  <div class="issue">
    <div style="display: flex; flex: 1; flex-direction: column; gap: 1.5rem;">
      <CobHeader id={issue.id}>
modified src/views/projects/Issue/IssueTeaser.svelte
@@ -29,7 +29,6 @@
    display: flex;
    padding: 1.25rem;
    background-color: var(--color-background-float);
-
    border-bottom: 1px solid var(--color-fill-separator);
  }
  .issue-teaser:hover {
    background-color: var(--color-fill-float-hover);
modified src/views/projects/Issue/New.svelte
@@ -102,6 +102,7 @@
  .form {
    display: flex;
    flex: 1;
+
    padding: 1rem;
  }
  .actions {
    display: flex;
@@ -134,7 +135,7 @@
</style>

<Layout {baseUrl} {project} activeTab="issues">
-
  <main>
+
  <div>
    {#if session}
      <div class="form">
        <div class="editor">
@@ -233,5 +234,5 @@
      <ErrorMessage
        message="Couldn't access issue creation. Make sure you're authenticated." />
    {/if}
-
  </main>
+
  </div>
</Layout>
modified src/views/projects/Issues.svelte
@@ -17,9 +17,10 @@
  import IssueTeaser from "@app/views/projects/Issue/IssueTeaser.svelte";
  import Layout from "./Layout.svelte";
  import Link from "@app/components/Link.svelte";
+
  import List from "@app/components/List.svelte";
  import Loading from "@app/components/Loading.svelte";
-
  import Popover from "@app/components/Popover.svelte";
  import Placeholder from "@app/components/Placeholder.svelte";
+
  import Popover from "@app/components/Popover.svelte";

  export let baseUrl: BaseUrl;
  export let issues: Issue[];
@@ -91,8 +92,8 @@
  }
</style>

-
<Layout {baseUrl} {project} activeTab="issues" styleRightContentPadding="0">
-
  <div slot="header" style:display="flex" style:padding="1rem 1rem 0 1rem">
+
<Layout {baseUrl} {project} activeTab="issues">
+
  <div slot="header" style:display="flex" style:padding="1rem">
    <Popover
      popoverPadding="0"
      popoverPositionTop="2.5rem"
@@ -160,12 +161,14 @@
    {/if}
  </div>

-
  {#if allIssues.length > 0}
-
    <div style:border-top="1px solid var(--color-fill-separator)" />
-
  {/if}
-
  {#each allIssues as issue}
-
    <IssueTeaser {baseUrl} projectId={project.id} {issue} />
-
  {/each}
+
  <List items={allIssues}>
+
    <IssueTeaser
+
      slot="item"
+
      let:item
+
      {baseUrl}
+
      projectId={project.id}
+
      issue={item} />
+
  </List>

  {#if error}
    <ErrorMessage message="Couldn't load issues" {error} />
@@ -173,7 +176,7 @@

  {#if project.issues[state] === 0}
    <div
-
      style="height: calc(100vh - 7.5rem); display: flex; align-items: center; justify-content: center;">
+
      style="height: calc(100% - 4rem); display: flex; align-items: center; justify-content: center;">
      <Placeholder iconName="no-issues" caption={`No ${state} issues`} />
    </div>
  {/if}
modified src/views/projects/Layout.svelte
@@ -1,427 +1,131 @@
<script lang="ts">
  import type { ActiveTab } from "./Header.svelte";
  import type { BaseUrl, Project } from "@httpd-client";
-
  import type { SvelteComponent } from "svelte";
-

-
  import { onMount } from "svelte";

  import AppHeader from "@app/App/Header.svelte";
-
  import Clipboard from "@app/components/Clipboard.svelte";
-
  import CopyableId from "@app/components/CopyableId.svelte";
-
  import ExternalLink from "@app/components/ExternalLink.svelte";

  import Button from "@app/components/Button.svelte";
-
  import IconButton from "@app/components/IconButton.svelte";
  import IconSmall from "@app/components/IconSmall.svelte";
-
  import KeyHint from "@app/components/KeyHint.svelte";
  import Link from "@app/components/Link.svelte";
-
  import Popover from "@app/components/Popover.svelte";
-
  import RadworksLogo from "@app/components/RadworksLogo.svelte";
-
  import ThemeSettings from "@app/App/Header/ThemeSettings.svelte";
  import MobileFooter from "@app/App/MobileFooter.svelte";
+
  import Sidebar from "./Sidebar.svelte";

-
  export let activeTab: ActiveTab = undefined;
+
  export let activeTab: ActiveTab | undefined = undefined;
  export let baseUrl: BaseUrl;
  export let project: Project;
-
  export let styleRightContentPadding: string = "1rem";
-
  export let styleContentMargin: string = "1rem 0 0 0";
-

-
  let expanded = true;
-

-
  const SIDEBAR_STATE_KEY = "sidebarState";
-
  export function storeSidebarState(expanded: boolean): void {
-
    window.localStorage.setItem(
-
      SIDEBAR_STATE_KEY,
-
      expanded ? "expanded" : "collapsed",
-
    );
-
  }
-

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

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

-
  onMount(() => {
-
    expanded = loadSidebarState();
-
  });
-
  let clipboard: SvelteComponent;
-

-
  let outerWidth: number;
-
  let rightContentMaxWidth: string;
-
  let rightContentMargin: string;
-

-
  $: if (outerWidth <= 720) {
-
    rightContentMaxWidth = "unset";
-
    rightContentMargin = "0 0 3rem 0";
-
  } else {
-
    if (expanded) {
-
      rightContentMaxWidth = `calc(100vw - 23rem)`;
-
      rightContentMargin = "3.5rem 0 0 22.5rem";
-
    } else {
-
      rightContentMaxWidth = `calc(100vw - 4.5rem)`;
-
      rightContentMargin = "3.5rem 0 0 4.5rem";
-
    }
-
  }
</script>

<style>
  .layout {
-
    display: flex;
-
  }
-
  .expanded {
-
    width: 22.5rem;
-
    position: fixed;
+
    display: grid;
+
    grid-template: auto 1fr auto / auto 1fr auto;
    height: 100%;
-
    justify-content: space-between;
-
    display: flex;
-
    flex-direction: column;
-
    top: 0;
-
    left: 0;
-
    padding: 4.5rem 1rem 1rem 1rem;
-
    border-right: 1px solid var(--color-fill-separator);
-
    z-index: 1;
-
  }
-
  .collapsed {
-
    width: 4.5rem;
-
    position: fixed;
-
    height: 100%;
-
    justify-content: space-between;
-
    display: flex;
-
    flex-direction: column;
-
    top: 0;
-
    left: 0;
-
    padding: 4.5rem 1rem 1rem 1rem;
-
    border-right: 1px solid var(--color-fill-separator);
-
    z-index: 1;
-
  }
-
  .right-content {
-
    margin-top: 3.5rem;
-
    width: 100%;
-
  }
-
  .footer {
-
    display: flex;
-
    justify-content: space-between;
-
    width: 100%;
-
    gap: 1rem;
-
  }
-
  .content {
-
    width: 100%;
-
    max-width: var(--content-max-width);
-
    min-width: var(--content-min-width);
-
  }
-
  .id {
-
    border-radius: var(--border-radius-regular);
-
    border: 1px solid var(--color-border-hint);
-
    display: flex;
-
    justify-content: center;
-
    align-items: center;
  }

-
  .header-container {
+
  .desktop-header {
+
    grid-column: 1 / 4;
    border-bottom: 1px solid var(--color-fill-separator);
-
    background-color: var(--color-background-default);
-
    width: 100%;
-
    position: fixed;
-
    z-index: 2;
-
  }
-
  .help {
-
    font-size: var(--font-size-small);
-
    color: var(--color-foreground-dim);
-
    display: flex;
-
    flex-direction: column;
-
    gap: 1rem;
-
  }
-
  .help-item {
-
    display: flex;
-
    justify-content: space-between;
-
    width: 100%;
-
  }
-
  .logo {
-
    color: var(--color-foreground-contrast);
-
  }
-
  .divider {
-
    border-bottom: 1px solid var(--color-fill-separator);
-
  }
-
  a:hover {
-
    color: var(--color-fill-secondary);
  }

-
  .container {
-
    display: flex;
-
    flex-direction: column;
-
    gap: 0.5rem;
-
  }
-

-
  .counter {
-
    border-radius: var(--border-radius-tiny);
-
    background-color: var(--color-fill-ghost);
-
    color: var(--color-foreground-dim);
-
    padding: 0 0.25rem;
+
  .sidebar {
+
    grid-column: 1 / 2;
+
    border-right: 1px solid var(--color-fill-separator);
  }

-
  .selected {
-
    background-color: var(--color-fill-counter);
-
    color: var(--color-foreground-contrast);
+
  .content {
+
    grid-column: 2 / 3;
+
    overflow: scroll;
  }

-
  .hover {
-
    background-color: var(--color-fill-ghost-hover);
-
    color: var(--color-foreground-contrast);
+
  .mobile-footer {
+
    display: none;
  }

-
  .title-counter {
-
    display: flex;
-
    gap: 0.5rem;
-
    justify-content: space-between;
-
    width: 100%;
+
  @media (max-width: 720px) {
+
    .desktop-header {
+
      display: none;
+
    }
+
    .sidebar {
+
      display: none;
+
    }
+
    .content {
+
      overflow-y: scroll;
+
      overflow-x: hidden;
+
    }
+
    .mobile-footer {
+
      margin-top: auto;
+
      display: grid;
+
      grid-column: 1 / 4;
+
      background-color: pink;
+
    }
  }
</style>

-
<svelte:window bind:outerWidth />
-

-
<div class="header-container">
-
  <AppHeader />
-
</div>
-

<div class="layout">
-
  <!-- svelte-ignore a11y-click-events-have-key-events -->
-
  <div class="sidebar global-hide-on-mobile">
-
    <div class={expanded ? "expanded" : "collapsed"}>
-
      <div style="display: flex; flex-direction: column; gap: 1rem;">
-
        {#if expanded}
-
          <div class="id" style:padding="0.5rem 0.75rem">
-
            <CopyableId id={project.id} />
-
          </div>
-
        {:else}
-
          <div
-
            title="Copy RID to clipboard"
-
            class="id"
-
            style:color="var(--color-fill-secondary)"
-
            style:cursor="pointer"
-
            style:padding="0.5rem 1rem"
-
            role="button"
-
            tabindex="0"
-
            on:click={() => {
-
              clipboard.copy();
-
            }}>
-
            <Clipboard bind:this={clipboard} text={project.id} />
-
          </div>
-
        {/if}
-
        <div class="container">
-
          <Link
-
            title="Home"
-
            route={{
-
              resource: "project.source",
-
              project: project.id,
-
              node: baseUrl,
-
              path: "/",
-
            }}>
-
            <Button
-
              size="large"
-
              styleWidth="100%"
-
              styleJustifyContent={expanded ? "flex-start" : "center"}
-
              variant={activeTab === "source" ? "gray" : "background"}>
-
              <IconSmall name="home" />
-
              {#if expanded}
-
                Home
-
              {/if}
-
            </Button>
-
          </Link>
-
          <Link
-
            title={`${project.issues.open} Issues`}
-
            route={{
-
              resource: "project.issues",
-
              project: project.id,
-
              node: baseUrl,
-
            }}>
-
            <Button
-
              let:hover
-
              size="large"
-
              styleJustifyContent={expanded ? "flex-start" : "center"}
-
              styleWidth="100%"
-
              variant={activeTab === "issues" ? "gray" : "background"}>
-
              <IconSmall name="issue" />
-
              {#if expanded}
-
                <div class="title-counter">
-
                  Issues
-
                  <span
-
                    class="counter"
-
                    class:selected={activeTab === "issues"}
-
                    class:hover={hover && activeTab !== "issues"}>
-
                    {project.issues.open}
-
                  </span>
-
                </div>
-
              {/if}
-
            </Button>
-
          </Link>
-

-
          <Link
-
            title={`${project.patches.open} Patches`}
-
            route={{
-
              resource: "project.patches",
-
              project: project.id,
-
              node: baseUrl,
-
            }}>
-
            <Button
-
              let:hover
-
              size="large"
-
              styleWidth="100%"
-
              styleJustifyContent={expanded ? "flex-start" : "center"}
-
              variant={activeTab === "patches" ? "gray" : "background"}>
-
              <IconSmall name="patch" />
-
              {#if expanded}
-
                <div class="title-counter">
-
                  Patches
-
                  <span
-
                    class="counter"
-
                    class:hover={hover && activeTab !== "patches"}
-
                    class:selected={activeTab === "patches"}>
-
                    {project.patches.open}
-
                  </span>
-
                </div>
-
              {/if}
-
            </Button>
-
          </Link>
-
        </div>
-
      </div>
-

-
      <div
-
        class="footer"
-
        style:flex-direction={expanded ? "row" : "column-reverse"}>
-
        <IconButton
-
          title={expanded ? "Collapse" : "Expand"}
-
          on:click={() => {
-
            expanded = !expanded;
-
            storeSidebarState(expanded);
-
          }}>
-
          {#if expanded}
-
            <IconSmall name="chevron-left" /> Collapse
-
          {:else}
-
            <IconSmall name="chevron-right" />
-
          {/if}
-
        </IconButton>
-
        <Popover popoverPositionBottom="2rem" popoverPositionLeft="0">
-
          <IconButton
-
            title="Settings"
-
            slot="toggle"
-
            let:toggle
-
            on:click={toggle}>
-
            <IconSmall name="settings" />
-
            {#if expanded}
-
              Settings
-
            {/if}
-
          </IconButton>
-

-
          <div slot="popover" style:width="18.5rem">
-
            <ThemeSettings />
-
          </div>
-
        </Popover>
-

-
        <Popover popoverPositionBottom="2rem" popoverPositionLeft="0">
-
          <IconButton title="Help" slot="toggle" let:toggle on:click={toggle}>
-
            <IconSmall name="help" />
-
            {#if expanded}
-
              Help
-
            {/if}
-
          </IconButton>
+
  <div class="desktop-header">
+
    <AppHeader />
+
  </div>

-
          <div slot="popover" style:width="18.5rem">
-
            <div class="help">
-
              <div class="help-item">
-
                Supported by
-
                <a
-
                  class="logo"
-
                  target="_blank"
-
                  rel="noreferrer"
-
                  href="https://radworks.org">
-
                  <RadworksLogo />
-
                </a>
-
              </div>
-
              <div class="help-item">
-
                About
-
                <ExternalLink href="https://radicle.xyz">
-
                  radicle.xyz
-
                </ExternalLink>
-
              </div>
-
              <div class="divider" />
-
              <div class="help-item">
-
                Keyboard shortcuts <KeyHint>?</KeyHint>
-
              </div>
-
            </div>
-
          </div>
-
        </Popover>
-
      </div>
-
    </div>
+
  <div class="sidebar">
+
    <Sidebar {activeTab} {baseUrl} {project} />
  </div>

-
  <div
-
    class="right-content"
-
    style:padding={styleRightContentPadding}
-
    style:max-width={rightContentMaxWidth}
-
    style:margin={rightContentMargin}>
+
  <div class="content">
    <slot name="header" />
    <slot name="subheader" />
-

-
    <div class="content" style:margin={styleContentMargin}>
-
      <slot />
-
    </div>
+
    <slot />
  </div>
-
</div>

-
<div class="global-hide-on-desktop">
-
  <MobileFooter>
-
    <div style:width="100%">
-
      <Link
-
        title="Home"
-
        route={{
-
          resource: "project.source",
-
          project: project.id,
-
          node: baseUrl,
-
          path: "/",
-
        }}>
-
        <Button
-
          variant={activeTab === "source" ? "secondary" : "secondary-mobile"}
-
          styleWidth="100%">
-
          <IconSmall name="home" />
-
        </Button>
-
      </Link>
-
    </div>
+
  <div class="mobile-footer">
+
    <MobileFooter>
+
      <div style:width="100%">
+
        <Link
+
          title="Home"
+
          route={{
+
            resource: "project.source",
+
            project: project.id,
+
            node: baseUrl,
+
            path: "/",
+
          }}>
+
          <Button
+
            variant={activeTab === "source" ? "secondary" : "secondary-mobile"}
+
            styleWidth="100%">
+
            <IconSmall name="home" />
+
          </Button>
+
        </Link>
+
      </div>

-
    <div style:width="100%">
-
      <Link
-
        title={`${project.issues.open} Issues`}
-
        route={{
-
          resource: "project.issues",
-
          project: project.id,
-
          node: baseUrl,
-
        }}>
-
        <Button
-
          variant={activeTab === "issues" ? "secondary" : "secondary-mobile"}
-
          styleWidth="100%">
-
          <IconSmall name="issue" />
-
        </Button>
-
      </Link>
-
    </div>
+
      <div style:width="100%">
+
        <Link
+
          title={`${project.issues.open} Issues`}
+
          route={{
+
            resource: "project.issues",
+
            project: project.id,
+
            node: baseUrl,
+
          }}>
+
          <Button
+
            variant={activeTab === "issues" ? "secondary" : "secondary-mobile"}
+
            styleWidth="100%">
+
            <IconSmall name="issue" />
+
          </Button>
+
        </Link>
+
      </div>

-
    <div style:width="100%">
-
      <Link
-
        title={`${project.patches.open} Patches`}
-
        route={{
-
          resource: "project.patches",
-
          project: project.id,
-
          node: baseUrl,
-
        }}>
-
        <Button
-
          variant={activeTab === "patches" ? "secondary" : "secondary-mobile"}
-
          styleWidth="100%">
-
          <IconSmall name="patch" />
-
        </Button>
-
      </Link>
-
    </div>
-
  </MobileFooter>
+
      <div style:width="100%">
+
        <Link
+
          title={`${project.patches.open} Patches`}
+
          route={{
+
            resource: "project.patches",
+
            project: project.id,
+
            node: baseUrl,
+
          }}>
+
          <Button
+
            variant={activeTab === "patches" ? "secondary" : "secondary-mobile"}
+
            styleWidth="100%">
+
            <IconSmall name="patch" />
+
          </Button>
+
        </Link>
+
      </div>
+
    </MobileFooter>
+
  </div>
</div>
modified src/views/projects/Patch.svelte
@@ -555,6 +555,7 @@
  .patch {
    display: flex;
    flex: 1;
+
    padding: 1rem;
  }
  .metadata {
    display: flex;
@@ -643,7 +644,7 @@
  }
</style>

-
<Layout {baseUrl} {project} activeTab="patches" styleContentMargin="0">
+
<Layout {baseUrl} {project} activeTab="patches">
  <div class="patch">
    <div style="display: flex; flex: 1; flex-direction: column;">
      <CobHeader id={patch.id}>
modified src/views/projects/Patch/PatchTeaser.svelte
@@ -41,7 +41,6 @@
    display: flex;
    padding: 1.25rem;
    background-color: var(--color-background-float);
-
    border-bottom: 1px solid var(--color-fill-separator);
  }
  .patch-teaser:hover {
    background-color: var(--color-fill-float-hover);
modified src/views/projects/Patches.svelte
@@ -13,10 +13,11 @@
  import IconSmall from "@app/components/IconSmall.svelte";
  import Layout from "./Layout.svelte";
  import Link from "@app/components/Link.svelte";
+
  import List from "@app/components/List.svelte";
  import Loading from "@app/components/Loading.svelte";
-
  import Popover, { closeFocused } from "@app/components/Popover.svelte";
  import PatchTeaser from "./Patch/PatchTeaser.svelte";
  import Placeholder from "@app/components/Placeholder.svelte";
+
  import Popover, { closeFocused } from "@app/components/Popover.svelte";

  export let baseUrl: BaseUrl;
  export let patches: Patch[];
@@ -96,8 +97,8 @@
  }
</style>

-
<Layout {baseUrl} {project} activeTab="patches" styleRightContentPadding="0">
-
  <div slot="header" style:display="flex" style:padding="1rem 1rem 0 1rem">
+
<Layout {baseUrl} {project} activeTab="patches">
+
  <div slot="header" style:display="flex" style:padding="1rem">
    <Popover
      popoverPadding="0"
      popoverPositionTop="2.5rem"
@@ -148,12 +149,14 @@
    </Popover>
  </div>

-
  {#if allPatches.length > 0}
-
    <div style:border-top="1px solid var(--color-fill-separator)" />
-
  {/if}
-
  {#each allPatches as patch}
-
    <PatchTeaser {baseUrl} projectId={project.id} {patch} />
-
  {/each}
+
  <List items={allPatches}>
+
    <PatchTeaser
+
      slot="item"
+
      let:item
+
      {baseUrl}
+
      projectId={project.id}
+
      patch={item} />
+
  </List>

  {#if error}
    <ErrorMessage message="Couldn't load patches" {error} />
@@ -161,7 +164,7 @@

  {#if project.patches[state] === 0}
    <div
-
      style="height: calc(100vh - 7.5rem); display: flex; align-items: center; justify-content: center;">
+
      style="height: calc(100% - 4rem); display: flex; align-items: center; justify-content: center;">
      <Placeholder iconName="no-patches" caption={`No ${state} patches`} />
    </div>
  {/if}
added src/views/projects/Sidebar.svelte
@@ -0,0 +1,310 @@
+
<script lang="ts">
+
  import type { ActiveTab } from "./Header.svelte";
+
  import type { BaseUrl, Project } from "@httpd-client";
+
  import type { SvelteComponent } from "svelte";
+

+
  import { onMount } from "svelte";
+

+
  import Button from "@app/components/Button.svelte";
+
  import Clipboard from "@app/components/Clipboard.svelte";
+
  import CopyableId from "@app/components/CopyableId.svelte";
+
  import IconButton from "@app/components/IconButton.svelte";
+
  import IconSmall from "@app/components/IconSmall.svelte";
+
  import Link from "@app/components/Link.svelte";
+
  import Popover from "@app/components/Popover.svelte";
+

+
  import Help from "@app/App/Help.svelte";
+
  import Settings from "@app/App/Settings.svelte";
+

+
  const SIDEBAR_STATE_KEY = "sidebarState";
+

+
  export let activeTab: ActiveTab | undefined = undefined;
+
  export let baseUrl: BaseUrl;
+
  export let project: Project;
+

+
  let expanded = true;
+

+
  export function storeSidebarState(expanded: boolean): void {
+
    window.localStorage.setItem(
+
      SIDEBAR_STATE_KEY,
+
      expanded ? "expanded" : "collapsed",
+
    );
+
  }
+

+
  function loadSidebarState(): boolean {
+
    const storedSidebarState = window.localStorage.getItem(SIDEBAR_STATE_KEY);
+

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

+
  function toggleSidebar() {
+
    expanded = !expanded;
+
    storeSidebarState(expanded);
+
  }
+

+
  onMount(() => {
+
    expanded = loadSidebarState();
+
  });
+

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

+
<style>
+
  .sidebar {
+
    padding: 1rem;
+
    height: 100%;
+
    display: flex;
+
    flex-direction: column;
+
    justify-content: space-between;
+
  }
+
  .id {
+
    border-radius: var(--border-radius-small);
+
    border: 1px solid var(--color-border-hint);
+
    display: flex;
+
    justify-content: center;
+
    align-items: center;
+
  }
+
  .project-navigation {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 0.25rem;
+
  }
+
  .counter {
+
    border-radius: var(--border-radius-tiny);
+
    background-color: var(--color-fill-ghost);
+
    color: var(--color-foreground-dim);
+
    padding: 0 0.25rem;
+
  }
+
  .selected {
+
    background-color: var(--color-fill-counter);
+
    color: var(--color-foreground-contrast);
+
  }
+
  .hover {
+
    background-color: var(--color-fill-ghost-hover);
+
    color: var(--color-foreground-contrast);
+
  }
+
  .title-counter {
+
    display: flex;
+
    gap: 0.5rem;
+
    justify-content: space-between;
+
    width: 100%;
+
  }
+
  .sidebar-footer {
+
    display: flex;
+
    justify-content: space-between;
+
    width: 100%;
+
    gap: 0.25rem;
+
  }
+
</style>
+

+
<div class="sidebar">
+
  {#if expanded}
+
    <div style="display: flex; flex-direction: column; gap: 1rem;">
+
      <div class="id" style:padding="0.5rem 0.75rem">
+
        <CopyableId id={project.id} />
+
      </div>
+
      <div class="project-navigation">
+
        <Link
+
          title="Home"
+
          route={{
+
            resource: "project.source",
+
            project: project.id,
+
            node: baseUrl,
+
            path: "/",
+
          }}>
+
          <Button
+
            size="large"
+
            styleWidth="100%"
+
            styleJustifyContent={"flex-start"}
+
            variant={activeTab === "source" ? "gray" : "background"}>
+
            <IconSmall name="home" />
+
            Home
+
          </Button>
+
        </Link>
+
        <Link
+
          title={`${project.issues.open} Issues`}
+
          route={{
+
            resource: "project.issues",
+
            project: project.id,
+
            node: baseUrl,
+
          }}>
+
          <Button
+
            let:hover
+
            size="large"
+
            styleJustifyContent={"flex-start"}
+
            styleWidth="100%"
+
            variant={activeTab === "issues" ? "gray" : "background"}>
+
            <IconSmall name="issue" />
+
            <div class="title-counter">
+
              Issues
+
              <span
+
                class="counter"
+
                class:selected={activeTab === "issues"}
+
                class:hover={hover && activeTab !== "issues"}>
+
                {project.issues.open}
+
              </span>
+
            </div>
+
          </Button>
+
        </Link>
+

+
        <Link
+
          title={`${project.patches.open} Patches`}
+
          route={{
+
            resource: "project.patches",
+
            project: project.id,
+
            node: baseUrl,
+
          }}>
+
          <Button
+
            let:hover
+
            size="large"
+
            styleWidth="100%"
+
            styleJustifyContent={"flex-start"}
+
            variant={activeTab === "patches" ? "gray" : "background"}>
+
            <IconSmall name="patch" />
+
            <div class="title-counter">
+
              Patches
+
              <span
+
                class="counter"
+
                class:hover={hover && activeTab !== "patches"}
+
                class:selected={activeTab === "patches"}>
+
                {project.patches.open}
+
              </span>
+
            </div>
+
          </Button>
+
        </Link>
+
      </div>
+
    </div>
+

+
    <div class="sidebar-footer" style:flex-direction="row">
+
      <IconButton title={"Collapse"} on:click={toggleSidebar}>
+
        <IconSmall name="chevron-left" /> Collapse
+
      </IconButton>
+

+
      <Popover popoverPositionBottom="2rem" popoverPositionLeft="0">
+
        <IconButton title="Settings" slot="toggle" let:toggle on:click={toggle}>
+
          <IconSmall name="settings" />
+
          Settings
+
        </IconButton>
+

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

+
      <Popover popoverPositionBottom="2rem" popoverPositionLeft="0">
+
        <IconButton title="Help" slot="toggle" let:toggle on:click={toggle}>
+
          <IconSmall name="help" />
+
          Help
+
        </IconButton>
+

+
        <Help slot="popover" />
+
      </Popover>
+
    </div>
+
  {:else}
+
    <div style="display: flex; flex-direction: column; gap: 1rem;">
+
      <!-- svelte-ignore a11y-click-events-have-key-events -->
+
      <div
+
        title="Copy RID to clipboard"
+
        class="id"
+
        style:color="var(--color-fill-secondary)"
+
        style:cursor="pointer"
+
        style:padding="0.5rem 0"
+
        role="button"
+
        tabindex="0"
+
        on:click={() => {
+
          clipboard.copy();
+
        }}>
+
        <Clipboard bind:this={clipboard} text={project.id} />
+
      </div>
+
      <div class="project-navigation">
+
        <Link
+
          title="Home"
+
          route={{
+
            resource: "project.source",
+
            project: project.id,
+
            node: baseUrl,
+
            path: "/",
+
          }}>
+
          <Button
+
            size="large"
+
            stylePadding="0 0.75rem"
+
            variant={activeTab === "source" ? "gray" : "background"}>
+
            <IconSmall name="home" />
+
          </Button>
+
        </Link>
+
        <Link
+
          title={`${project.issues.open} Issues`}
+
          route={{
+
            resource: "project.issues",
+
            project: project.id,
+
            node: baseUrl,
+
          }}>
+
          <Button
+
            size="large"
+
            stylePadding="0 0.75rem"
+
            variant={activeTab === "issues" ? "gray" : "background"}>
+
            <IconSmall name="issue" />
+
          </Button>
+
        </Link>
+

+
        <Link
+
          title={`${project.patches.open} Patches`}
+
          route={{
+
            resource: "project.patches",
+
            project: project.id,
+
            node: baseUrl,
+
          }}>
+
          <Button
+
            size="large"
+
            stylePadding="0 0.75rem"
+
            variant={activeTab === "patches" ? "gray" : "background"}>
+
            <IconSmall name="patch" />
+
          </Button>
+
        </Link>
+
      </div>
+
    </div>
+

+
    <div class="sidebar-footer" style:flex-direction="column-reverse">
+
      <Button
+
        size="large"
+
        stylePadding="0 0.75rem"
+
        variant="background"
+
        title={"Expand"}
+
        on:click={toggleSidebar}>
+
        <IconSmall name="chevron-right" />
+
      </Button>
+

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

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

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

+
        <Help slot="popover" />
+
      </Popover>
+
    </div>
+
  {/if}
+
</div>
modified src/views/projects/Source.svelte
@@ -88,7 +88,6 @@
  .column-right {
    display: flex;
    flex-direction: column;
-
    min-width: var(--content-min-width);
    width: 100%;
  }
  .placeholder {
@@ -104,15 +103,15 @@
  }
  .sticky {
    position: sticky;
-
    top: 4.5rem;
+
    top: 0rem;
    max-height: calc(100vh - 5.5rem);
  }
</style>

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

-
  <div style:margin-top="1rem" style:margin-left="1rem" slot="subheader">
+
  <div style:margin="1rem 0 1rem 1rem" slot="subheader">
    <Header
      node={baseUrl}
      {project}
modified src/views/projects/Source/Header.svelte
@@ -131,17 +131,19 @@
    </Link>
  </div>

-
  <HoverPopover stylePopoverPositionLeft="0" stylePopoverPositionTop="0.5rem">
-
    <Button disabled notAllowed={false} variant="tab" slot="toggle">
-
      <IconSmall name="user" />
-
      <div class="title-counter">
-
        Contributors
-
        <div class="counter">{tree.stats.contributors}</div>
+
  <div class="global-hide-on-mobile">
+
    <HoverPopover stylePopoverPositionLeft="0" stylePopoverPositionTop="0.5rem">
+
      <Button disabled notAllowed={false} variant="tab" slot="toggle">
+
        <IconSmall name="user" />
+
        <div class="title-counter">
+
          Contributors
+
          <div class="counter">{tree.stats.contributors}</div>
+
        </div>
+
      </Button>
+
      <div class="txt-small" slot="popover">
+
        <div style:margin-bottom="1rem">Coming soon.</div>
+
        <div>Listing contributors is not implemented yet.</div>
      </div>
-
    </Button>
-
    <div class="txt-small" slot="popover">
-
      <div style:margin-bottom="1rem">Coming soon.</div>
-
      <div>Listing contributors is not implemented yet.</div>
-
    </div>
-
  </HoverPopover>
+
    </HoverPopover>
+
  </div>
</div>
modified tests/e2e/hashRouter.spec.ts
@@ -27,7 +27,9 @@ test("navigate between landing and project page", async ({ page }) => {
test("navigation between node and project pages", async ({ page }) => {
  await page.goto("/#/nodes/radicle.local");

-
  const project = page.locator(".project", { hasText: "source-browsing" });
+
  const project = page
+
    .locator(".project", { hasText: "source-browsing" })
+
    .nth(0);
  await project.click();
  await expect(page).toHaveURL(`/#${sourceBrowsingUrl}`);

modified tests/e2e/historyRouter.spec.ts
@@ -27,7 +27,9 @@ test("navigate between landing and project page", async ({ page }) => {
test("navigation between node and project pages", async ({ page }) => {
  await page.goto("/nodes/radicle.local");

-
  const project = page.locator(".project", { hasText: "source-browsing" });
+
  const project = page
+
    .locator(".project", { hasText: "source-browsing" })
+
    .nth(0);
  await project.click();
  await expect(page).toHaveURL(sourceBrowsingUrl);

modified tests/e2e/node.spec.ts
@@ -34,7 +34,9 @@ test("node metadata", async ({ page, peerManager }) => {

test("node projects", async ({ page }) => {
  await page.goto("/nodes/radicle.local");
-
  const project = page.locator(".project", { hasText: "source-browsing" });
+
  const project = page
+
    .locator(".project", { hasText: "source-browsing" })
+
    .nth(0);

  // Project metadata.
  {