Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
Add sidebar
Merged rudolfs opened 1 year ago
16 files changed +590 -273 4f63f9e8 f27974c2
modified public/index.css
@@ -34,3 +34,96 @@ body {
  align-items: center;
  gap: 0.5rem;
}
+

+
.global-tab {
+
  padding-left: 0.5rem;
+
  border-left: 1px solid var(--color-fill-ghost-hover);
+
  margin-left: 1rem;
+
  flex-direction: column;
+
  justify-content: space-between;
+
  align-items: flex-start;
+
  column-gap: 0.5rem;
+
}
+

+
.global-counter {
+
  display: flex;
+
  align-items: center;
+
  justify-content: center;
+
  background-color: var(--color-fill-ghost-hover);
+
  clip-path: var(--1px-corner-fill);
+
  height: 1.5rem;
+
  padding: 0 0.5rem;
+
  min-width: 1.5rem;
+
}
+

+
:root {
+
  --1px-corner-fill: polygon(
+
    0 2px,
+
    2px 2px,
+
    2px 0,
+
    calc(100% - 2px) 0,
+
    calc(100% - 2px) 2px,
+
    100% 2px,
+
    100% calc(100% - 2px),
+
    calc(100% - 2px) calc(100% - 2px),
+
    calc(100% - 2px) calc(100% - 2px),
+
    calc(100% - 2px) 100%,
+
    2px 100%,
+
    2px calc(100% - 2px),
+
    0 calc(100% - 2px)
+
  );
+

+
  --2px-corner-fill: polygon(
+
    0 4px,
+
    2px 4px,
+
    2px 2px,
+
    4px 2px,
+
    4px 0,
+
    calc(100% - 4px) 0,
+
    calc(100% - 4px) 2px,
+
    calc(100% - 2px) 2px,
+
    calc(100% - 2px) 4px,
+
    100% 4px,
+
    100% calc(100% - 4px),
+
    calc(100% - 2px) calc(100% - 4px),
+
    calc(100% - 2px) calc(100% - 2px),
+
    calc(100% - 4px) calc(100% - 2px),
+
    calc(100% - 4px) 100%,
+
    4px 100%,
+
    4px calc(100% - 2px),
+
    2px calc(100% - 2px),
+
    2px calc(100% - 4px),
+
    0 calc(100% - 4px)
+
  );
+

+
  --3px-corner-fill: polygon(
+
    0 6px,
+
    2px 6px,
+
    2px 4px,
+
    4px 4px,
+
    4px 2px,
+
    6px 2px,
+
    6px 0,
+
    calc(100% - 6px) 0,
+
    calc(100% - 6px) 2px,
+
    calc(100% - 4px) 2px,
+
    calc(100% - 4px) 4px,
+
    calc(100% - 2px) 4px,
+
    calc(100% - 2px) 6px,
+
    100% 6px,
+
    100% calc(100% - 6px),
+
    calc(100% - 2px) calc(100% - 6px),
+
    calc(100% - 2px) calc(100% - 4px),
+
    calc(100% - 4px) calc(100% - 4px),
+
    calc(100% - 4px) calc(100% - 2px),
+
    calc(100% - 6px) calc(100% - 2px),
+
    calc(100% - 6px) 100%,
+
    6px 100%,
+
    6px calc(100% - 2px),
+
    4px calc(100% - 2px),
+
    4px calc(100% - 4px),
+
    2px calc(100% - 4px),
+
    2px calc(100% - 6px),
+
    0 calc(100% - 6px)
+
  );
+
}
modified src/components/Avatar.svelte
@@ -23,22 +23,7 @@
    background-repeat: no-repeat;
    width: 1rem;
    height: 1rem;
-
    clip-path: polygon(
-
      0 2px,
-
      2px 2px,
-
      2px 0,
-
      calc(100% - 2px) 0,
-
      calc(100% - 2px) 2px,
-
      100% 2px,
-
      100% calc(100% - 2px),
-
      calc(100% - 2px) calc(100% - 2px),
-
      calc(100% - 2px) calc(100% - 2px),
-
      calc(100% - 2px) 100%,
-
      2px 100%,
-
      2px calc(100% - 2px),
-
      0 calc(100% - 2px)
-
    );
-
    background-color: red;
