Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
Implement 2 column layout and sidebar
Open rudolfs opened 1 year ago
26 files changed +1110 -566 376e475a 9ff06179
modified public/index.css
@@ -61,6 +61,7 @@ body {
  height: 1.5rem;
  padding: 0 0.5rem;
  min-width: 1.5rem;
+
  font-weight: var(--font-weight-regular);
}

:root {
modified src/components/Border.svelte
@@ -14,6 +14,9 @@
    styleGap?: string;
    styleOverflow?: string;
    flatTop?: boolean;
+
    styleBackgroundColor?: string;
+
    styleFlexDirection?: string;
+
    styleJustifyContent?: string;
  }

  const {
@@ -29,11 +32,15 @@
    styleGap = "0.5rem",
    styleOverflow,
    flatTop = false,
+
    styleBackgroundColor = "var(--color-background-default)",
+
    styleFlexDirection = "row",
+
    styleJustifyContent,
  }: Props = $props();

  const style = $derived(
-
    `--local-button-color-1: var(--color-fill-${variant});` +
-
      `--local-hover-background-color: ${hoverable ? "var(--color-background-float)" : "var(--color-background-default)"}`,
+
    `--local-background-color: ${styleBackgroundColor};` +
+
      `--local-button-color-1: var(--color-fill-${variant});` +
+
      `--local-hover-background-color: ${hoverable ? "var(--color-background-float)" : styleBackgroundColor}`,
  );
</script>