+
    clip-path: var(--1px-corner-fill);
  }
</style>

modified src/components/Border.svelte
@@ -7,6 +7,7 @@
  export let styleHeight: string | undefined = undefined;
  export let styleMinHeight: string | undefined = undefined;
  export let styleWidth: string | undefined = undefined;
+
  export let styleCursor: "default" | "pointer" = "default";

  $: style =
    `--local-button-color-1: var(--color-fill-${variant});` +
@@ -110,7 +111,6 @@
  }

  .container {
-
    cursor: pointer;
    white-space: nowrap;

    -webkit-touch-callout: none;
@@ -150,6 +150,7 @@
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
  style:width={styleWidth}
+
  style:cursor={styleCursor}
  class="container"
  {onclick}
  role="button"
deleted src/components/Fill.svelte
@@ -1,105 +0,0 @@
-
<script lang="ts">
-
  export let variant:
-
    | "delegate"
-
    | "ghost"
-
    | "primary"
-
    | "private"
-
    | "secondary"
-
    | "transparent";
-
  export let stylePadding: string | undefined = undefined;
-
  export let styleHeight: string | undefined = undefined;
-
  export let styleWidth: string | undefined = undefined;
-
  export let onclick: (() => void) | undefined = undefined;
-

-
  $: style =
-
    variant === "transparent"
-
      ? "--button-color-1: transparent"
-
      : `--button-color-1: var(--color-fill-${variant});`;
-
</script>
-

-
<style>
-
  .pixel {
-
    background-color: var(--button-color-1);
-
  }
-

-
  .p1-1 {
-
    grid-area: p1-1;
-
    background-color: transparent;
-
  }
-
  .p1-2 {
-
    grid-area: p1-2;
-
  }
-
  .p1-3 {
-
    grid-area: p1-3;
-
    background-color: transparent;
-
  }
-

-
  .p2-1 {
-
    grid-area: p2-1;
-
  }
-
  .p2-2 {
-
    grid-area: p2-2;
-
    display: flex;
-
    align-items: center;
-
    gap: 0.5rem;
-
    justify-content: center;
-
  }
-
  .p2-3 {
-
    grid-area: p2-3;
-
  }
-

-
  .p3-1 {
-
    grid-area: p3-1;
-
    background-color: transparent;
-
  }
-
  .p3-2 {
-
    grid-area: p3-2;
-
  }
-
  .p3-3 {
-
    grid-area: p3-3;
-
    background-color: transparent;
-
  }
-

-
  .container {
-
    cursor: pointer;
-
    white-space: nowrap;
-

-
    -webkit-touch-callout: none;
-
    -webkit-user-select: none;
-
    user-select: none;
-

-
    column-gap: 0;
-
    row-gap: 0;
-
    display: grid;
-
    grid-template-columns: 2px auto 2px;
-
    grid-template-rows: 2px auto 2px;
-
    grid-template-areas:
-
      "p1-1 p1-2 p1-3"
-
      "p2-1 p2-2 p2-3"
-
      "p3-1 p3-2 p3-3";
-
  }
-
</style>
-

-
<!-- svelte-ignore a11y-click-events-have-key-events -->
-
<div
-
  class="container"
-
  role="button"
-
  tabindex="0"
-
  {onclick}
-
  {style}
-
  style:width={styleWidth}
-
  style:height={styleHeight}>
-
  <div class="pixel p1-1"></div>
-
  <div class="pixel p1-2"></div>
-
  <div class="pixel p1-3"></div>
-

-
  <div class="pixel p2-1"></div>
-
  <div class="pixel p2-2" style:padding={stylePadding}>
-
    <slot />
-
  </div>
-
  <div class="pixel p2-3"></div>
-

-
  <div class="pixel p3-1"></div>
-
  <div class="pixel p3-2"></div>
-
  <div class="pixel p3-3"></div>
-
</div>
modified src/components/Icon.svelte
@@ -24,6 +24,7 @@
    | "plus"
    | "repo"
    | "seedling"
+
    | "seedling-filled"
    | "settings"
    | "sidebar"
    | "sun"
@@ -355,6 +356,35 @@
    <path d="M7.33301 8V7L6.33301 7L6.33301 8H7.33301Z" />
    <path d="M7.33301 9L7.33301 7L6.33301 7L6.33301 9H7.33301Z" />
    <path d="M6.33301 10V9H5.33301L5.33301 10H6.33301Z" />
+
  {:else if name === "seedling-filled"}
+
    <path d="M10 6H9V5L10 5V6Z" />
+
    <path d="M11 5L10 5V4L11 4V5Z" />
+
    <path d="M12 4H11L11 3L12 3L12 4Z" />
+
    <path d="M13 4L12 4L12 3L13 3L13 4Z" />
+
    <path d="M14 5L13 5V4L14 4V5Z" />
+
    <path d="M13 6H12V5H13V6Z" />
+
    <path d="M5 7L5 6L4 6L4 7L5 7Z" />
+
    <path d="M4 8V7L3 7L3 8L4 8Z" />
+
    <path d="M3 9L3 8L2 8L2 9H3Z" />
+
    <path d="M3 10L3 9H2L2 10H3Z" />
+
    <path d="M4 11L4 10H3L3 11L4 11Z" />
+
    <path d="M10 7H9L9 6H10L10 7Z" />
+
    <path d="M9 8H8V7L9 7L9 8Z" />
+
    <path d="M8 8H7V7H8V8Z" />
+
    <path d="M11 8L10 8L10 7L11 7V8Z" />
+
    <path d="M12 8L10 8L10 7L12 7V8Z" />
+
    <path d="M13 7H12V6H13V7Z" />
+
    <path d="M5 10L5 9H4L4 10L5 10Z" />
+
    <path d="M9 8H8V11H9V8Z" />
+
    <path d="M8 11L7 11V14H8V11Z" />
+
    <path d="M6 7L6 6H5L5 7H6Z" />
+
    <path d="M7 8V7L6 7L6 8H7Z" />
+
    <path d="M7 9L7 7L6 7L6 9H7Z" />
+
    <path d="M6 10V9H5L5 10H6Z" />
+
    <path d="M4 7L6 7L6 9H4L4 7Z" />
+
    <path d="M3 8L4 8L4 10H3L3 8Z" />
+
    <path d="M11 4L13 4V5L11 5V4Z" />
+
    <path d="M10 5L12 5V7L10 7L10 5Z" />
  {:else if name === "settings"}
    <path d="M7 5H14V6H7V5Z" />
    <path d="M9 11L2 11L2 10L9 10V11Z" />
modified src/components/Link.svelte
@@ -5,6 +5,7 @@

  export let route: Route;
  export let disabled: boolean = false;
+
  export let variant: "active" | "regular" | "tab" = "regular";

  function navigateToRoute(event: MouseEvent): void {
    event.preventDefault();
@@ -21,13 +22,41 @@
    color: var(--color-foreground-contrast);
    text-decoration: none;
  }
-
  a:hover {
+
  .regular: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)}>