@@ -97,7 +104,7 @@
  }
  .p2-3 {
    grid-area: p2-3;
-
    background-color: var(--color-background-default);
+
    background-color: var(--local-background-color);
  }
  .p2-4 {
    grid-area: p2-4;
@@ -114,17 +121,17 @@
  }
  .p3-2 {
    grid-area: p3-2;
-
    background-color: var(--color-background-default);
+
    background-color: var(--local-background-color);
  }
  .p3-3 {
    grid-area: p3-3;
    display: flex;
    align-items: center;
-
    background-color: var(--color-background-default);
+
    background-color: var(--local-background-color);
  }
  .p3-4 {
    grid-area: p3-4;
-
    background-color: var(--color-background-default);
+
    background-color: var(--local-background-color);
  }
  .p3-5 {
    grid-area: p3-5;
@@ -141,7 +148,7 @@
  }
  .p4-3 {
    grid-area: p4-3;
-
    background-color: var(--color-background-default);
+
    background-color: var(--local-background-color);
  }
  .p4-4 {
    grid-area: p4-4;
@@ -217,7 +224,9 @@
    class="p3-3"
    style:padding={stylePadding}
    style:gap={styleGap}
-
    style:overflow={styleOverflow}>
+
    style:overflow={styleOverflow}
+
    style:justify-content={styleJustifyContent}
+
    style:flex-direction={styleFlexDirection}>
    {@render children()}
  </div>
  <div class="p3-4"></div>
modified src/components/Comment.svelte
@@ -116,6 +116,7 @@
  .edit-buttons {
    display: flex;
    gap: 0.25rem;
+
    cursor: pointer;
  }
</style>

@@ -143,7 +144,7 @@
      <div class="header-right">
        {#if id && editComment}
          <div class="edit-buttons">
-
            <Icon styleCursor="pointer" name="pen" onclick={toggleEdit} />
+
            <Icon name="pen" onclick={toggleEdit} />
          </div>
        {/if}
        {#if id && reactions && reactOnComment}
modified src/components/Header.svelte
@@ -3,86 +3,89 @@

  import { nodeRunning } from "@app/lib/events";

-
  import AnnounceSwitch from "./AnnounceSwitch.svelte";
-
  import Border from "./Border.svelte";
  import Icon from "./Icon.svelte";
  import NakedButton from "./NakedButton.svelte";
  import OutlineButton from "./OutlineButton.svelte";
-
  import Popover from "./Popover.svelte";
-
  import ThemeSwitch from "./ThemeSwitch.svelte";

  interface Props {
    breadcrumbs: Snippet;
-
    iconLeft?: Snippet;
+
    columnSwitch?: Snippet;
    center?: Snippet;
+
    settingsButton?: Snippet;
  }

-
  const { breadcrumbs, iconLeft, center }: Props = $props();
+
  const { breadcrumbs, columnSwitch, center, settingsButton }: Props = $props();
</script>

<style>
  .header {
-
    padding: 0 0.5rem;
-
    gap: 0.25rem;
    height: 5rem;
+
    padding: 0.5rem 1rem;
+
    display: flex;
+
    align-items: flex-start;
  }
-
  .wrapper {
-
    width: 100%;
-
    justify-content: space-between;
-
    padding: 0 0.5rem;
-
  }
-
  .wrapper-left {
-
    gap: 0.5rem;
-
    padding: 0 0.5rem;
-
  }
-
  .bottom-pixel-corners {
+
  .header:after {
+
    content: " ";
    position: absolute;
    top: 0;
    left: 0.5rem;
    right: 0.5rem;
    height: 5rem;
    z-index: -1;
-

    background-color: var(--color-background-float);
    clip-path: var(--3px-bottom-corner-fill);
  }
-
  .breadcrumbs {
+
  .wrapper {
+
    display: flex;
+
    flex-direction: column;
+
    width: 100%;
+
    row-gap: 8px;
+
  }
+
  .top-row {
+
    display: flex;
+
    width: 100%;
+
    justify-content: space-between;
+
  }
+
  .bottom-row {
+
    display: flex;
    gap: 0.5rem;
-
    margin-left: 1rem;
+
    font-size: var(--font-size-tiny);
+
    font-weight: var(--font-weight-semibold);
+
    align-items: center;
+

    min-height: 1.5rem;
+
    width: 100%;
+
    padding-left: 12px;
+
    /* Fixed height so that the navigation arrow buttons don't jump vertically
+
       when the column buttons aren't shown on the Home view vs Repo view. */
+
    height: 24px;
  }
</style>

<div class="header global-flex">
-
  <div
-
    class="global-flex"
-
    style:flex-direction="column"
-
    style:width="100%"
-
    style:align-items="flex-start">
-
    <div class="wrapper global-flex">
-
      <div class="wrapper-left global-flex" style:gap="0">
-
        <div class="global-flex" style:gap="0">
-
          <NakedButton
-
            variant="ghost"
-
            onclick={() => {
-
              window.history.back();
-
            }}>
-
            <Icon name="arrow-left" />
-
          </NakedButton>
-
          <NakedButton
-
            variant="ghost"
-
            onclick={() => {
-
              window.history.forward();
-
            }}>
-
            <Icon name="arrow-right" />
-
          </NakedButton>
-
        </div>
-
        {@render iconLeft?.()}
+
  <div class="wrapper">
+
    <div class="top-row">
+
      <div class="global-flex" style:gap="0">
+
        <NakedButton
+
          variant="ghost"
+
          onclick={() => {
+
            window.history.back();
+
          }}>
+
          <Icon name="arrow-left" />
+
        </NakedButton>
+
        <NakedButton
+
          variant="ghost"
+
          onclick={() => {
+
            window.history.forward();
+
          }}>
+
          <Icon name="arrow-right" />
+
        </NakedButton>
      </div>

      {@render center?.()}

-
      <div class="global-flex" style:gap="0.5rem">
+
      <div class="global-flex">
+
        {@render settingsButton?.()}
        <OutlineButton variant="ghost">
          {#if $nodeRunning}
            <Icon name="online" />
@@ -92,44 +95,16 @@
            Offline
          {/if}
        </OutlineButton>
-
        <Popover popoverPositionRight="0" popoverPositionTop="3rem">
-
          {#snippet toggle(onclick)}
-
            <NakedButton title="Settings" variant="ghost" {onclick}>
-
              <Icon name="settings" />
-
            </NakedButton>
-
          {/snippet}
-
          {#snippet popover()}
-
            <Border
-
              variant="ghost"
-
              stylePadding="0.5rem 1rem"
-
              styleWidth="27rem">
-
              <div
-
                class="global-flex"
-
                style:flex-direction="column"
-
                style:align-items="flex-start"
-
                style:gap="1rem"
-
                style:width="100%">
-
                <div
-
                  class="global-flex"
-
                  style:justify-content="space-between"
-
                  style:width="100%">
-
                  Theme <ThemeSwitch />
-
                </div>
-
                <div
-
                  class="global-flex"
-
                  style:justify-content="space-between"
-
                  style:width="100%">
-
                  Announce changes <AnnounceSwitch />
-
                </div>
-
              </div>
-
            </Border>
-
          {/snippet}
-
        </Popover>
      </div>
    </div>
-
    <div class="global-flex txt-tiny txt-semibold breadcrumbs">
+

+
    <div class="bottom-row">
      {@render breadcrumbs()}
+
      {#if columnSwitch}
+
        <div style:margin-left="auto">
+
          {@render columnSwitch()}
+
        </div>
+
      {/if}
    </div>
  </div>
-
  <div class="bottom-pixel-corners"></div>
</div>
modified src/components/Icon.svelte
@@ -4,7 +4,7 @@
  interface Props {
    size?: "16" | "32";
    onclick?: () => void;
-
    styleCursor?: "default" | "pointer";
+
    styleCursor?: "default" | "pointer" | "inherit";
    name:
      | "arrow-left"
      | "arrow-right"
@@ -27,6 +27,7 @@
      | "moon"
      | "more-vertical"
      | "offline"
+
      | "one"
      | "online"
      | "patch"
      | "pen"
@@ -37,15 +38,15 @@
      | "seedling"
      | "seedling-filled"
      | "settings"
-
      | "sidebar"
      | "sun"
+
      | "two"
      | "warning";
  }

  const {
    size = "16",
    onclick = undefined,
-
    styleCursor = "default",
+
    styleCursor = "inherit",
    name,
  }: Props = $props();
</script>
@@ -387,6 +388,15 @@
    <path d="M4 11L5 11V12L4 12L4 11Z" />
    <path d="M3 12H4L4 13H3L3 12Z" />
    <path d="M2 13L3 13L3 14H2V13Z" />
+
  {:else if name === "one"}
+
    <path d="M2 3.00003H3V13H2V3.00003Z" />
+
    <path d="M13 3.00003H14V13H13V3.00003Z" />
+
    <path d="M3 2.00003H13V3.00003H3L3 2.00003Z" />
+
    <path d="M3 13H13V14H3L3 13Z" />
+
    <path d="M8 5.00003H9V11H8V5.00003Z" />
+
    <path d="M6 7.00003H7V8.00003H6V7.00003Z" />
+
    <path d="M7 6.00003H8V7.00003H7V6.00003Z" />
+
    <path d="M6 10H11V11H6V10Z" />
  {:else if name === "online"}
    <path d="M3 5.99999L3 7.99999H2L2 5.99999H3Z" />
    <path d="M13 9.99998V7.99998H14V9.99998H13Z" />
@@ -600,12 +610,6 @@
    <path d="M4 4L5 4L5 7H4V4Z" />
    <path d="M11 9L12 9V12H11V9Z" />
    <path d="M7 4L8 4V7L7 7V4Z" />
-
  {:else if name === "sidebar"}
-
    <path d="M2 3H3V13H2V3Z" />
-
    <path d="M13 3H14V13H13V3Z" />
-
    <path d="M6 2H7V14H6V2Z" />
-
    <path d="M3 2H13L13 3H3L3 2Z" />
-
    <path d="M3 13H13L13 14H3L3 13Z" />
  {:else if name === "sun"}
    <path d="M8 2H9V3H8V2Z" />
    <path d="M14 8V9H13V8H14Z" />
@@ -631,6 +635,32 @@
    <path d="M6 9L7 9L7 10H6V9Z" />
    <path d="M9 9L10 9L10 10H9L9 9Z" />
    <path d="M9 6H10V7H9V6Z" />
+
  {:else if name === "two"}
+
    <path d="M2 3.00003H3V13H2V3.00003Z" />
+
    <path d="M13 3.00003H14V13H13V3.00003Z" />
+
    <path d="M6 2.00003H3L3 3.00003L6 3.00003V2.00003Z" />
+
    <path d="M10 2.00003H13V3.00003L10 3.00003V2.00003Z" />
+
    <path d="M10 13H13V14H10V13Z" />
+
    <path d="M3 13H6V14H3L3 13Z" />
+
    <path d="M2 3.00003H3V13H2V3.00003Z" />
+
    <path d="M13 3.00003H14V13H13V3.00003Z" />
+
    <path d="M6 3.00003L7 3.00003V4.00003H6V3.00003Z" />
+
    <path d="M6 12H7V13H6L6 12Z" />
+
    <path d="M9 3.00003L10 3.00003V4.00003H9V3.00003Z" />
+
    <path d="M9 12H10V13L9 13V12Z" />
+
    <path d="M7 2.00003L3 2.00003L3 3.00003L7 3.00003V2.00003Z" />
+
    <path d="M9 2.00003L13 2.00003V3.00003L9 3.00003L9 2.00003Z" />
+
    <path d="M9 13L13 13V14H9L9 13Z" />
+
    <path d="M3 13H7L7 14H3L3 13Z" />
+
    <path d="M6 5.00003H10V6.00003H6V5.00003Z" />
+
    <path d="M6 9.00003H8V10H6V9.00003Z" />
+
    <path d="M5 10H6V11H5V10Z" />
+
    <path d="M6 10H7V11H6V10Z" />
+
    <path d="M7 10H8V11H7V10Z" />
+
    <path d="M6 10H11V11H6V10Z" />
+
    <path d="M10 6.00003L11 6.00003V8.00003H10V6.00003Z" />
+
    <path d="M5 6.00003H6V7.00003H5V6.00003Z" />
+
    <path d="M8 8.00003H10V9.00003L8 9.00003L8 8.00003Z" />
  {:else if name === "warning"}
    <path d="M7 2H9V3H7V2Z" />
    <path d="M6 3H7V5H6V3Z" />
added src/components/IssueSecondColumn.svelte
@@ -0,0 +1,56 @@
+
<script lang="ts">
+
  import type { Issue } from "@bindings/cob/issue/Issue";
+
  import type { RepoInfo } from "@bindings/repo/RepoInfo";
+

+
  import * as router from "@app/lib/router";
+

+
  import IssueTeaser from "@app/components/IssueTeaser.svelte";
+
  import Icon from "./Icon.svelte";
+
  import OutlineButton from "./OutlineButton.svelte";
+

+
  interface Props {
+
    repo: RepoInfo;
+
    selectedIssueId?: string;
+
    issues: Issue[];
+
  }
+

+
  /* eslint-disable prefer-const */
+
  let { repo, selectedIssueId, issues }: Props = $props();
+
  /* eslint-enable prefer-const */
+
</script>
+

+
<style>
+
  .issue-list {
+
    margin-top: 0.5rem;
+
    display: flex;
+
    flex-direction: column;
+
    gap: 2px;
+
    padding-bottom: 1rem;
+
  }
+
</style>
+

+
<div class="global-flex" style:justify-content="space-between">
+
  <div class="txt-medium" style:font-weight="var(--font-weight-medium)">
+
    Issues
+
  </div>
+

+
  <OutlineButton
+
    variant="ghost"
+
    onclick={() => {
+
      void router.push({
+
        resource: "repo.createIssue",
+
        rid: repo.rid,
+
      });
+
    }}>
+
    <Icon name="plus" />New
+
  </OutlineButton>
+
</div>
+
<div class="issue-list">
+
  {#each issues as issue}
+
    <IssueTeaser
+
      compact
+
      {issue}
+
      rid={repo.rid}
+
      selected={issue.id === selectedIssueId} />
+
  {/each}
+
</div>
modified src/components/IssueTeaser.svelte
@@ -17,9 +17,11 @@
  interface Props {
    issue: Issue;
    rid: string;
+
    selected?: boolean;
+
    compact?: boolean;
  }

-
  const { issue, rid }: Props = $props();
+
  const { issue, rid, selected = false, compact = false }: Props = $props();
</script>

<style>
@@ -28,11 +30,15 @@
    align-items: center;
    justify-content: space-between;
    gap: 0.25rem;
-
    height: 5rem;
+
    min-height: 5rem;
    background-color: var(--color-background-float);
    padding: 1rem;
    cursor: pointer;
    font-size: var(--font-size-regular);
+
    word-break: break-word;
+
  }
+
  .selected {
+
    background-color: var(--color-fill-float-hover);
  }
  .issue-teaser:hover {
    background-color: var(--color-fill-float-hover);
@@ -57,6 +63,7 @@
  tabindex="0"
  role="button"
  class="issue-teaser"
+
  class:selected
  onclick={() => {
    void push({ resource: "repo.issue", rid, issue: issue.id });
  }}>
@@ -72,7 +79,7 @@
      style:flex-direction="column"
      style:align-items="flex-start">
      <InlineTitle content={issue.title} />
-
      <div class="global-flex txt-small">
+
      <div class="global-flex txt-small" style:flex-wrap="wrap">
        <NodeId {...authorForNodeId(issue.author)} />
        opened
        <Id id={issue.id} variant="oid" />
@@ -80,10 +87,13 @@
      </div>
    </div>
  </div>
+

  <div class="global-flex">
-
    {#each issue.labels as label}
-
      <div class="global-counter txt-small">{label}</div>
-
    {/each}
+
    {#if !compact}
+
      {#each issue.labels as label}
+
        <div class="global-counter txt-small">{label}</div>
+
      {/each}
+
    {/if}

    {#if issue.commentCount > 0}
      <div class="txt-small global-flex" style:gap="0.25rem">
added src/components/IssuesSecondColumn.svelte
@@ -0,0 +1,149 @@
+
<script lang="ts">
+
  import type { IssueStatus } from "@app/views/repo/router";
+
  import type { ProjectPayload } from "@bindings/repo/ProjectPayload";
+
  import type { RepoInfo } from "@bindings/repo/RepoInfo";
+

+
  import Border from "./Border.svelte";
+
  import Icon from "./Icon.svelte";
+
  import Link from "./Link.svelte";
+
  import RepoTeaser from "./RepoTeaser.svelte";
+
  import Settings from "./Settings.svelte";
+

+
  interface Props {
+
    project: ProjectPayload;
+
    status: IssueStatus;
+
    repo: RepoInfo;
+
  }
+

+
  const { project, status, repo }: Props = $props();
+
</script>
+

+
<style>
+
  .container {
+
    display: flex;
+
    flex-direction: column;
+
    height: 100%;
+
    justify-content: space-between;
+
    padding-bottom: 1rem;
+
  }
+
  .tab {
+
    align-items: center;
+
    background-color: var(--color-background-float);
+
    clip-path: var(--1px-corner-fill);
+
    display: flex;
+
    font-size: var(--font-size-small);
+
    justify-content: space-between;
+
    padding: 8px 4px 8px 8px;
+
    width: 100%;
+
  }
+
  .tab:not(.active) {
+
    color: var(--color-foreground-dim);
+
  }
+
  .tab:not(.active):hover {
+
    background-color: var(--color-fill-float-hover);
+
  }
+
  .active {
+
    background-color: var(--color-background-default);
+
    font-weight: var(--font-weight-semibold);
+
  }
+
  .highlight {
+
    color: var(--color-foreground-contrast);
+
  }
+
  .closed {
+
    color: var(--color-foreground-red);
+
  }
+
  .open {
+
    color: var(--color-foreground-success);
+
  }
+
</style>
+

+
<div class="container">
+
  <div>
+
    <RepoTeaser name={project.data.name} seeding={repo.seeding} />
+

+
    <Border
+
      variant="ghost"
+
      styleFlexDirection="column"
+
      styleGap="2px"
+
      styleBackgroundColor="var(--color-background-float)">
+
      <Link
+
        styleWidth="100%"
+
        underline={false}
+
        route={{ resource: "repo.issues", rid: repo.rid, status: "all" }}>
+
        <div class="tab active">
+
          <div class="global-flex"><Icon name="issue" />Issues</div>
+
          <div class="global-counter">
+
            {project.meta.issues.open + project.meta.issues.closed}
+
          </div>
+
        </div>
+
      </Link>
+

+
      <Link
+
        styleWidth="100%"
+
        underline={false}
+
        route={{
+
          resource: "repo.issues",
+
          rid: repo.rid,
+
          status: "open",
+
        }}>
+
        <div class="tab" class:active={status === "open"}>
+
          <div
+
            class="global-flex"
+
            class:open={["open", "all"].includes(status)}>
+
            <Icon name="issue" />Open
+
          </div>
+
          <div class="global-counter" class:highlight={status === "all"}>
+
            {project.meta.issues.open}
+
          </div>
+
        </div>
+
      </Link>
+

+
      <Link
+
        styleWidth="100%"
+
        underline={false}
+
        route={{
+
          resource: "repo.issues",
+
          rid: repo.rid,
+
          status: "closed",
+
        }}>
+
        <div class="tab" class:active={status === "closed"}>
+
          <div
+
            class="global-flex"
+
            class:closed={["closed", "all"].includes(status)}>
+
            <Icon name="issue" />Closed
+
          </div>
+
          <div class="global-counter" class:highlight={status === "all"}>
+
            {project.meta.issues.closed}
+
          </div>
+
        </div>
+
      </Link>
+
    </Border>
+

+
    <div style:margin-top="0.5rem">
+
      <Link
+
        styleWidth="100%"
+
        underline={false}
+
        route={{ resource: "repo.patches", rid: repo.rid, status: "open" }}>
+
        <div
+
          class="tab"
+
          style:color="var(--color-foreground-contrast)"
+
          style:padding-left="12px">
+
          <div class="global-flex"><Icon name="patch" />Patches</div>
+
          <div class="global-counter">
+
            {project.meta.patches.draft +
+
              project.meta.patches.open +
+
              project.meta.patches.archived +
+
              project.meta.patches.merged}
+
          </div>
+
        </div>
+
      </Link>
+
    </div>
+
  </div>
+

+
  <Settings
+
    compact={false}
+
    popoverProps={{
+
      popoverPositionBottom: "3rem",
+
      popoverPositionLeft: "0",
+
    }} />
+
</div>
modified src/components/Link.svelte
@@ -8,14 +8,16 @@
    children: Snippet;
    route: Route;
    disabled?: boolean;
-
    variant?: "active" | "regular" | "tab";
+
    underline?: boolean;
+
    styleWidth?: string;
  }

  const {
    children,
    route,
    disabled = false,
-
    variant = "regular",
+
    underline = true,
+
    styleWidth,
  }: Props = $props();

  function navigateToRoute(event: MouseEvent): void {
@@ -33,41 +35,17 @@
    color: var(--color-foreground-contrast);
    text-decoration: none;
  }
-
  .regular:hover {
+
  .underline:hover {
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-underline-offset: 2px;
  }
-

-
  .tab {
-
    display: flex;
-
    width: 100%;
-
    justify-content: space-between;
-
    align-items: center;
-
    padding: 4px 4px 4px 10px;
-
    clip-path: var(--2px-corner-fill);
-
  }
-

-
  .tab:hover {
-
    background-color: var(--color-fill-ghost);
-
  }
-

-
  .active {
-
    background-color: var(--color-fill-ghost);
-
    display: flex;
-
    width: 100%;
-
    justify-content: space-between;
-
    align-items: center;
-
    padding: 4px 4px 4px 10px;
-
    clip-path: var(--2px-corner-fill);
-
  }
</style>

<a
  onclick={navigateToRoute}
  href={routeToPath(route)}
-
  class:regular={variant === "regular"}
-
  class:active={variant === "active"}
-
  class:tab={variant === "tab"}>
+
  class:underline
+
  style:width={styleWidth}>
  {@render children()}
</a>
modified src/components/NakedButton.svelte
@@ -6,9 +6,16 @@
    title?: string;
    variant: "primary" | "secondary" | "ghost";
    onclick?: () => void;
+
    styleHeight?: string;
  }

-
  const { children, title, variant, onclick }: Props = $props();
+
  const {
+
    children,
+
    title,
+
    variant,
+
    onclick,
+
    styleHeight = "32px",
+
  }: Props = $props();

  const style = $derived(
    `--button-color-1: var(--color-fill-${variant});` +
@@ -181,7 +188,6 @@
  }

  .container {
-
    height: 32px;
    cursor: pointer;
    white-space: nowrap;

@@ -204,7 +210,14 @@
</style>

<!-- svelte-ignore a11y_click_events_have_key_events -->
-
<div class="container" {onclick} {title} role="button" tabindex="0" {style}>
+
<div
+
  class="container"
+
  {onclick}
+
  {title}
+
  role="button"
+
  tabindex="0"
+
  {style}
+
  style:height={styleHeight}>
  <div class="pixel p1-1"></div>
  <div class="pixel p1-2"></div>
  <div class="pixel p1-3"></div>
modified src/components/OutlineButton.svelte
@@ -224,7 +224,6 @@
  }

  .container {
-
    height: 32px;
    cursor: pointer;
    white-space: nowrap;

modified src/components/PatchTeaser.svelte
@@ -9,31 +9,29 @@
    patchStatusColor,
  } from "@app/lib/utils";
  import { invoke } from "@app/lib/invoke";
-
  import { onMount } from "svelte";
  import { push } from "@app/lib/router";

  import DiffStatBadge from "./DiffStatBadge.svelte";
  import Icon from "./Icon.svelte";
+
  import Id from "./Id.svelte";
  import InlineTitle from "./InlineTitle.svelte";
  import NodeId from "./NodeId.svelte";
-
  import Id from "./Id.svelte";
-

-
  let stats: Stats | undefined = $state(undefined);
-

-
  onMount(async () => {
-
    stats = await invoke<Stats>("diff_stats", {
-
      rid,
-
      base: patch.base,
-
      head: patch.head,
-
    });
-
  });

  interface Props {
    patch: Patch;
    rid: string;
+
    selected?: boolean;
+
    compact?: boolean;
+
    loadPatch?: (rid: string, patchId: string) => void;
  }

-
  const { patch, rid }: Props = $props();
+
  const {
+
    patch,
+
    rid,
+
    selected = false,
+
    compact = false,
+
    loadPatch,
+
  }: Props = $props();
</script>

<style>
@@ -42,11 +40,15 @@
    align-items: center;
    justify-content: space-between;
    gap: 0.25rem;
-
    height: 5rem;
+
    min-height: 5rem;
    background-color: var(--color-background-float);
    padding: 1rem;
    cursor: pointer;
    font-size: var(--font-size-regular);
+
    word-break: break-word;
+
  }
+
  .selected {
+
    background-color: var(--color-fill-float-hover);
  }
  .patch-teaser:hover {
    background-color: var(--color-fill-float-hover);
@@ -70,9 +72,14 @@
<div
  tabindex="0"
  role="button"
+
  class:selected
  class="patch-teaser"
  onclick={() => {
-
    void push({ resource: "repo.patch", rid, patch: patch.id });
+
    if (loadPatch) {
+
      loadPatch(rid, patch.id);
+
    } else {
+
      void push({ resource: "repo.patch", rid, patch: patch.id });
+
    }
  }}>
  <div class="global-flex">
    <div
@@ -86,7 +93,7 @@
      style:flex-direction="column"
      style:align-items="flex-start">
      <InlineTitle content={patch.title} />
-
      <div class="global-flex txt-small">
+
      <div class="global-flex txt-small" style:flex-wrap="wrap">
        <NodeId {...authorForNodeId(patch.author)} />
        opened
        <Id id={patch.id} variant="oid" />
@@ -94,17 +101,21 @@
      </div>
    </div>
  </div>
-
  <div class="global-flex">
-
    {#if stats}
-
      <DiffStatBadge {stats} />
-
    {/if}
-
    {#each patch.labels as label}
-
      <div class="global-counter txt-small">{label}</div>
-
    {/each}

-
    <div class="txt-small global-flex" style:gap="0.25rem">
-
      <Icon name="revision" />
-
      {patch.revisionCount}
+
  {#if !compact}
+
    <div class="global-flex">
+
      {#await invoke<Stats>( "diff_stats", { rid, base: patch.base, head: patch.head }, ) then stats}
+
        <DiffStatBadge {stats} />
+
      {/await}
+

+
      {#each patch.labels as label}
+
        <div class="global-counter txt-small">{label}</div>
+
      {/each}
+

+
      <div class="txt-small global-flex" style:gap="0.25rem">
+
        <Icon name="revision" />
+
        {patch.revisionCount}
+
      </div>
    </div>
-
  </div>
+
  {/if}
</div>
added src/components/PatchesSecondColumn.svelte
@@ -0,0 +1,195 @@
+
<script lang="ts">
+
  import type { PatchStatus } from "@app/views/repo/router";
+
  import type { ProjectPayload } from "@bindings/repo/ProjectPayload";
+
  import type { RepoInfo } from "@bindings/repo/RepoInfo";
+

+
  import Border from "./Border.svelte";
+
  import Icon from "./Icon.svelte";
+
  import Link from "./Link.svelte";
+
  import RepoTeaser from "./RepoTeaser.svelte";
+
  import Settings from "./Settings.svelte";
+

+
  interface Props {
+
    project: ProjectPayload;
+
    status?: PatchStatus;
+
    repo: RepoInfo;
+
  }
+
  const { project, status, repo }: Props = $props();
+
</script>
+

+
<style>
+
  .container {
+
    display: flex;
+
    flex-direction: column;
+
    height: 100%;
+
    justify-content: space-between;
+
    padding-bottom: 1rem;
+
  }
+
  .tab {
+
    align-items: center;
+
    background-color: var(--color-background-float);
+
    clip-path: var(--1px-corner-fill);
+
    display: flex;
+
    font-size: var(--font-size-small);
+
    justify-content: space-between;
+
    padding: 8px 4px 8px 8px;
+
    width: 100%;
+
  }
+
  .tab:not(.active) {
+
    color: var(--color-foreground-dim);
+
  }
+
  .tab:not(.active):hover {
+
    background-color: var(--color-fill-float-hover);
+
  }
+
  .active {
+
    background-color: var(--color-background-default);
+
    font-weight: var(--font-weight-semibold);
+
  }
+
  .highlight {
+
    color: var(--color-foreground-contrast);
+
  }
+
  .draft {
+
    color: var(--color-fill-gray);
+
  }
+
  .open {
+
    color: var(--color-fill-success);
+
  }
+
  .archived {
+
    color: var(--color-fill-yellow-iconic);
+
  }
+
  .merged {
+
    color: var(--color-foreground-primary);
+
  }
+
</style>
+

+
<div class="container">
+
  <div>
+
    <RepoTeaser name={project.data.name} seeding={repo.seeding} />
+

+
    <div style:margin-bottom="0.5rem">
+
      <Link
+
        styleWidth="100%"
+
        underline={false}
+
        route={{ resource: "repo.issues", rid: repo.rid, status: "open" }}>
+
        <div
+
          class="tab"
+
          style:color="var(--color-foreground-contrast)"
+
          style:padding-left="12px">
+
          <div class="global-flex"><Icon name="issue" />Issues</div>
+
          <div class="global-counter">
+
            {project.meta.issues.open + project.meta.issues.closed}
+
          </div>
+
        </div>
+
      </Link>
+
    </div>
+

+
    <Border
+
      variant="ghost"
+
      styleFlexDirection="column"
+
      styleGap="2px"
+
      styleBackgroundColor="var(--color-background-float)">
+
      <Link
+
        styleWidth="100%"
+
        underline={false}
+
        route={{ resource: "repo.patches", rid: repo.rid }}>
+
        <div class="tab active">
+
          <div class="global-flex"><Icon name="patch" />Patches</div>
+
          <div class="global-counter">
+
            {project.meta.patches.draft +
+
              project.meta.patches.open +
+
              project.meta.patches.archived +
+
              project.meta.patches.merged}
+
          </div>
+
        </div>
+
      </Link>
+

+
      <Link
+
        styleWidth="100%"
+
        underline={false}
+
        route={{
+
          resource: "repo.patches",
+
          rid: repo.rid,
+
          status: "open",
+
        }}>
+
        <div class="tab" class:active={status === "open"}>
+
          <div
+
            class="global-flex"
+
            class:open={["open", undefined].includes(status)}>
+
            <Icon name="patch" />
+
            Open
+
          </div>
+
          <div class="global-counter" class:highlight={status === undefined}>
+
            {project.meta.patches.open}
+
          </div>
+
        </div>
+
      </Link>
+

+
      <Link
+
        styleWidth="100%"
+
        underline={false}
+
        route={{
+
          resource: "repo.patches",
+
          rid: repo.rid,
+
          status: "merged",
+
        }}>
+
        <div class="tab" class:active={status === "merged"}>
+
          <div
+
            class="global-flex"
+
            class:merged={["merged", undefined].includes(status)}>
+
            <Icon name="patch" />Merged
+
          </div>
+
          <div class="global-counter" class:highlight={status === undefined}>
+
            {project.meta.patches.merged}
+
          </div>
+
        </div>
+
      </Link>
+

+
      <Link
+
        styleWidth="100%"
+
        underline={false}
+
        route={{
+
          resource: "repo.patches",
+
          rid: repo.rid,
+
          status: "archived",
+
        }}>
+
        <div class="tab" class:active={status === "archived"}>
+
          <div
+
            class="global-flex"
+
            class:archived={["archived", undefined].includes(status)}>
+
            <Icon name="patch" />Archived
+
          </div>
+
          <div class="global-counter" class:highlight={status === undefined}>
+
            {project.meta.patches.archived}
+
          </div>
+
        </div>
+
      </Link>
+
      <Link
+
        styleWidth="100%"
+
        underline={false}
+
        route={{
+
          resource: "repo.patches",
+
          rid: repo.rid,
+
          status: "draft",
+
        }}>
+
        <div class="tab" class:active={status === "draft"}>
+
          <div
+
            class="global-flex"
+
            class:draft={["draft", undefined].includes(status)}>
+
            <Icon name="patch" />
+
            Draft
+
          </div>
+
          <div class="global-counter" class:highlight={status === undefined}>
+
            {project.meta.patches.draft}
+
          </div>
+
        </div>
+
      </Link>
+
    </Border>
+
  </div>
+

+
  <Settings
+
    compact={false}
+
    popoverProps={{
+
      popoverPositionBottom: "3rem",
+
      popoverPositionLeft: "0",
+
    }} />
+
</div>
modified src/components/RepoHeader.svelte
@@ -6,16 +6,9 @@
  interface Props {
    repo: RepoInfo;
    selfDid: string;
-
    emphasizedTitle?: boolean;
-
    showLabels?: boolean;
  }

-
  const {
-
    repo,
-
    selfDid,
-
    emphasizedTitle = true,
-
    showLabels = true,
-
  }: Props = $props();
+
  const { repo, selfDid }: Props = $props();

  const project = $derived(repo.payloads["xyz.radicle.project"]!);
</script>
@@ -37,22 +30,14 @@
      style:background-color="var(--color-fill-ghost)">
      {project.data.name[0]}
    </div>
-
    {#if emphasizedTitle}
-
      <span
-
        title={project.data.name}
-
        class="txt-regular txt-overflow txt-semibold">
-
        {project.data.name}
-
      </span>
-
    {:else}
-
      <span
-
        title={project.data.name}
-
        class="txt-small txt-overflow txt-semibold">
-
        {project.data.name}
-
      </span>
-
    {/if}
+
    <span
+
      title={project.data.name}
+
      class="txt-regular txt-overflow txt-semibold">
+
      {project.data.name}
+
    </span>
  </div>
  <div class="global-flex">
-
    {#if showLabels && repo.visibility.type === "private"}
+
    {#if repo.visibility.type === "private"}
      <div
        class="global-counter"
        style:padding="0"
@@ -62,7 +47,7 @@
        </div>
      </div>
    {/if}
-
    {#if showLabels && repo.delegates.find(x => x.did === selfDid)}
+
    {#if repo.delegates.find(x => x.did === selfDid)}
      <div
        class="global-counter"
        style:padding="0"
added src/components/RepoTeaser.svelte
@@ -0,0 +1,36 @@
+
<script lang="ts">
+
  import Icon from "@app/components/Icon.svelte";
+

+
  interface Props {
+
    name: string;
+
    seeding: number;
+
  }
+

+
  const { name, seeding }: Props = $props();
+
</script>
+

+
<style>
+
  .teaser {
+
    align-items: center;
+
    height: 34px;
+
    margin: 0 4px 1rem 12px;
+
  }
+

+
  .seeding {
+
    margin-left: auto;
+
    padding: 0 6px;
+
    background-color: var(--color-fill-ghost);
+
    gap: 4px;
+
  }
+
</style>
+

+
<div class="global-flex teaser">
+
  <Icon name="repo" />
+
  <span title={name} class="txt-small txt-overflow txt-semibold">
+
    {name}
+
  </span>
+
  <div class="global-counter txt-small seeding">
+
    <Icon name="seedling-filled" />
+
    {seeding}
+
  </div>
+
</div>
added src/components/Settings.svelte
@@ -0,0 +1,56 @@
+
<script lang="ts">
+
  import type { ComponentProps } from "svelte";
+

+
  import AnnounceSwitch from "./AnnounceSwitch.svelte";
+
  import Border from "./Border.svelte";
+
  import Icon from "./Icon.svelte";
+
  import NakedButton from "./NakedButton.svelte";
+
  import Popover from "./Popover.svelte";
+
  import ThemeSwitch from "./ThemeSwitch.svelte";
+

+
  interface Props {
+
    compact?: boolean;
+
    styleHeight?: string;
+
    popoverProps: Partial<ComponentProps<typeof Popover>>;
+
  }
+

+
  const {
+
    compact = true,
+
    styleHeight = "40px",
+
    popoverProps,
+
  }: Props = $props();
+
</script>
+

+
<Popover {...popoverProps}>
+
  {#snippet toggle(onclick)}
+
    <NakedButton title="Settings" variant="ghost" {onclick} {styleHeight}>
+
      <Icon name="settings" />
+
      {#if !compact}
+
        Settings
+
      {/if}
+
    </NakedButton>
+
  {/snippet}
+
  {#snippet popover()}
+
    <Border variant="ghost" stylePadding="0.5rem 1rem" styleWidth="27rem">
+
      <div
+
        class="global-flex"
+
        style:flex-direction="column"
+
        style:align-items="flex-start"
+
        style:gap="1rem"
+
        style:width="100%">
+
        <div
+
          class="global-flex"
+
          style:justify-content="space-between"
+
          style:width="100%">
+
          Theme <ThemeSwitch />
+
        </div>
+
        <div
+
          class="global-flex"
+
          style:justify-content="space-between"
+
          style:width="100%">
+
          Announce changes <AnnounceSwitch />
+
        </div>
+
      </div>
+
    </Border>
+
  {/snippet}
+
</Popover>
added src/components/Sidebar.svelte
@@ -0,0 +1,107 @@
+
<script lang="ts">
+
  import * as router from "@app/lib/router";
+

+
  import Icon from "@app/components/Icon.svelte";
+
  import Settings from "@app/components/Settings.svelte";
+
  import Border from "./Border.svelte";
+

+
  interface Props {
+
    activeTab: "issues" | "patches";
+
    rid: string;
+
    activeIconColor?: string;
+
  }
+

+
  const { activeTab, rid, activeIconColor }: Props = $props();
+
</script>
+

+
<style>
+
  .sidebar-button {
+
    cursor: pointer;
+
    border: 0;
+
    background: none;
+
    height: 40px;
+
    width: 40px;
+
    clip-path: var(--2px-corner-fill);
+
    margin: 0;
+
    display: flex;
+
    align-items: center;
+
    justify-content: center;
+
    color: var(--color-foreground-contrast);
+
    background-color: var(--color-background-float);
+
  }
+

+
  .sidebar-button:hover {
+
    background-color: var(--color-fill-float-hover);
+
  }
+
</style>
+

+
<div class="global-flex" style:flex-direction="column" style:gap="0.5rem">
+
  <div class="global-flex" style:margin-bottom="5px" style:height="40px">
+
    <Icon name="repo" />
+
  </div>
+
  {#if activeTab === "issues"}
+
    <Border
+
      styleCursor="pointer"
+
      onclick={() => {
+
        void router.push({
+
          resource: "repo.issues",
+
          rid,
+
          status: "open",
+
        });
+
      }}
+
      variant="ghost"
+
      styleWidth="40px"
+
      styleHeight="40px"
+
      styleJustifyContent="center">
+
      <div style:color={activeIconColor}><Icon name="issue" /></div>
+
    </Border>
+
  {:else}
+
    <button
+
      class="sidebar-button"
+
      onclick={() => {
+
        void router.push({
+
          resource: "repo.issues",
+
          rid,
+
          status: "open",
+
        });
+
      }}>
+
      <Icon name="issue" />
+
    </button>
+
  {/if}
+

+
  {#if activeTab === "patches"}
+
    <Border
+
      styleCursor="pointer"
+
      onclick={() => {
+
        void router.push({
+
          resource: "repo.patches",
+
          rid,
+
          status: "open",
+
        });
+
      }}
+
      variant="ghost"
+
      styleWidth="40px"
+
      styleHeight="40px"
+
      styleJustifyContent="center">
+
      <div style:color={activeIconColor}><Icon name="patch" /></div>
+
    </Border>
+
  {:else}
+
    <button
+
      class="sidebar-button"
+
      onclick={() => {
+
        void router.push({
+
          resource: "repo.patches",
+
          rid,
+
          status: "open",
+
        });
+
      }}>
+
      <Icon name="patch" />
+
    </button>
+
  {/if}
+
</div>
+

+
<Settings
+
  popoverProps={{
+
    popoverPositionBottom: "3rem",
+
    popoverPositionLeft: "0",
+
  }} />
modified src/views/Home.svelte
@@ -6,8 +6,9 @@

  import CopyableId from "@app/components/CopyableId.svelte";
  import Header from "@app/components/Header.svelte";
-
  import RepoCard from "@app/components/RepoCard.svelte";
  import NodeId from "@app/components/NodeId.svelte";
+
  import RepoCard from "@app/components/RepoCard.svelte";
+
  import Settings from "@app/components/Settings.svelte";

  interface Props {
    repos: RepoInfo[];
@@ -51,6 +52,14 @@
          styleFontFamily="var(--font-family-sans-serif)"
          styleFontSize="var(--font-size-tiny)" />
      {/snippet}
+
      {#snippet settingsButton()}
+
        <Settings
+
          styleHeight="32px"
+
          popoverProps={{
+
            popoverPositionRight: "0",
+
            popoverPositionTop: "2.5rem",
+
          }} />
+
      {/snippet}
    </Header>
  </div>
  <div style:padding="1rem">
modified src/views/repo/CreateIssue.svelte
@@ -6,20 +6,21 @@

  import { invoke } from "@app/lib/invoke";

-
  import { issueStatusColor } from "@app/lib/utils";
  import * as router from "@app/lib/router";

-
  import Border from "@app/components/Border.svelte";
  import Button from "@app/components/Button.svelte";
  import Icon from "@app/components/Icon.svelte";
  import InlineTitle from "@app/components/InlineTitle.svelte";
-
  import Layout from "./Layout.svelte";
+
  import IssueSecondColumn from "@app/components/IssueSecondColumn.svelte";
  import Link from "@app/components/Link.svelte";
+
  import Markdown from "@app/components/Markdown.svelte";
  import NodeId from "@app/components/NodeId.svelte";
  import OutlineButton from "@app/components/OutlineButton.svelte";
+
  import Sidebar from "@app/components/Sidebar.svelte";
  import TextInput from "@app/components/TextInput.svelte";
  import Textarea from "@app/components/Textarea.svelte";
-
  import Markdown from "@app/components/Markdown.svelte";
+

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

  interface Props {
    repo: RepoInfo;
@@ -63,17 +64,6 @@
    margin-top: 0.35rem;
    margin-bottom: 1rem;
  }
-
  .issue-teaser {
-
    max-width: 11rem;
-
    white-space: nowrap;
-
  }
-
  .issue-list {
-
    margin-top: 0.5rem;
-
    display: flex;
-
    flex-direction: column;
-
    gap: 0.5rem;
-
    padding-bottom: 1rem;
-
  }
  .content {
    padding: 0 1rem 1rem 1rem;
    height: calc(100% - 8rem);
@@ -110,48 +100,11 @@
  {/snippet}

  {#snippet sidebar()}
-
    <Border
-
      hoverable={false}
-
      variant="ghost"
-
      styleWidth="100%"
-
      styleHeight="32px">
-
      <div style:margin-left="0.5rem">
-
        <Icon name="issue" />
-
      </div>
-
      <span class="txt-small txt-semibold">Issues</span>
-
      <div class="global-flex txt-small" style:margin-left="auto">
-
        <div
-
          class="global-counter"
-
          style:padding="0 6px"
-
          style:background-color="var(--color-fill-ghost)"
-
          style:gap="4px">
-
          {project.meta.issues.open + project.meta.issues.closed}
-
        </div>
-
      </div>
-
    </Border>
+
    <Sidebar activeTab="issues" rid={repo.rid} />
+
  {/snippet}

-
    <div class="issue-list">
-
      {#each issues as sidebarIssue}
-
        <Link
-
          variant="tab"
-
          route={{
-
            resource: "repo.issue",
-
            rid: repo.rid,
-
            issue: sidebarIssue.id,
-
          }}>
-
          <div class="global-flex">
-
            <div
-
              style:color={issueStatusColor[sidebarIssue.state.status]}
-
              style:margin-left="2px">
-
              <Icon name="issue" />
-
            </div>
-
            <span class="txt-small issue-teaser txt-overflow">
-
              <InlineTitle content={sidebarIssue.title} fontSize="small" />
-
            </span>
-
          </div>
-
        </Link>
-
      {/each}
-
    </div>
+
  {#snippet secondColumn()}
+
    <IssueSecondColumn {repo} {issues} />
  {/snippet}

  <div class="content">
modified src/views/repo/Issue.svelte
@@ -11,23 +11,19 @@
  import { tick } from "svelte";

  import * as roles from "@app/lib/roles";
-
  import {
-
    issueStatusColor,
-
    publicKeyFromDid,
-
    scrollIntoView,
-
  } from "@app/lib/utils";
  import { invoke } from "@app/lib/invoke";
+
  import { publicKeyFromDid, scrollIntoView } from "@app/lib/utils";

  import { announce } from "@app/components/AnnounceSwitch.svelte";

-
  import Border from "@app/components/Border.svelte";
-
  import IssueStateButton from "@app/components/IssueStateButton.svelte";
  import CommentComponent from "@app/components/Comment.svelte";
  import CommentToggleInput from "@app/components/CommentToggleInput.svelte";
  import CopyableId from "@app/components/CopyableId.svelte";
  import Icon from "@app/components/Icon.svelte";
  import InlineTitle from "@app/components/InlineTitle.svelte";
  import IssueMetadata from "@app/components/IssueMetadata.svelte";
+
  import IssueSecondColumn from "@app/components/IssueSecondColumn.svelte";
+
  import IssueStateButton from "@app/components/IssueStateButton.svelte";
  import IssueTimelineLifecycleAction from "@app/components/IssueTimelineLifecycleAction.svelte";
  import Link from "@app/components/Link.svelte";
  import NodeId from "@app/components/NodeId.svelte";
@@ -35,6 +31,7 @@
  import ThreadComponent from "@app/components/Thread.svelte";

  import Layout from "./Layout.svelte";
+
  import Sidebar from "@app/components/Sidebar.svelte";

  interface Props {
    repo: RepoInfo;
@@ -62,9 +59,9 @@
  let updatedTitle = $state(issue.title);

  // The view doesn't get destroyed when we switch between different issues in
-
  // the sidebar and because of that the top-level state gets retained when the
-
  // issue changes. This reactive statement makes sure we always load the new
-
  // issue and reset the state to defaults.
+
  // the second column and because of that the top-level state gets retained
+
  // when the issue changes. This reactive statement makes sure we always load
+
  // the new issue and reset the state to defaults.
  let issueId = issue.id;
  $effect(() => {
    if (issueId !== issue.id) {
@@ -177,7 +174,7 @@
        opts: { announce: $announce },
      });
      issue.title = updatedTitle;
-
      // Update sidebar issue title without reloading the whole issue list.
+
      // Update second colum issue title without reloading the whole issue list.
      const issueIndex = issues.findIndex(i => i.id === issue.id);
      if (issueIndex !== -1) {
        issues[issueIndex].title = updatedTitle;
@@ -226,7 +223,7 @@
        },
        opts: { announce: $announce },
      });
-
      // Update sidebar issue icon without reloading the whole issue list.
+
      // Update second column issue icon without reloading the whole issue list.
      const issueIndex = issues.findIndex(i => i.id === issue.id);
      if (issueIndex !== -1) {
        issues[issueIndex].state = state;
@@ -267,19 +264,8 @@
    height: 100%;
    top: 0;
  }
-
  .issue-teaser {
-
    max-width: 11rem;
-
    white-space: nowrap;
-
  }
-
  .issue-list {
-
    margin-top: 0.5rem;
-
    display: flex;
-
    flex-direction: column;
-
    gap: 0.5rem;
-
    padding-bottom: 1rem;
-
  }
  .content {
-
    padding: 0 1rem 1rem 1rem;
+
    padding: 0 1rem 1rem 0;
  }
  .connector {
    width: 2px;
@@ -312,7 +298,11 @@
      </div>
    </Link>
    <Icon name="chevron-right" />
-
    Issues
+
    <Link route={{ resource: "repo.issues", rid: repo.rid, status: "open" }}>
+
      Issues
+
    </Link>
+
    <Icon name="chevron-right" />
+
    {issue.title}
  {/snippet}

  {#snippet headerCenter()}
@@ -320,48 +310,11 @@
  {/snippet}

  {#snippet sidebar()}
-
    <Border
-
      hoverable={false}
-
      variant="ghost"
-
      styleWidth="100%"
-
      styleHeight="32px">
-
      <div style:margin-left="0.5rem">
-
        <Icon name="issue" />
-
      </div>
-
      <span class="txt-small txt-semibold">Issues</span>
-
      <div class="global-flex txt-small" style:margin-left="auto">
-
        <div
-
          class="global-counter"
-
          style:padding="0 6px"
-
          style:background-color="var(--color-fill-ghost)"
-
          style:gap="4px">
-
          {project.meta.issues.open + project.meta.issues.closed}
-
        </div>
-
      </div>
-
    </Border>
-

-
    <div class="issue-list">
-
      {#each issues as sidebarIssue}
-
        <Link
-
          variant={sidebarIssue.id === issue.id ? "active" : "tab"}
-
          route={{
-
            resource: "repo.issue",
-
            rid: repo.rid,
-
            issue: sidebarIssue.id,
-
          }}>
-
          <div class="global-flex">
-
            <div
-
              style:color={issueStatusColor[sidebarIssue.state.status]}
-
              style:margin-left="2px">
-
              <Icon name="issue" />
-
            </div>
-
            <span class="txt-small issue-teaser txt-overflow">
-
              <InlineTitle content={sidebarIssue.title} fontSize="small" />
-
            </span>
-
          </div>
-
        </Link>
-
      {/each}
-
    </div>
+
    <Sidebar activeTab="issues" rid={repo.rid} />
+
  {/snippet}
+

+
  {#snippet secondColumn()}
+
    <IssueSecondColumn {repo} selectedIssueId={issue.id} {issues} />
  {/snippet}

  <div class="content">
@@ -426,6 +379,7 @@
          ? issue.body.edits.at(-1)
          : undefined}
        author={issue.body.author}
+
        caption="opened"
        reactions={issue.body.reactions}
        timestamp={issue.body.edits.slice(-1)[0].timestamp}
        body={issue.body.edits.slice(-1)[0].body}
modified src/views/repo/Issues.svelte
@@ -5,17 +5,18 @@
  import type { RepoInfo } from "@bindings/repo/RepoInfo";

  import * as router from "@app/lib/router";
+
  import { issueStatusColor } from "@app/lib/utils";

  import Layout from "./Layout.svelte";

-
  import Border from "@app/components/Border.svelte";
+
  import Button from "@app/components/Button.svelte";
  import CopyableId from "@app/components/CopyableId.svelte";
  import Icon from "@app/components/Icon.svelte";
  import IssueTeaser from "@app/components/IssueTeaser.svelte";
+
  import IssuesSecondColumn from "@app/components/IssuesSecondColumn.svelte";
  import Link from "@app/components/Link.svelte";
  import NodeId from "@app/components/NodeId.svelte";
-
  import RepoHeader from "@app/components/RepoHeader.svelte";
-
  import Button from "@app/components/Button.svelte";
+
  import Sidebar from "@app/components/Sidebar.svelte";

  interface Props {
    repo: RepoInfo;
@@ -34,7 +35,7 @@
    display: flex;
    flex-direction: column;
    gap: 2px;
-
    padding: 0 1rem 1rem 1rem;
+
    padding: 0 1rem 1rem 0;
  }
  .header {
    font-weight: var(--font-weight-medium);
@@ -46,7 +47,7 @@
  }
</style>

-
<Layout>
+
<Layout hideSidebar styleSecondColumnOverflow="visible">
  {#snippet breadcrumbs()}
    <Link route={{ resource: "home" }}>
      <NodeId
@@ -70,61 +71,17 @@
  {/snippet}

  {#snippet sidebar()}
-
    <Border
-
      hoverable={false}
-
      variant="ghost"
-
      styleWidth="100%"
-
      styleHeight="32px">
-
      <RepoHeader
-
        {repo}
-
        showLabels={false}
-
        selfDid={`did:key:${config.publicKey}`}
-
        emphasizedTitle={false} />
-
    </Border>
-

-
    <div class="global-flex txt-small" style:margin="0.5rem 0">
-
      <Link
-
        variant={status === "all" ? "active" : "tab"}
-
        route={{ resource: "repo.issues", rid: repo.rid, status: "all" }}>
-
        <div class="global-flex"><Icon name="issue" />Issues</div>
-
        <div class="global-counter">
-
          {project.meta.issues.open + project.meta.issues.closed}
-
        </div>
-
      </Link>
-
    </div>
-
    <div class="global-flex txt-small global-tab">
-
      <Link
-
        variant={status === "open" ? "active" : "tab"}
-
        route={{ resource: "repo.issues", rid: repo.rid, status: "open" }}>
-
        Open
-
        <div class="global-counter">
-
          {project.meta.issues.open}
-
        </div>
-
      </Link>
-
      <Link
-
        variant={status === "closed" ? "active" : "tab"}
-
        route={{
-
          resource: "repo.issues",
-
          rid: repo.rid,
-
          status: "closed",
-
        }}>
-
        Closed
-
        <div class="global-counter">
-
          {project.meta.issues.closed}
-
        </div>
-
      </Link>
-
    </div>
+
    <Sidebar
+
      activeTab="issues"
+
      rid={repo.rid}
+
      activeIconColor={status !== "all"
+
        ? issueStatusColor[status]
+
        : undefined} />
+
  {/snippet}

-
    <div class="global-flex txt-small" style:margin="0.5rem 0">
-
      <Link variant="tab" route={{ resource: "repo.patches", rid: repo.rid }}>
-
        <div class="global-flex"><Icon name="patch" />Patches</div>
-
        <div class="global-counter">
-
          {project.meta.patches.draft +
-
            project.meta.patches.open +
-
            project.meta.patches.archived +
-
            project.meta.patches.merged}
-
        </div>
-
      </Link>
+
  {#snippet secondColumn()}
+
    <div style:margin-left="1rem" style:height="100%">
+
      <IssuesSecondColumn {project} {status} {repo} />
    </div>
  {/snippet}

@@ -143,13 +100,14 @@
      </Button>
    </div>
  </div>
+

  <div class="list">
    {#each issues as issue}
      <IssueTeaser {issue} rid={repo.rid} />
    {/each}

    {#if issues.length === 0}
-
      <div class="txt-missing txt-small">
+
      <div class="txt-missing txt-small" style:margin-left="1rem">
        {#if status === "all"}
          No issues.
        {:else}
modified src/views/repo/Layout.svelte
@@ -1,43 +1,85 @@
<script lang="ts">
  import type { Snippet } from "svelte";
+
  type LayoutState = "one-column" | "two-column";

  import { onMount } from "svelte";

  import Header from "@app/components/Header.svelte";
  import Icon from "@app/components/Icon.svelte";
-
  import NakedButton from "@app/components/NakedButton.svelte";

  interface Props {
    children: Snippet;
    breadcrumbs: Snippet;
    headerCenter?: Snippet;
+
    secondColumn: Snippet;
    sidebar: Snippet;
-
    loadMore?: () => Promise<void>;
+
    loadMoreContent?: () => Promise<void>;
+
    loadMoreSecondColumn?: () => Promise<void>;
+
    hideSidebar?: boolean;
+
    styleSecondColumnOverflow?: string;
  }

  const {
    children,
    breadcrumbs,
    headerCenter = undefined,
+
    secondColumn,
    sidebar,
-
    loadMore = undefined,
+
    loadMoreContent = undefined,
+
    loadMoreSecondColumn = undefined,
+
    hideSidebar = false,
+
    styleSecondColumnOverflow = "scroll",
  }: Props = $props();

-
  let hidden = $state(false);
-
  let listElement: HTMLElement | undefined = $state();
-
  let loading = false;
+
  const LAYOUT_KEY = "one-column-layout-enabled";
+

+
  let oneColumnLayout = $state(
+
    localStorage ? localStorage.getItem(LAYOUT_KEY) === "one-column" : false,
+
  );
+
  let contentContainer: HTMLElement | undefined = $state();
+
  let secondColumnContainer: HTMLElement | undefined = $state();
+
  let loadingContent = false;
+
  let loadingSecondColumn = false;
+

+
  function storeLayout(newValue: LayoutState): void {
+
    oneColumnLayout = newValue === "one-column";
+
    if (localStorage) {
+
      localStorage.setItem(LAYOUT_KEY, newValue);
+
    } else {
+
      console.warn(
+
        "localStorage isn't available, not able to persist the selected layout settings without it.",
+
      );
+
    }
+
  }

  onMount(() => {
-
    if (listElement && loadMore) {
-
      listElement.addEventListener("scroll", async () => {
+
    if (contentContainer && loadMoreContent) {
+
      contentContainer.addEventListener("scroll", async () => {
        if (
-
          listElement &&
-
          listElement.scrollTop + listElement.clientHeight >=
-
            listElement.scrollHeight - 600 &&
-
          loading === false
+
          contentContainer &&
+
          contentContainer.scrollTop + contentContainer.clientHeight >=
+
            contentContainer.scrollHeight - 600 &&
+
          loadingContent === false
        ) {
-
          loading = true;
-
          void loadMore().finally(() => (loading = false));
+
          loadingContent = true;
+
          void loadMoreContent().finally(() => (loadingContent = false));
+
        }
+
      });
+
    }
+

+
    if (secondColumnContainer && loadMoreSecondColumn) {
+
      secondColumnContainer.addEventListener("scroll", async () => {
+
        if (
+
          secondColumnContainer &&
+
          secondColumnContainer.scrollTop +
+
            secondColumnContainer.clientHeight >=
+
            secondColumnContainer.scrollHeight - 600 &&
+
          loadingSecondColumn === false
+
        ) {
+
          loadingSecondColumn = true;
+
          void loadMoreSecondColumn().finally(
+
            () => (loadingSecondColumn = false),
+
          );
        }
      });
    }
@@ -47,7 +89,8 @@
<style>
  .layout {
    display: grid;
-
    grid-template: auto 1fr auto / auto 1fr auto;
+
    grid-template-columns: auto auto 1fr auto;
+
    grid-template-rows: auto 1fr auto;
    height: 100%;
  }

@@ -58,45 +101,97 @@

  .sidebar {
    grid-column: 1 / 2;
-
    margin: 1rem 0.5rem 0 1rem;
+
    width: 40px;
+
    margin: 0 1rem;
+
    display: flex;
+
    flex-direction: column;
+
    align-items: center;
+
    justify-content: space-between;
+
    margin-top: 13px;
+
    margin-bottom: 1rem;
+
  }
+

+
  .secondColumn {
+
    grid-column: 2 / 3;
+
    margin: 1rem 0 0 0;
+
    max-width: 28rem;
    min-width: 14rem;
-
    overflow: scroll;
+
    margin-right: 1rem;
  }

  .content {
    padding-top: 1rem;
-
    grid-column: 2 / 3;
+
    grid-column: 3 / 4;
+
    width: 100%;
    overflow: scroll;
    overscroll-behavior: none;
  }

-
  .hidden {
-
    display: none;
+
  .column-radio {
+
    display: flex;
+
    background-color: var(--color-background-dip);
+
    clip-path: var(--1px-corner-fill);
+
    gap: 2px;
+
  }
+
  .toggle {
+
    cursor: pointer;
+
    border: 0;
+
    height: 24px;
+
    clip-path: var(--1px-corner-fill);
+
    margin: 0;
+
    background-color: var(--color-fill-ghost);
+
    color: var(--color-foreground-active);
+
  }
+
  .toggle:hover,
+
  .toggle.active {
+
    background: none;
+
    color: var(--color-foreground-emphasized);
  }
</style>

<div class="layout">
  <div class="header">
    <Header {breadcrumbs} center={headerCenter}>
-
      {#snippet iconLeft()}
-
        <NakedButton
-
          variant="ghost"
-
          onclick={() => {
-
            hidden = !hidden;
-
          }}>
-
          <Icon name="sidebar" />
-
        </NakedButton>
+
      {#snippet columnSwitch()}
+
        <div class="column-radio">
+
          <button
+
            class="toggle"
+
            class:active={oneColumnLayout}
+
            onclick={() => {
+
              storeLayout("one-column");
+
            }}>
+
            <Icon name="one" />
+
          </button>
+
          <button
+
            class="toggle"
+
            class:active={!oneColumnLayout}
+
            onclick={() => {
+
              storeLayout("two-column");
+
            }}>
+
            <Icon name="two" />
+
          </button>
+
        </div>
      {/snippet}
    </Header>
  </div>

-
  <div class="sidebar" class:hidden>
+
  <div
+
    class="sidebar"
+
    style:display={hideSidebar && !oneColumnLayout ? "none" : "flex"}>
    {@render sidebar()}
  </div>

  <div
+
    class="secondColumn"
+
    bind:this={secondColumnContainer}
+
    style:display={oneColumnLayout ? "none" : undefined}
+
    style:overflow={styleSecondColumnOverflow}>
+
    {@render secondColumn()}
+
  </div>
+

+
  <div
    class="content global-reset-scroll-after-navigate"
-
    bind:this={listElement}>
+
    bind:this={contentContainer}>
    {@render children()}
  </div>
</div>
modified src/views/repo/Patch.svelte
@@ -1,34 +1,69 @@
<script lang="ts">
  import type { Config } from "@bindings/config/Config";
+
  import type { PaginatedQuery } from "@bindings/cob/PaginatedQuery";
  import type { Patch } from "@bindings/cob/patch/Patch";
  import type { RepoInfo } from "@bindings/repo/RepoInfo";
  import type { Revision } from "@bindings/cob/patch/Revision";

-
  import {
-
    authorForNodeId,
-
    formatTimestamp,
-
    patchStatusColor,
-
  } from "@app/lib/utils";
+
  import { invoke } from "@app/lib/invoke";

-
  import Border from "@app/components/Border.svelte";
+
  import CommentComponent from "@app/components/Comment.svelte";
  import CopyableId from "@app/components/CopyableId.svelte";
  import Icon from "@app/components/Icon.svelte";
+
  import Id from "@app/components/Id.svelte";
  import InlineTitle from "@app/components/InlineTitle.svelte";
  import Layout from "./Layout.svelte";
  import Link from "@app/components/Link.svelte";
  import NodeId from "@app/components/NodeId.svelte";
-
  import Markdown from "@app/components/Markdown.svelte";
-
  import Id from "@app/components/Id.svelte";
+
  import PatchTeaser from "@app/components/PatchTeaser.svelte";
+
  import Sidebar from "@app/components/Sidebar.svelte";

  interface Props {
    repo: RepoInfo;
    patch: Patch;
-
    patches: Patch[];
+
    patches: PaginatedQuery<Patch[]>;
    revisions: Revision[];
    config: Config;
  }

-
  const { repo, patch, patches, revisions, config }: Props = $props();
+
  /* eslint-disable prefer-const */
+
  let { repo, patch, patches, revisions, config }: Props = $props();
+
  /* eslint-enable prefer-const */
+

+
  let items = $state(patches.content);
+
  let cursor = patches.cursor;
+
  let more = patches.more;
+

+
  $effect(() => {
+
    items = patches.content;
+
    cursor = patches.cursor;
+
    more = patches.more;
+
  });
+

+
  async function loadPatch(rid: string, patchId: string) {
+
    patch = await invoke<Patch>("patch_by_id", {
+
      rid: rid,
+
      id: patchId,
+
    });
+
    revisions = await invoke<Revision[]>("revisions_by_patch", {
+
      rid: rid,
+
      id: patchId,
+
    });
+
  }
+

+
  async function loadMoreSecondColumn() {
+
    if (more) {
+
      const p = await invoke<PaginatedQuery<Patch[]>>("list_patches", {
+
        rid: repo.rid,
+
        skip: cursor + 20,
+
        take: 20,
+
      });
+

+
      cursor = p.cursor;
+
      more = p.more;
+
      items = [...items, ...p.content];
+
    }
+
  }

  const project = $derived(repo.payloads["xyz.radicle.project"]!);
</script>
@@ -42,28 +77,36 @@
    margin-bottom: 1rem;
    margin-top: 0.35rem;
  }
-
  .patch-teaser {
-
    max-width: 11rem;
-
    white-space: nowrap;
-
  }
  .patch-list {
    margin-top: 0.5rem;
    display: flex;
    flex-direction: column;
-
    gap: 0.5rem;
+
    gap: 2px;
    padding-bottom: 1rem;
  }
  .content {
-
    padding: 0 1rem 1rem 1rem;
+
    padding: 0 1rem 1rem 0;
  }

-
  .body {
+
  .patch-body {
+
    margin-top: 1rem;
+
    position: relative;
+
  }
+
  /* We put the background and clip-path in a separate element to prevent
+
     popovers being clipped in the main element. */
+
  .patch-body::after {
+
    position: absolute;
+
    z-index: -1;
+
    content: " ";
    background-color: var(--color-background-float);
-
    padding: 1rem;
+
    clip-path: var(--2px-corner-fill);
+
    width: 100%;
+
    height: 100%;
+
    top: 0;
  }
</style>

-
<Layout>
+
<Layout {loadMoreSecondColumn}>
  {#snippet breadcrumbs()}
    <Link route={{ resource: "home" }}>
      <NodeId
@@ -79,7 +122,11 @@
      </div>
    </Link>
    <Icon name="chevron-right" />
-
    Patches
+
    <Link route={{ resource: "repo.patches", rid: repo.rid, status: "open" }}>
+
      Patches
+
    </Link>
+
    <Icon name="chevron-right" />
+
    {patch.title}
  {/snippet}

  {#snippet headerCenter()}
@@ -87,49 +134,24 @@
  {/snippet}

  {#snippet sidebar()}
-
    <Border
-
      hoverable={false}
-
      variant="ghost"
-
      styleWidth="100%"
-
      styleHeight="32px">
-
      <div style:margin-left="0.5rem">
-
        <Icon name="patch" />
-
      </div>
-
      <span class="txt-small txt-semibold">Patches</span>
-
      <div class="global-flex txt-small" style:margin-left="auto">
-
        <div
-
          class="global-counter"
-
          style:padding="0 6px"
-
          style:background-color="var(--color-fill-ghost)"
-
          style:gap="4px">
-
          {project.meta.patches.draft +
-
            project.meta.patches.open +
-
            project.meta.patches.merged +
-
            project.meta.patches.archived}
-
        </div>
-
      </div>
-
    </Border>
+
    <Sidebar activeTab="patches" rid={repo.rid} />
+
  {/snippet}

+
  {#snippet secondColumn()}
+
    <div
+
      style:height="34px"
+
      class="global-flex txt-medium"
+
      style:font-weight="var(--font-weight-medium)">
+
      Patches
+
    </div>
    <div class="patch-list">
-
      {#each patches as sidebarPatch}
-
        <Link
-
          variant="tab"
-
          route={{
-
            resource: "repo.patch",
-
            rid: repo.rid,
-
            patch: sidebarPatch.id,
-
          }}>
-
          <div class="global-flex">
-
            <div
-
              style:color={patchStatusColor[sidebarPatch.state.status]}
-
              style:margin-left="2px">
-
              <Icon name="patch" />
-
            </div>
-
            <span class="txt-small patch-teaser txt-overflow">
-
              <InlineTitle content={sidebarPatch.title} fontSize="small" />
-
            </span>
-
          </div>
-
        </Link>
+
      {#each items as p}
+
        <PatchTeaser
+
          compact
+
          {loadPatch}
+
          patch={p}
+
          rid={repo.rid}
+
          selected={patch && p.id === patch.id} />
      {/each}
    </div>
  {/snippet}
@@ -138,27 +160,23 @@
    <div class="title">
      <InlineTitle content={patch.title} fontSize="medium" />
    </div>
-
    <div class="txt-small body">
-
      {#if revisions[0].description.slice(-1)[0].body !== ""}
-
        <Markdown
-
          rid={repo.rid}
-
          breaks
-
          content={revisions[0].description.slice(-1)[0].body} />
-
      {:else}
-
        <span class="txt-missing" style:line-height="1.625rem">
-
          No description.
-
        </span>
-
      {/if}
-
      <div class="global-flex txt-small" style:margin-top="1.5rem">
-
        <NodeId {...authorForNodeId(patch.author)} />
-
        opened
-
        <Id id={patch.id} variant="oid" />
-
        {formatTimestamp(patch.timestamp)}
-
      </div>
+
    <div class="txt-small patch-body">
+
      <CommentComponent
+
        caption="opened"
+
        rid={repo.rid}
+
        id={patch.id}
+
        lastEdit={revisions[0].description.length > 1
+
          ? revisions[0].description.at(-1)
+
          : undefined}
+
        author={revisions[0].author}
+
        reactions={revisions[0].reactions}
+
        timestamp={revisions[0].description.slice(-1)[0].timestamp}
+
        body={revisions[0].description.slice(-1)[0].body}>
+
      </CommentComponent>
    </div>
    <div class="txt-small" style:margin-top="1rem">Revisions</div>
    {#each revisions as revision}
-
      <Id id={revision.id} variant="oid" />
+
      <div><Id id={revision.id} variant="oid" /></div>
    {/each}
  </div>
</Layout>
modified src/views/repo/Patches.svelte
@@ -6,15 +6,16 @@
  import type { RepoInfo } from "@bindings/repo/RepoInfo";

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

-
  import Layout from "./Layout.svelte";
-
  import Border from "@app/components/Border.svelte";
  import CopyableId from "@app/components/CopyableId.svelte";
  import Icon from "@app/components/Icon.svelte";
+
  import Layout from "./Layout.svelte";
  import Link from "@app/components/Link.svelte";
  import NodeId from "@app/components/NodeId.svelte";
  import PatchTeaser from "@app/components/PatchTeaser.svelte";
-
  import RepoHeader from "@app/components/RepoHeader.svelte";
+
  import PatchesSecondColumn from "@app/components/PatchesSecondColumn.svelte";
+
  import Sidebar from "@app/components/Sidebar.svelte";

  interface Props {
    repo: RepoInfo;
@@ -35,7 +36,7 @@
    more = patches.more;
  });

-
  async function loadMore() {
+
  async function loadMoreContent() {
    if (more) {
      const p = await invoke<PaginatedQuery<Patch[]>>("list_patches", {
        rid: repo.rid,
@@ -58,11 +59,19 @@
    display: flex;
    flex-direction: column;
    gap: 2px;
+
    padding: 0 1rem 1rem 0;
+
  }
+
  .header {
+
    font-weight: var(--font-weight-medium);
+
    font-size: var(--font-size-medium);
    padding: 0 1rem 1rem 1rem;
+
    display: flex;
+
    align-items: center;
+
    height: 50px;
  }
</style>

-
<Layout {loadMore}>
+
<Layout {loadMoreContent} hideSidebar styleSecondColumnOverflow="visible">
  {#snippet breadcrumbs()}
    <Link route={{ resource: "home" }}>
      <NodeId
@@ -86,92 +95,29 @@
  {/snippet}

  {#snippet sidebar()}
-
    <Border
-
      hoverable={false}
-
      variant="ghost"
-
      styleWidth="100%"
-
      styleHeight="32px">
-
      <RepoHeader
-
        {repo}
-
        showLabels={false}
-
        selfDid={`did:key:${config.publicKey}`}
-
        emphasizedTitle={false} />
-
    </Border>
-

-
    <div class="global-flex txt-small" style:margin="0.5rem 0">
-
      <Link
-
        variant="tab"
-
        route={{ resource: "repo.issues", rid: repo.rid, status: "all" }}>
-
        <div class="global-flex"><Icon name="issue" />Issues</div>
-
        <div class="global-counter">
-
          {project.meta.issues.open + project.meta.issues.closed}
-
        </div>
-
      </Link>
-
    </div>
-
    <div class="global-flex txt-small" style:margin="0.5rem 0">
-
      <Link
-
        variant={status === undefined ? "active" : "tab"}
-
        route={{ resource: "repo.patches", rid: repo.rid }}>
-
        <div class="global-flex"><Icon name="patch" />Patches</div>
-
        <div class="global-counter">
-
          {project.meta.patches.draft +
-
            project.meta.patches.open +
-
            project.meta.patches.archived +
-
            project.meta.patches.merged}
-
        </div>
-
      </Link>
-
    </div>
-
    <div class="global-flex txt-small global-tab">
-
      <Link
-
        variant={status === "draft" ? "active" : "tab"}
-
        route={{
-
          resource: "repo.patches",
-
          rid: repo.rid,
-
          status: "draft",
-
        }}>
-
        Draft <div class="global-counter">
-
          {project.meta.patches.draft}
-
        </div>
-
      </Link>
-
      <Link
-
        variant={status === "open" ? "active" : "tab"}
-
        route={{ resource: "repo.patches", rid: repo.rid, status: "open" }}>
-
        Open <div class="global-counter">
-
          {project.meta.patches.open}
-
        </div>
-
      </Link>
-
      <Link
-
        variant={status === "archived" ? "active" : "tab"}
-
        route={{
-
          resource: "repo.patches",
-
          rid: repo.rid,
-
          status: "archived",
-
        }}>
-
        Archived <div class="global-counter">
-
          {project.meta.patches.archived}
-
        </div>
-
      </Link>
-
      <Link
-
        variant={status === "merged" ? "active" : "tab"}
-
        route={{
-
          resource: "repo.patches",
-
          rid: repo.rid,
-
          status: "merged",
-
        }}>
-
        Merged <div class="global-counter">
-
          {project.meta.patches.merged}
-
        </div>
-
      </Link>
+
    <Sidebar
+
      activeTab="patches"
+
      rid={repo.rid}
+
      activeIconColor={status !== undefined
+
        ? patchStatusColor[status]
+
        : undefined} />
+
  {/snippet}
+

+
  {#snippet secondColumn()}
+
    <div style:margin-left="1rem" style:height="100%">
+
      <PatchesSecondColumn {project} {status} {repo} />
    </div>
  {/snippet}

+
  <div class="header">Patches</div>
+

  <div class="list">
    {#each items as patch}
      <PatchTeaser rid={repo.rid} {patch} />
    {/each}

    {#if patches.content.length === 0}
-
      <div class="txt-missing txt-small">
+
      <div class="txt-missing txt-small" style:margin-left="1rem">
        {#if status === undefined}
          No patches.
        {:else}
modified src/views/repo/router.ts
@@ -74,7 +74,7 @@ export interface LoadedRepoPatchRoute {
    repo: RepoInfo;
    config: Config;
    patch: Patch;
-
    patches: Patch[];
+
    patches: PaginatedQuery<Patch[]>;
    revisions: Revision[];
  };
}
@@ -131,7 +131,7 @@ export async function loadPatch(

  return {
    resource: "repo.patch",
-
    params: { repo, config, patch, patches: patches.content, revisions },
+
    params: { repo, config, patch, patches, revisions },
  };
}

modified tests/e2e/repo/issue.spec.ts
@@ -46,7 +46,7 @@ test("creation of top level comments", async ({ page }) => {
    page.getByText("Make sure that comment creation is working").last(),
  ).toBeVisible();
  await expect(
-
    page.getByRole("link", { name: "icon-issue Make sure that" }),
+
    page.getByRole("button", { name: "icon-issue Make sure that" }),
  ).toBeVisible();
  await expect(
    page