+
<a
+
  onclick={navigateToRoute}
+
  href={routeToPath(route)}
+
  class:regular={variant === "regular"}
+
  class:active={variant === "active"}
+
  class:tab={variant === "tab"}>
  <slot />
</a>
modified src/components/RepoCard.svelte
@@ -4,8 +4,8 @@
  import { formatRepositoryId, formatTimestamp } from "@app/lib/utils";

  import Border from "./Border.svelte";
-
  import Fill from "./Fill.svelte";
  import Icon from "./Icon.svelte";
+
  import RepoHeader from "./RepoHeader.svelte";

  export let repo: RepoInfo;
  export let selfDid: string;
@@ -15,9 +15,6 @@
</script>

<style>
-
  .header {
-
    justify-content: space-between;
-
  }
  .footer {
    margin-top: 1rem;
    justify-content: space-between;
@@ -34,40 +31,13 @@

<Border
  variant="ghost"
+
  styleCursor="pointer"
  styleWidth="100%"
  stylePadding="8px 12px"
  hoverable
  {onclick}>
  <div class="container txt-small">
-
    <div class="global-flex header">
-
      <div class="global-flex">
-
        <Fill styleWidth="1.5rem" styleHeight="24px" variant="ghost">
-
          {project.data.name[0]}
-
        </Fill>{project.data.name}
-
      </div>
-
      <div class="global-flex">
-
        {#if repo.visibility.type === "private"}
-
          <Fill variant="private" styleWidth="24px" styleHeight="24px">
-
            <div style:color="var(--color-foreground-yellow)">
-
              <Icon name="lock" />
-
            </div>
-
          </Fill>
-
        {/if}
-
        {#if repo.delegates.find(x => x.did === selfDid)}
-
          <Fill variant="delegate" styleWidth="24px" styleHeight="24px">
-
            <div style:color="var(--color-fill-primary)">
-
              <Icon name="delegate" />
-
            </div>
-
          </Fill>
-
        {/if}
-
        <div class="global-flex">
-
          <Fill variant="ghost" styleHeight="24px" stylePadding="0 4px">
-
            <Icon name="seedling" />
-
            {repo.seeding}
-
          </Fill>
-
        </div>
-
      </div>
-
    </div>
+
    <RepoHeader {repo} {selfDid} />

    <div class="title">
      {#if project.data.description}
added src/components/RepoHeader.svelte
@@ -0,0 +1,68 @@
+
<script lang="ts">
+
  import type { RepoInfo } from "@bindings/RepoInfo";
+

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

+
  export let repo: RepoInfo;
+
  export let selfDid: string;
+
  export let emphasizedTitle: boolean = true;
+

+
  $: project = repo.payloads["xyz.radicle.project"]!;
+
</script>
+

+
<style>
+
  .header {
+
    display: flex;
+
    align-items: center;
+
    justify-content: space-between;
+
    width: 100%;
+
    gap: 0.5rem;
+
  }
+
</style>
+

+
<div class="header txt-small">
+
  <div class="global-flex">
+
    <div
+
      class="global-counter"
+
      style:background-color="var(--color-fill-ghost)">
+
      {project.data.name[0]}
+
    </div>
+
    {#if emphasizedTitle}
+
      <span class="txt-regular txt-semibold">{project.data.name}</span>
+
    {:else}
+
      <span class="txt-small txt-semibold">{project.data.name}</span>
+
    {/if}
+
  </div>
+
  <div class="global-flex">
+
    {#if repo.visibility.type === "private"}
+
      <div
+
        class="global-counter"
+
        style:padding="0"
+
        style:background-color="var(--color-fill-private)">
+
        <div style:color="var(--color-foreground-yellow)">
+
          <Icon name="lock" />
+
        </div>
+
      </div>
+
    {/if}
+
    {#if repo.delegates.find(x => x.did === selfDid)}
+
      <div
+
        class="global-counter"
+
        style:padding="0"
+
        style:background-color="var(--color-fill-delegate)">
+
        <div style:color="var(--color-fill-primary)">
+
          <Icon name="delegate" />
+
        </div>
+
      </div>
+
    {/if}
+
    <div class="global-flex">
+
      <div
+
        class="global-counter"
+
        style:padding="0 6px"
+
        style:background-color="var(--color-fill-ghost)"
+
        style:gap="4px">
+
        <Icon name="seedling" />
+
        {repo.seeding}
+
      </div>
+
    </div>
+
  </div>
+
</div>
modified src/components/ThemeSwitch.svelte
@@ -30,36 +30,51 @@
  import { writable } from "svelte/store";

  import Border from "./Border.svelte";
-
  import Fill from "./Fill.svelte";
  import Icon from "./Icon.svelte";
</script>

-
<div style="display: flex; gap: 1rem;">
-
  <Border variant="secondary">
-
    <Fill
-
      stylePadding="0 0.5rem"
-
      variant={$theme === "dark" ? "secondary" : "transparent"}
-
      onclick={() => {
-
        storeTheme("dark");
-
      }}>
+
<style>
+
  button {
+
    cursor: pointer;
+
    display: flex;
+
    align-items: center;
+
    border: none;
+
    white-space: nowrap;
+
    touch-action: manipulation;
+
    clip-path: var(--1px-corner-fill);
+
    height: 24px;
+
    font-size: var(--font-size-small);
+
  }
+
</style>
+

+
<Border variant="secondary">
+
  <button
+
    style:background-color={$theme === "dark"
+
      ? "var(--color-fill-secondary)"
+
      : "transparent"}
+
    onclick={() => {
+
      storeTheme("dark");
+
    }}>
+
    <span style="display: flex; align-items: center; gap: 0.5rem">
      <Icon name="moon" />
      Dark
-
    </Fill>
+
    </span>
+
  </button>

-
    <Fill
-
      stylePadding="0 0.5rem"
-
      variant={$theme === "light" ? "secondary" : "transparent"}
-
      onclick={() => {
-
        storeTheme("light");
-
      }}>
-
      <span
-
        style="display: flex; align-items: center; gap: 0.5rem"
-
        style:color={$theme === "light"
-
          ? "var(--color-foreground-white)"
-
          : "var(--color-foreground-contrast)"}>
-
        <Icon name="sun" />
-
        Light
-
      </span>
-
    </Fill>
-
  </Border>
-
</div>
+
  <button
+
    style:background-color={$theme === "light"
+
      ? "var(--color-fill-secondary)"
+
      : "transparent"}
+
    onclick={() => {
+
      storeTheme("light");
+
    }}>
+
    <span
+
      style="display: flex; align-items: center; gap: 0.5rem"
+
      style:color={$theme === "light"
+
        ? "var(--color-foreground-white)"
+
        : "var(--color-foreground-contrast)"}>
+
      <Icon name="sun" />
+
      Light
+
    </span>
+
  </button>
+
</Border>
modified src/lib/router/definitions.ts
@@ -1,6 +1,11 @@
import type { Config } from "@bindings/Config";
import type { RepoInfo } from "@bindings/RepoInfo";
-
import type { LoadedRepoRoute, RepoRoute } from "@app/views/repo/router";
+
import type {
+
  RepoIssuesRoute,
+
  RepoPatchesRoute,
+
  LoadedRepoIssuesRoute,
+
  LoadedRepoPatchesRoute,
+
} from "@app/views/repo/router";

import { invoke } from "@tauri-apps/api/core";

@@ -31,13 +36,15 @@ export type Route =
  | AuthenticationErrorRoute
  | BootingRoute
  | HomeRoute
-
  | RepoRoute;
+
  | RepoIssuesRoute
+
  | RepoPatchesRoute;

export type LoadedRoute =
  | AuthenticationErrorRoute
  | BootingRoute
  | LoadedHomeRoute
-
  | LoadedRepoRoute;
+
  | LoadedRepoIssuesRoute
+
  | LoadedRepoPatchesRoute;

export async function loadRoute(
  route: Route,
modified src/lib/utils.ts
@@ -34,6 +34,10 @@ export function truncateId(pubkey: string): string {
  return `${pubkey.substring(0, 6)}…${pubkey.slice(-6)}`;
}

+
export function formatOid(id: string): string {
+
  return id.substring(0, 7);
+
}
+

export const formatTimestamp = (
  timestamp: number,
  current = new Date().getTime(),
modified src/views/Home.svelte
@@ -30,36 +30,7 @@
    background-position: center;
    background-size: cover;
    height: 9.5rem;
-
    clip-path: polygon(
-
      0 6px,
-
      2px 6px,
-
      2px 4px,
-
      4px 4px,
-
      4px 2px,
-
      6px 2px,
-
      6px 0,
-
      calc(100% - 6px) 0,
-
      calc(100% - 6px) 2px,
-
      calc(100% - 4px) 2px,
-
      calc(100% - 4px) 4px,
-
      calc(100% - 2px) 4px,
-
      calc(100% - 2px) 6px,
-
      100% 6px,
-
      100% calc(100% - 6px),
-
      calc(100% - 2px) calc(100% - 6px),
-
      calc(100% - 2px) calc(100% - 4px),
-
      calc(100% - 4px) calc(100% - 4px),
-
      calc(100% - 4px) calc(100% - 2px),
-
      calc(100% - 6px) calc(100% - 2px),
-
      calc(100% - 6px) 100%,
-
      6px 100%,
-
      6px calc(100% - 2px),
-
      4px calc(100% - 2px),
-
      4px calc(100% - 4px),
-
      2px calc(100% - 4px),
-
      2px calc(100% - 6px),
-
      0 calc(100% - 6px)
-
    );
+
    clip-path: var(--3px-corner-fill);
  }
</style>

modified src/views/repo/Issues.svelte
@@ -1,8 +1,11 @@
<script lang="ts">
  import type { Config } from "@bindings/Config";
  import type { Issue } from "@bindings/Issue";
+
  import type { IssueStatus } from "./router";
  import type { RepoInfo } from "@bindings/RepoInfo";

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

  import Layout from "./Layout.svelte";

  import Icon from "@app/components/Icon.svelte";
@@ -12,11 +15,28 @@
  export let repo: RepoInfo;
  export let issues: Issue[];
  export let config: Config;
+
  export let status: IssueStatus;

+
  const statusColor: Record<Issue["state"]["status"], string> = {
+
    open: "var(--color-fill-success)",
+
    closed: "var(--color-foreground-red)",
+
  };
+
  const statusBackgroundColor: Record<Issue["state"]["status"], string> = {
+
    open: "var(--color-fill-diff-green)",
+
    closed: "var(--color-fill-diff-red)",
+
  };
  $: project = repo.payloads["xyz.radicle.project"]!;
</script>

-
<Layout {repo}>
+
<style>
+
  .list {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 0.5rem;
+
  }
+
</style>
+

+
<Layout {repo} selfDid={`did:key:${config.publicKey}`}>
  <svelte:fragment slot="breadcrumbs">
    <Link route={{ resource: "home" }}>
      <NodeId
@@ -25,18 +45,86 @@
        styleFontFamily="var(--font-family-sans-serif)"
        styleFontSize="var(--font-size-tiny)" />
    </Link>
-
    <Icon name="chevron-right" />
-
    {project.data.name}
+
    <Link route={{ resource: "repo.issues", rid: repo.rid, status: "open" }}>
+
      <div class="global-flex">
+
        <Icon name="chevron-right" />
+
        {project.data.name}
+
      </div>
+
    </Link>
    <Icon name="chevron-right" />
    Issues
  </svelte:fragment>

-
  <pre>
-
    <!-- prettier-ignore -->
+
  <svelte:fragment slot="sidebar">
+
    <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>
+

+
    <div class="global-flex txt-small" style:margin="0.5rem 0">
+
      <Link
+
        variant="tab"
+
        route={{ resource: "repo.patches", rid: repo.rid, status: "all" }}>
+
        <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>
+
  </svelte:fragment>
+

+
  <div class="list">
    {#each issues as issue}
-
      - {issue.title}
-
    {:else}
-
      No issues.
+
      <div class="global-flex">
+
        <div
+
          class="global-counter"
+
          style:padding="0"
+
          style:color={statusColor[issue.state.status]}
+
          style:background-color={statusBackgroundColor[issue.state.status]}>
+
          <Icon name="issue" />
+
        </div>
+
        <div class="global-oid">{formatOid(issue.id)}</div>
+
        {issue.title}
+
      </div>
    {/each}
-
  </pre>
+

+
    {#if issues.length === 0}
+
      {#if status === "all"}
+
        No issues.
+
      {:else}
+
        No {status} issues.
+
      {/if}
+
    {/if}
+
  </div>
</Layout>
modified src/views/repo/Layout.svelte
@@ -1,27 +1,58 @@
<script lang="ts">
  import type { RepoInfo } from "@bindings/RepoInfo";

+
  import Border from "@app/components/Border.svelte";
  import CopyableId from "@app/components/CopyableId.svelte";
  import Header from "@app/components/Header.svelte";
  import Icon from "@app/components/Icon.svelte";
-
  import Link from "@app/components/Link.svelte";
  import NakedButton from "@app/components/NakedButton.svelte";
+
  import RepoHeader from "@app/components/RepoHeader.svelte";

  export let repo: RepoInfo;
+
  export let selfDid: string;
+

+
  let hidden = false;
</script>

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

  .header {
-
    position: sticky;
-
    top: 0;
+
    grid-column: 1 / 4;
+
    margin-bottom: 1rem;
+
  }
+

+
  .sidebar {
+
    grid-column: 1 / 2;
+
    margin-left: 1rem;
+
    margin-right: 1.5rem;
+
    min-width: 14rem;
+
  }
+

+
  .content {
+
    grid-column: 2 / 3;
+
    overflow: scroll;
+
    overscroll-behavior: none;
+
  }
+

+
  .hidden {
+
    display: none;
  }
</style>

-
<div style:height="fit-content">
+
<div class="layout">
  <div class="header">
    <Header>
      <svelte:fragment slot="icon-left">
-
        <NakedButton variant="ghost">
+
        <NakedButton
+
          variant="ghost"
+
          onclick={() => {
+
            hidden = !hidden;
+
          }}>
          <Icon name="sidebar" />
        </NakedButton>
      </svelte:fragment>
@@ -34,27 +65,19 @@
    </Header>
  </div>

-
  Issues
-
  <Link route={{ resource: "repo.issues", rid: repo.rid, status: "open" }}>
-
    Open
-
  </Link>
-
  <Link route={{ resource: "repo.issues", rid: repo.rid, status: "closed" }}>
-
    Closed
-
  </Link>
-

-
  <br />
-
  Patches
-
  <Link route={{ resource: "repo.patches", rid: repo.rid, status: "draft" }}>
-
    Draft
-
  </Link>
-
  <Link route={{ resource: "repo.patches", rid: repo.rid, status: "open" }}>
-
    Open
-
  </Link>
-
  <Link route={{ resource: "repo.patches", rid: repo.rid, status: "archived" }}>
-
    Archived
-
  </Link>
-
  <Link route={{ resource: "repo.patches", rid: repo.rid, status: "merged" }}>
-
    Merged
-
  </Link>
-
  <slot />
+
  <div class="sidebar" class:hidden>
+
    <Border
+
      hoverable={false}
+
      variant="ghost"
+
      styleWidth="100%"
+
      styleHeight="32px">
+
      <RepoHeader {repo} {selfDid} emphasizedTitle={false} />
+
    </Border>
+

+
    <slot name="sidebar" />
+
  </div>
+

+
  <div class="content">
+
    <slot />
+
  </div>
</div>
modified src/views/repo/Patches.svelte
@@ -1,8 +1,11 @@
<script lang="ts">
  import type { Config } from "@bindings/Config";
  import type { Patch } from "@bindings/Patch";
+
  import type { PatchStatus } from "./router";
  import type { RepoInfo } from "@bindings/RepoInfo";

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

  import Layout from "./Layout.svelte";

  import Icon from "@app/components/Icon.svelte";
@@ -12,11 +15,34 @@
  export let repo: RepoInfo;
  export let patches: Patch[];
  export let config: Config;
+
  export let status: PatchStatus;
+

+
  const statusColor: Record<Patch["state"]["status"], string> = {
+
    draft: "var(--color-fill-gray)",
+
    open: "var(--color-fill-success)",
+
    archived: "var(--color-foreground-yellow)",
+
    merged: "var(--color-fill-primary)",
+
  };
+

+
  const statusBackgroundColor: Record<Patch["state"]["status"], string> = {
+
    draft: "var(--color-fill-ghost)",
+
    open: "var(--color-fill-diff-green)",
+
    archived: "var(--color-fill-private)",
+
    merged: "var(--color-fill-delegate)",
+
  };

  $: project = repo.payloads["xyz.radicle.project"]!;
</script>

-
<Layout {repo}>
+
<style>
+
  .list {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 0.5rem;
+
  }
+
</style>
+

+
<Layout {repo} selfDid={`did:key:${config.publicKey}`}>
  <svelte:fragment slot="breadcrumbs">
    <Link route={{ resource: "home" }}>
      <NodeId
@@ -25,17 +51,105 @@
        styleFontFamily="var(--font-family-sans-serif)"
        styleFontSize="var(--font-size-tiny)" />
    </Link>
-
    <Icon name="chevron-right" />
-
    {project.data.name}
+
    <Link route={{ resource: "repo.issues", rid: repo.rid, status: "open" }}>
+
      <div class="global-flex">
+
        <Icon name="chevron-right" />
+
        {project.data.name}
+
      </div>
+
    </Link>
    <Icon name="chevron-right" />
    Patches
  </svelte:fragment>
-
  <pre>
-
    <!-- prettier-ignore -->
+

+
  <svelte:fragment slot="sidebar">
+
    <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 === "all" ? "active" : "tab"}
+
        route={{ resource: "repo.patches", rid: repo.rid, status: "all" }}>
+
        <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>
+
    </div>
+
  </svelte:fragment>
+

+
  <div class="list">
    {#each patches as patch}
-
      - {patch.title}
-
    {:else}
-
      No patches.
+
      <div class="global-flex">
+
        <div
+
          class="global-counter"
+
          style:padding="0"
+
          style:color={statusColor[patch.state.status]}
+
          style:background-color={statusBackgroundColor[patch.state.status]}>
+
          <Icon name="patch" />
+
        </div>
+
        <div class="global-oid">{formatOid(patch.id)}</div>
+
        {patch.title}
+
      </div>
    {/each}
-
  </pre>
+

+
    {#if patches.length === 0}
+
      {#if status === "all"}
+
        No patches.
+
      {:else}
+
        No {status} patches.
+
      {/if}
+
    {/if}
+
  </div>
</Layout>
modified src/views/repo/router.ts
@@ -6,32 +6,48 @@ import type { RepoInfo } from "@bindings/RepoInfo";
import { invoke } from "@tauri-apps/api/core";
import { unreachable } from "@app/lib/utils";

+
export type IssueStatus = "all" | Issue["state"]["status"];
+

export interface RepoIssuesRoute {
  resource: "repo.issues";
  rid: string;
-
  status?: "open" | "closed";
+
  status: IssueStatus;
}

export interface LoadedRepoIssuesRoute {
  resource: "repo.issues";
-
  params: { repo: RepoInfo; config: Config; issues: Issue[] };
+
  params: {
+
    repo: RepoInfo;
+
    config: Config;
+
    issues: Issue[];
+
    status: IssueStatus;
+
  };
}

+
export type PatchStatus = "all" | Patch["state"]["status"];
+

export interface RepoPatchesRoute {
  resource: "repo.patches";
  rid: string;
-
  status?: "draft" | "open" | "archived" | "merged";
+
  status: PatchStatus;
}

export interface LoadedRepoPatchesRoute {
  resource: "repo.patches";
-
  params: { repo: RepoInfo; config: Config; patches: Patch[] };
+
  params: {
+
    repo: RepoInfo;
+
    config: Config;
+
    patches: Patch[];
+
    status: PatchStatus;
+
  };
}

export type RepoRoute = RepoIssuesRoute | RepoPatchesRoute;
export type LoadedRepoRoute = LoadedRepoIssuesRoute | LoadedRepoPatchesRoute;

-
export async function loadPatches(route: RepoRoute): Promise<LoadedRepoRoute> {
+
export async function loadPatches(
+
  route: RepoPatchesRoute,
+
): Promise<LoadedRepoPatchesRoute> {
  const repo: RepoInfo = await invoke("repo_by_id", {
    rid: route.rid,
  });
@@ -41,10 +57,15 @@ export async function loadPatches(route: RepoRoute): Promise<LoadedRepoRoute> {
    status: route.status,
  });

-
  return { resource: "repo.patches", params: { repo, config, patches } };
+
  return {
+
    resource: "repo.patches",
+
    params: { repo, config, patches, status: route.status },
+
  };
}

-
export async function loadIssues(route: RepoRoute): Promise<LoadedRepoRoute> {
+
export async function loadIssues(
+
  route: RepoIssuesRoute,
+
): Promise<LoadedRepoIssuesRoute> {
  const repo: RepoInfo = await invoke("repo_by_id", {
    rid: route.rid,
  });
@@ -54,7 +75,10 @@ export async function loadIssues(route: RepoRoute): Promise<LoadedRepoRoute> {
    status: route.status,
  });

-
  return { resource: "repo.issues", params: { repo, config, issues } };
+
  return {
+
    resource: "repo.issues",
+
    params: { repo, config, issues, status: route.status },
+
  };
}

export function repoRouteToPath(route: RepoRoute): string {
@@ -94,7 +118,7 @@ export function repoUrlToRoute(
      if (status === "open" || status === "closed") {
        return { resource: "repo.issues", rid, status };
      } else {
-
        return { resource: "repo.issues", rid };
+
        return { resource: "repo.issues", rid, status: "all" };
      }
    } else if (resource === "patches") {
      const status = searchParams.get("status");
@@ -106,7 +130,7 @@ export function repoUrlToRoute(
      ) {
        return { resource: "repo.patches", rid, status };
      } else {
-
        return { resource: "repo.patches", rid };
+
        return { resource: "repo.patches", rid, status: "all" };
      }
    } else {
      return null;