Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Allow opening links in a new tab
Rūdolfs Ošiņš committed 3 years ago
commit 1f09d8a2a88da3419ddcddee894f08c9c24aec21
parent 518b82125060d6a4e53558dbc16bc8df8ec5b714
45 files changed +1004 -979
modified public/typography.css
@@ -144,14 +144,6 @@ p {
.txt-faded {
  color: var(--color-foreground-5);
}
-
.txt-title {
-
  font-size: var(--font-size-large);
-
  font-weight: var(--font-weight-normal);
-
  color: var(--color-secondary);
-
  text-align: left;
-
  text-overflow: ellipsis;
-
  overflow-x: hidden;
-
}
.txt-link {
  color: var(--color-foreground-6);
  text-decoration: none;
modified src/App/Header/SearchResultsModal.svelte
@@ -35,7 +35,7 @@
        {#each results as result}
          <li>
            <Link
-
              on:click={modal.hide}
+
              on:afterNavigate={modal.hide}
              route={{
                resource: "projects",
                params: {
modified src/App/Header/SettingsDropdown.svelte
@@ -96,7 +96,7 @@
    class="item selector"
    on:click|stopPropagation={() => (showFonts = !showFonts)}>
    <div>Code font</div>
-
    <Icon name={`chevron-${showFonts ? "up" : "down"}`} />
+
    <Icon name={`chevron-${showFonts ? "down" : "right"}`} />
  </div>
  {#if showFonts}
    <div
@@ -112,7 +112,7 @@
          style:font-family={font.fontFamily}>
          {font.displayName}
          {#if isSelectedFont}
-
            <Icon name="checkmark" />
+
            <Icon name="checkmark-small" />
          {/if}
        </div>
      {/each}
modified src/components/Authorship.svelte
@@ -17,6 +17,7 @@
    align-items: center;
    color: var(--color-foreground-6);
    padding: 0.125rem 0;
+
    gap: 0.25rem;
  }
  .id {
    overflow: hidden;
@@ -24,7 +25,6 @@
    white-space: nowrap;
  }
  .body {
-
    margin: 0 0.4rem;
    white-space: nowrap;
  }
</style>
modified src/components/Avatar.svelte
@@ -32,7 +32,6 @@
    display: inline-block !important;
    width: 1rem;
    height: 1rem;
-
    margin-right: 0.5rem;
  }
</style>

modified src/components/Chip.svelte
@@ -17,7 +17,7 @@
  .section {
    display: flex;
    align-items: center;
-
    max-width: 13rem;
+
    max-width: 13.5rem;
    padding: 0.2rem 0.5rem;
  }
  .text {
modified src/components/Clipboard.svelte
@@ -1,19 +1,30 @@
<script lang="ts" strictEvents>
+
  import debounce from "lodash/debounce";
  import { createEventDispatcher } from "svelte";

-
  import Icon from "@app/components/Icon.svelte";
  import { toClipboard } from "@app/lib/utils";

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

+
  export let text: string;
+
  export let small = false;
+
  export let tooltip: string | undefined = undefined;
+

  const dispatch = createEventDispatcher<{ copied: never }>();

+
  let icon: "clipboard-small" | "checkmark-small" | "clipboard" | "checkmark" =
+
    small ? "clipboard-small" : "clipboard";
+

+
  const restoreIcon = debounce(() => {
+
    icon = small ? "clipboard-small" : "clipboard";
+
  }, 800);
+

  const copy = () => {
    toClipboard(text);
    dispatch("copied");
+
    icon = small ? "checkmark-small" : "checkmark";
+
    restoreIcon();
  };
-

-
  export let text: string;
-
  export let small = false;
-
  export let tooltip: string | undefined = undefined;
</script>

<style>
@@ -24,6 +35,7 @@
    display: inline-flex;
    justify-content: center;
    align-items: center;
+
    user-select: none;
  }
  .clipboard.small {
    width: 1.5rem;
@@ -46,9 +58,5 @@
  class="clipboard"
  class:small
  on:click|stopPropagation={copy}>
-
  {#if small}
-
    <Icon name="clipboard-small" />
-
  {:else}
-
    <Icon name="clipboard" />
-
  {/if}
+
  <Icon name={icon} />
</span>
modified src/components/ErrorMessage.svelte
@@ -25,6 +25,7 @@
  {#if stackTrace}
    <div class="stack-trace">
      <Clipboard
+
        small
        tooltip="Copy error to clipboard"
        text={JSON.stringify({ errorMessage: message, stackTrace }, null, 2)} />
    </div>
modified src/components/Icon.svelte
@@ -6,6 +6,8 @@
    | "checkmark"
    | "checkmark-small"
    | "chevron-down"
+
    | "chevron-left"
+
    | "chevron-right"
    | "chevron-up"
    | "clipboard"
    | "clipboard-small"
@@ -36,41 +38,44 @@
  viewBox="0 0 24 24">
  {#if name === "browse"}
    <path
-
      d="M8.46934 7.23871C8.61151 7.10623 8.79956 7.03411 8.99386
-
    7.03753C9.18816 7.04096 9.37355 7.11967 9.51096 7.25709C9.64838 7.3945
-
    9.72709 7.57988 9.73052 7.77419C9.73394 7.96849 9.66182 8.15653 9.52934
-
    8.29871L5.80934 12.0187L9.52934 15.7387C9.60303 15.8074 9.66213 15.8902
-
    9.70312 15.9822C9.74411 16.0742 9.76615 16.1735 9.76793 16.2742C9.76971
-
    16.3749 9.75118 16.4749 9.71346 16.5683C9.67574 16.6617 9.6196 16.7465
-
    9.54838 16.8177C9.47716 16.889 9.39233 16.9451 9.29894 16.9828C9.20555
-
    17.0206 9.10552 17.0391 9.00482 17.0373C8.90411 17.0355 8.8048 17.0135
-
    8.7128 16.9725C8.6208 16.9315 8.538 16.8724 8.46934 16.7987L4.21934
-
    12.5487C4.07889 12.4081 4 12.2175 4 12.0187C4 11.82 4.07889 11.6293 4.21934
-
    11.4887L8.46934 7.23871V7.23871ZM15.0293 7.23871C14.9607 7.16502 14.8779
-
    7.10592 14.7859 7.06493C14.6939 7.02394 14.5946 7.00189 14.4939
-
    7.00012C14.3932 6.99834 14.2931 7.01686 14.1997 7.05459C14.1064 7.09231
-
    14.0215 7.14845 13.9503 7.21967C13.8791 7.29089 13.8229 7.37572 13.7852
-
    7.46911C13.7475 7.5625 13.729 7.66253 13.7307 7.76323C13.7325 7.86393
-
    13.7546 7.96325 13.7956 8.05525C13.8366 8.14725 13.8957 8.23005 13.9693
-
    8.29871L17.6893 12.0187L13.9693 15.7387C13.8957 15.8074 13.8366 15.8902
-
    13.7956 15.9822C13.7546 16.0742 13.7325 16.1735 13.7307 16.2742C13.729
-
    16.3749 13.7475 16.4749 13.7852 16.5683C13.8229 16.6617 13.8791 16.7465
-
    13.9503 16.8177C14.0215 16.889 14.1064 16.9451 14.1997 16.9828C14.2931
-
    17.0206 14.3932 17.0391 14.4939 17.0373C14.5946 17.0355 14.6939 17.0135
-
    14.7859 16.9725C14.8779 16.9315 14.9607 16.8724 15.0293 16.7987L19.2793
-
    12.5487C19.4198 12.4081 19.4987 12.2175 19.4987 12.0187C19.4987 11.82
-
    19.4198 11.6293 19.2793 11.4887L15.0293 7.23871V7.23871Z" />
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M9.35356 6.64645C9.54882 6.84171 9.54882 7.15829 9.35356
+
      7.35355L5.06066 11.6464C4.8654 11.8417 4.8654 12.1583 5.06066
+
      12.3536L9.35356 16.6464C9.54882 16.8417 9.54882 17.1583 9.35356
+
      17.3536C9.15829 17.5488 8.84171 17.5488 8.64645 17.3536L4.35355
+
      13.0607C3.76777 12.4749 3.76777 11.5251 4.35356 10.9393L8.64645
+
      6.64645C8.84171 6.45118 9.15829 6.45118 9.35356 6.64645ZM14.6464
+
      6.64645C14.8417 6.45118 15.1583 6.45118 15.3536 6.64645L19.6464
+
      10.9393C20.2322 11.5251 20.2322 12.4749 19.6464 13.0607L15.3536
+
      17.3536C15.1583 17.5488 14.8417 17.5488 14.6464 17.3536C14.4512 17.1583
+
      14.4512 16.8417 14.6464 16.6464L18.9393 12.3536C19.1346 12.1583 19.1346
+
      11.8417 18.9393 11.6464L14.6464 7.35355C14.4512 7.15829 14.4512 6.84171
+
      14.6464 6.64645Z" />
  {:else if name === "clipboard"}
    <path
-
      d="M9 5H14.7071L18 8.29289V17H9V5ZM10 6V16H17V9H14V6H10ZM15
-
    6.70711L16.2929 8H15V6.70711ZM7 8H8V18H15V19H7V8Z" />
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M8.5 6C8.5 5.17157 9.17157 4.5 10 4.5H18C18.8284 4.5 19.5 5.17157 19.5
+
      6V14C19.5 14.8284 18.8284 15.5 18 15.5H15.5V18C15.5 18.8284 14.8284 19.5
+
      14 19.5H6C5.17157 19.5 4.5 18.8284 4.5 18V10C4.5 9.17157 5.17157 8.5 6
+
      8.5H8.5V6ZM8.5 9.5H6C5.72386 9.5 5.5 9.72386 5.5 10V18C5.5 18.2761
+
      5.72386 18.5 6 18.5H14C14.2761 18.5 14.5 18.2761 14.5 18V15.5H10C9.17157
+
      15.5 8.5 14.8284 8.5 14V9.5ZM10 5.5C9.72386 5.5 9.5 5.72386 9.5 6V14C9.5
+
      14.2761 9.72386 14.5 10 14.5H18C18.2761 14.5 18.5 14.2761 18.5 14V6C18.5
+
      5.72386 18.2761 5.5 18 5.5H10Z" />
  {:else if name === "clipboard-small"}
    <path
      fill-rule="evenodd"
      clip-rule="evenodd"
-
      d="M14 7L17 10V16H10V7H14ZM13
-
    8H11V15H16V11H13V8ZM14 8.41421V10H15.5858L14 8.41421ZM8
-
    10H9V17H14V18H8V10Z" />
+
      d="M10 6.5C10 5.67157 10.6716 5 11.5 5H17.5C18.3284 5 19 5.67157 19
+
      6.5V12.5C19 13.3284 18.3284 14 17.5 14H15V16.5C15 17.3284 14.3284 18 13.5
+
      18H7.5C6.67157 18 6 17.3284 6 16.5V10.5C6 9.67157 6.67157 9 7.5
+
      9H10V6.5ZM10 10H7.5C7.22386 10 7 10.2239 7 10.5V16.5C7 16.7761 7.22386 17
+
      7.5 17H13.5C13.7761 17 14 16.7761 14 16.5V14H11.5C10.6716 14 10 13.3284
+
      10 12.5V10ZM11.5 13C11.2239 13 11 12.7761 11 12.5V6.5C11 6.22386 11.2239
+
      6 11.5 6H17.5C17.7761 6 18 6.22386 18 6.5V12.5C18 12.7761 17.7761 13 17.5
+
      13H11.5Z" />
  {:else if name === "chat"}
    <path
      fill-rule="evenodd"
@@ -88,161 +93,161 @@
    13.5H8C7.72386 13.5 7.5 13.2761 7.5 13Z" />
  {:else if name === "ellipsis"}
    <path
-
      d="M7 12a2 2 0 1 1-4.001-.001A2 2 0 0 1 7 12zm12-2a2 2 0 1 0 .001
-
    4.001A2 2 0 0 0 19 10zm-7 0a2 2 0 1 0 .001 4.001A2 2 0 0 0 12 10z" />
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M5.5 13C6.32843 13 7 12.3284 7 11.5C7 10.6716 6.32843 10 5.5
+
      10C4.67157 10 4 10.6716 4 11.5C4 12.3284 4.67157 13 5.5 13ZM13.5
+
      11.5C13.5 12.3284 12.8284 13 12 13C11.1716 13 10.5 12.3284 10.5 11.5C10.5
+
      10.6716 11.1716 10 12 10C12.8284 10 13.5 10.6716 13.5 11.5ZM20 11.5C20
+
      12.3284 19.3284 13 18.5 13C17.6716 13 17 12.3284 17 11.5C17 10.6716
+
      17.6716 10 18.5 10C19.3284 10 20 10.6716 20 11.5Z" />
  {:else if name === "chevron-down"}
    <path
      fill-rule="evenodd"
      clip-rule="evenodd"
-
      d="m 16.353549 9.8535548 c 0.19527 0.1952622 0.19527 0.5118432 0
-
      0.7071032 l -3.29289 3.2929 c -0.58579 0.58578 -1.53553 0.58578
-
      -2.12132 0 l -3.292894 -3.2929 c -0.195262 -0.19526 -0.195262
-
      -0.511841 0 -0.7071042 0.195263 -0.195262 0.511844 -0.195262
-
      0.707104 0 l 3.2929 3.2928942 c 0.19526 0.19526 0.51184 0.19526
-
      0.7071 0 l 3.2929 -3.2928932 c 0.19526 -0.195262 0.51184
-
      -0.195262 0.7071 0 z" />
+
      d="M16.3536 9.64645C16.5488 9.84171 16.5488 10.1583 16.3536
+
      10.3536L13.0607 13.6464C12.4749 14.2322 11.5251 14.2322 10.9393
+
      13.6464L7.64645 10.3536C7.45118 10.1583 7.45118 9.84171 7.64645
+
      9.64645C7.84171 9.45118 8.15829 9.45118 8.35355 9.64645L11.6464
+
      12.9393C11.8417 13.1346 12.1583 13.1346 12.3536 12.9393L15.6464
+
      9.64645C15.8417 9.45118 16.1583 9.45118 16.3536 9.64645Z" />
  {:else if name === "chevron-up"}
    <path
      fill-rule="evenodd"
      clip-rule="evenodd"
      d="M7.64645 14.3536C7.45118 14.1583 7.45118 13.8417 7.64645
-
      13.6464L10.9393 10.3536C11.5251 9.76777 12.4749 9.76777 13.0607
-
      10.3536L16.3536 13.6464C16.5488 13.8417 16.5488 14.1583 16.3536
-
      14.3536C16.1583 14.5488 15.8417 14.5488 15.6464 14.3536L12.3536
-
      11.0607C12.1583 10.8654 11.8417 10.8654 11.6464 11.0607L8.35355
-
      14.3536C8.15829 14.5488 7.84171 14.5488 7.64645 14.3536Z" />
+
         13.6464L10.9393 10.3536C11.5251 9.76777 12.4749 9.76777 13.0607
+
         10.3536L16.3536 13.6464C16.5488 13.8417 16.5488 14.1583 16.3536
+
         14.3536C16.1583 14.5488 15.8417 14.5488 15.6464 14.3536L12.3536
+
         11.0607C12.1583 10.8654 11.8417 10.8654 11.6464 11.0607L8.35355
+
         14.3536C8.15829 14.5488 7.84171 14.5488 7.64645 14.3536Z" />
+
  {:else if name === "chevron-left"}
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M14.3536 16.3536C14.1583 16.5488 13.8417 16.5488 13.6464
+
      16.3536L10.3536 13.0607C9.76777 12.4749 9.76777 11.5251 10.3536
+
      10.9393L13.6464 7.64645C13.8417 7.45119 14.1583 7.45119 14.3536
+
      7.64645C14.5488 7.84171 14.5488 8.15829 14.3536 8.35355L11.0607
+
      11.6464C10.8654 11.8417 10.8654 12.1583 11.0607 12.3536L14.3536
+
      15.6464C14.5488 15.8417 14.5488 16.1583 14.3536 16.3536Z" />
+
  {:else if name === "chevron-right"}
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M9.64645 7.64645C9.84171 7.45118 10.1583 7.45118 10.3536
+
      7.64645L13.6464 10.9393C14.2322 11.5251 14.2322 12.4749 13.6464
+
      13.0607L10.3536 16.3536C10.1583 16.5488 9.84171 16.5488 9.64645
+
      16.3536C9.45118 16.1583 9.45118 15.8417 9.64645 15.6464L12.9393
+
      12.3536C13.1346 12.1583 13.1346 11.8417 12.9393 11.6464L9.64645
+
      8.35355C9.45118 8.15829 9.45118 7.84171 9.64645 7.64645Z" />
  {:else if name === "gear"}
    <path
      fill-rule="evenodd"
      clip-rule="evenodd"
-
      d="M9.23219 2.01728C9.38084
-
    1.97716 9.53969 2.00775 9.66279 2.10022L11.5852 3.5442C11.8032 3.53214
-
    12.0218 3.53214 12.2398 3.5442L14.1622 2.10022C14.2851 2.00788 14.4437
-
    1.97724 14.5922 2.01712C15.4415 2.24523 16.2573 2.58349 17.0188
-
    3.02327C17.1517 3.10003 17.242 3.23359 17.2637 3.38554L17.5851
-
    5.63541L20.5219 6.58028C20.6402 6.61834 20.7401 6.69907 20.8021
-
    6.80675C21.2406 7.56841 21.5787 8.38367 21.8077 9.23219C21.8479 9.38084
-
    21.8173 9.53969 21.7248 9.66279L20.2747 11.5933C20.2763 11.6489 20.278
-
    11.7141 20.2792 11.7842C20.2816 11.9194 20.2827 12.0847 20.2779
-
    12.236L21.7248 14.1622C21.8171 14.2851 21.8478 14.4437 21.8079
-
    14.5922C21.5798 15.4415 21.2415 16.2573 20.8017 17.0188C20.7397 17.1262
-
    20.64 17.2067 20.5219 17.2447L17.5851 18.1896L17.2637 20.4395C17.242 20.5916
-
    17.1515 20.7253 17.0183 20.8021C16.2566 21.2406 15.4413 21.5787 14.5928
-
    21.8077C14.4442 21.8479 14.2853 21.8173 14.1622 21.7248L12.2398
-
    20.2808C12.0255 20.2927 11.8107 20.2929 11.5964 20.2814L9.72748
-
    21.8005C9.60295 21.9017 9.4374 21.937 9.28241 21.8954C8.41326 21.6619
-
    7.54437 21.228 6.8062 20.8017C6.67328 20.725 6.58299 20.5914 6.56128
-
    20.4395L6.22012 18.0514C6.06864 17.9052 5.91981 17.7564 5.77364
-
    17.6049L3.38554 17.2637C3.23337 17.242 3.09966 17.1515 3.02295
-
    17.0183C2.58438 16.2566 2.24635 15.4413 2.01728 14.5928C1.97716 14.4442
-
    2.00775 14.2853 2.10022 14.1622L3.55032 12.2317C3.54868 12.1761 3.54702
-
    12.1109 3.54579 12.0409C3.5434 11.9056 3.54229 11.7403 3.5471
-
    11.5891L2.10022 9.66279C2.00788 9.53986 1.97724 9.38129 2.01712
-
    9.23281C2.24523 8.38352 2.58349 7.56772 3.02327 6.8062C3.10003 6.67328
-
    3.23359 6.58299 3.38554 6.56128L5.77364 6.22012C5.91981 6.06864 6.06864
-
    5.91981 6.22012 5.77364L6.56128 3.38554C6.58302 3.23337 6.67354 3.09966
-
    6.80675 3.02295C7.56841 2.58438 8.38367 2.24635 9.23219 2.01728ZM4.55577
-
    12.3686C4.56087 12.4844 4.52562 12.5983 4.45604 12.6909L3.04873
-
    14.5645C3.22919 15.1693 3.47157 15.7538 3.77202 16.3088L6.08634
-
    16.6394C6.19815 16.6554 6.30125 16.7087 6.37886 16.7908C6.59125 17.0153
-
    6.8097 17.2338 7.03423 17.4461C7.11628 17.5238 7.16963 17.6269 7.1856
-
    17.7387L7.51653 20.0552C8.09055 20.3755 8.70317 20.6699 9.30047
-
    20.8589L11.119 19.3808C11.2199 19.2987 11.3489 19.2592 11.4784
-
    19.2707C11.7672 19.2962 12.0578 19.2962 12.3466 19.2707C12.4696 19.2598
-
    12.5922 19.2948 12.6909 19.369L14.5645 20.7763C15.1693 20.5958 15.7538
-
    20.3534 16.3088 20.053L16.6394 17.7387C16.6665 17.5493 16.7992 17.392
-
    16.9812 17.3334L20.0293 16.3527C20.3415 15.7851 20.5919 15.1857 20.7765
-
    14.5648L19.369 12.6909C19.2948 12.5922 19.2598 12.4696 19.2707
-
    12.3467C19.2822 12.2167 19.2829 12.0017 19.2794 11.8018C19.2777 11.7056
-
    19.2751 11.6187 19.273 11.5559L19.2703 11.4822L19.2695 11.4626L19.2693
-
    11.4577L19.2692 11.4566C19.2641 11.3409 19.2994 11.2267 19.369
-
    11.1341L20.7763 9.26051C20.591 8.63973 20.3405 8.04027 20.029
-
    7.47218L16.9812 6.4916C16.7992 6.43302 16.6665 6.27569 16.6394
-
    6.08634L16.3087 3.77166C15.7541 3.47068 15.1696 3.22832 14.5648
-
    3.04851L12.6909 4.45604C12.5922 4.53018 12.4696 4.56518 12.3466
-
    4.55431C12.0578 4.52877 11.7672 4.52877 11.4784 4.55431C11.3554 4.56518
-
    11.2328 4.53018 11.1341 4.45604L9.26051 3.04873C8.65575 3.22919 8.07123
-
    3.47157 7.51622 3.77202L7.1856 6.08634C7.16963 6.19815 7.11628 6.30125
-
    7.03423 6.37886C6.8097 6.59125 6.59125 6.8097 6.37886 7.03423C6.30125
-
    7.11628 6.19815 7.16963 6.08634 7.1856L3.77166 7.51627C3.47068 8.07088
-
    3.22832 8.65536 3.04851 9.26022L4.45604 11.1341C4.53016 11.2328 4.56517
-
    11.3554 4.55432 11.4783C4.54285 11.6083 4.5421 11.8233 4.54563
-
    12.0232C4.54733 12.1194 4.54988 12.2063 4.55201 12.2691L4.55471
-
    12.3429L4.55551 12.3624L4.55577 12.3686ZM11.9121 8.41248C9.97912 8.41248
-
    8.41211 9.97948 8.41211 11.9125C8.41211 13.8455 9.97912 15.4125 11.9121
-
    15.4125C13.8451 15.4125 15.4121 13.8455 15.4121 11.9125C15.4121 9.97948
-
    13.8451 8.41248 11.9121 8.41248ZM7.41211 11.9125C7.41211 9.4272 9.42683
-
    7.41248 11.9121 7.41248C14.3974 7.41248 16.4121 9.4272 16.4121
-
    11.9125C16.4121 14.3978 14.3974 16.4125 11.9121 16.4125C9.42683 16.4125
-
    7.41211 14.3978 7.41211 11.9125Z" />
+
      d="M12 7.5C9.51472 7.5 7.5 9.51472 7.5 12C7.5 14.4853 9.51472 16.5 12
+
      16.5C14.4853 16.5 16.5 14.4853 16.5 12C16.5 9.51472 14.4853 7.5 12
+
      7.5ZM8.5 12C8.5 10.067 10.067 8.5 12 8.5C13.933 8.5 15.5 10.067 15.5
+
      12C15.5 13.933 13.933 15.5 12 15.5C10.067 15.5 8.5 13.933 8.5 12Z" />
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M9.31972 2.10481C9.46836 2.06468 9.62721 2.09528 9.75032
+
      2.18775L11.6727 3.63172C11.8908 3.61967 12.1093 3.61967 12.3273
+
      3.63172L14.2497 2.18775C14.3727 2.09541 14.5312 2.06476 14.6797
+
      2.10464C15.529 2.33276 16.3448 2.67101 17.1063 3.1108C17.2392 3.18756
+
      17.3295 3.32112 17.3513 3.47307L17.692 5.85812C17.8472 6.00529 17.9972
+
      6.15528 18.1421 6.30811L20.527 6.6488C20.6792 6.67054 20.8129 6.76107
+
      20.8896 6.89428C21.1184 7.29169 21.3199 7.7037 21.4928 8.12741C21.6512
+
      8.51576 21.7857 8.91393 21.8952 9.31972C21.9354 9.46836 21.9048 9.62721
+
      21.8123 9.75032L20.2861 11.7822V12.2617L21.8123 14.2935C21.9047 14.4164
+
      21.9353 14.575 21.8954 14.7235C21.6673 15.5728 21.3291 16.3886 20.8893
+
      17.1501C20.8125 17.283 20.6789 17.3733 20.527 17.395L18.1389
+
      17.7362C17.9927 17.8877 17.8439 18.0365 17.6924 18.1827L17.3513
+
      20.5708C17.3295 20.7229 17.239 20.8566 17.1058 20.9333C16.3441 21.3719
+
      15.5289 21.7099 14.6803 21.939C14.5317 21.9791 14.3729 21.9485 14.2497
+
      21.8561L12.3273 20.4121C12.1093 20.4242 11.8908 20.4242 11.6727
+
      20.4121L9.75032 21.8561C9.62739 21.9484 9.46882 21.9791 9.32033
+
      21.9392C8.47105 21.7111 7.65525 21.3728 6.89373 20.933C6.76081 20.8563
+
      6.67052 20.7227 6.64881 20.5708L6.30809 18.1857C6.15289 18.0385 6.00284
+
      17.8885 5.85795 17.7357L3.47307 17.395C3.3209 17.3733 3.18719 17.2828
+
      3.11048 17.1495C2.88165 16.7521 2.68018 16.3401 2.50729 15.9164C2.34882
+
      15.5281 2.21436 15.1299 2.10481 14.7241C2.06469 14.5755 2.09528 14.4166
+
      2.18775 14.2935L3.71344 12.2623V11.7815L2.18775 9.75032C2.09541 9.62739
+
      2.06476 9.46881 2.10464 9.32033C2.33276 8.47104 2.67101 7.65525 3.1108
+
      6.89373C3.18756 6.76081 3.32112 6.67051 3.47307 6.6488L5.86116
+
      6.30765C6.00733 6.15616 6.15616 6.00733 6.30765 5.86116L6.6488
+
      3.47307C6.67054 3.3209 6.76107 3.18718 6.89428 3.11048C7.65593 2.67191
+
      8.4712 2.33387 9.31972 2.10481ZM19.4565 11.2216C19.3906 11.3094 19.3579
+
      11.4117 19.3563 11.5137H19.351V12.5316L19.3563 12.5301C19.3579 12.632
+
      19.3906 12.7344 19.4565 12.8222L20.864 14.6961C20.6842 15.3009 20.4419
+
      15.8854 20.1409 16.44L17.8262 16.7707C17.7144 16.7867 17.6113 16.84
+
      17.5337 16.9221C17.3213 17.1466 17.1028 17.365 16.8783 17.5774C16.7963
+
      17.655 16.7429 17.7582 16.7269 17.87L16.3963 20.1843C15.8413 20.4847
+
      15.2568 20.7271 14.652 20.9076L12.7784 19.5003C12.6797 19.4261 12.5571
+
      19.3911 12.4341 19.402C12.1453 19.4275 11.8548 19.4275 11.5659
+
      19.402C11.443 19.3911 11.3203 19.4261 11.2216 19.5003L9.34775
+
      20.9078C8.74289 20.728 8.15841 20.4856 7.6038 20.1846L7.27313
+
      17.87C7.25684 17.7559 7.20165 17.651 7.1169 17.5729C6.88932 17.3633
+
      6.67417 17.1481 6.47132 16.9274C6.39323 16.8424 6.28813 16.787 6.17387
+
      16.7707L3.85955 16.4401C3.70108 16.1473 3.55877 15.8464 3.43317
+
      15.5386C3.32064 15.2628 3.22154 14.9816 3.13626 14.6958L4.54357
+
      12.8222C4.60946 12.7345 4.64214 12.6321 4.64375 12.5302L4.64855
+
      12.5316V11.5137H4.64375C4.64215 11.4117 4.60947 11.3094 4.54356
+
      11.2216L3.13604 9.34774C3.31584 8.74288 3.5582 8.15841 3.85919
+
      7.6038L6.17386 7.27313C6.28567 7.25716 6.38877 7.2038 6.46639
+
      7.12175C6.67878 6.89723 6.89723 6.67878 7.12175 6.46639C7.2038 6.38877
+
      7.25716 6.28567 7.27313 6.17386L7.60374 3.85954C8.15875 3.5591 8.74328
+
      3.31672 9.34803 3.13625L11.2216 4.54356C11.3203 4.6177 11.443 4.65271
+
      11.5659 4.64184C11.8548 4.6163 12.1453 4.6163 12.4341 4.64184C12.5571
+
      4.65271 12.6797 4.6177 12.7784 4.54356L14.6523 3.13604C15.2572 3.31584
+
      15.8416 3.5582 16.3963 3.85919L16.7269 6.17386C16.7432 6.28793 16.7984
+
      6.39287 16.8832 6.47093C17.1107 6.68053 17.3259 6.89572 17.5287
+
      7.11647C17.6068 7.20145 17.7119 7.25681 17.8262 7.27313L20.1405
+
      7.60374C20.299 7.89648 20.4413 8.19742 20.5669 8.50522C20.6794 8.78098
+
      20.7785 9.06225 20.8638 9.34803L19.4565 11.2216Z" />
  {:else if name === "fork"}
    <path
      fill-rule="evenodd"
      clip-rule="evenodd"
-
      d="M8.34375 6.9375C7.5671
-
    6.9375 6.9375 7.5671 6.9375 8.34375C6.9375 9.1204 7.5671 9.75 8.34375
-
    9.75C9.1204 9.75 9.75 9.1204 9.75 8.34375C9.75 7.5671 9.1204 6.9375 8.34375
-
    6.9375ZM8.8125 10.6406C9.8823 10.4235 10.6875 9.47764 10.6875
-
    8.34375C10.6875 7.04933 9.63817 6 8.34375 6C7.04933 6 6 7.04933 6 8.34375C6
-
    9.47764 6.8052 10.4235 7.875 10.6406V11.1562C7.875 11.8999 8.18601 12.4028
-
    8.62101 12.7834C8.82767 12.9643 9.06032 13.1164 9.28665 13.2541C9.39277
-
    13.3187 9.48974 13.3756 9.5849 13.4316C9.71089 13.5056 9.83369 13.5777
-
    9.97031 13.6631C10.4167 13.9421 10.8179 14.2499 11.1146 14.7072C11.3641
-
    15.0919 11.5582 15.6119 11.6108 16.3623C10.5481 16.5849 9.75 17.5274 9.75
-
    18.6562C9.75 19.9507 10.7993 21 12.0938 21C13.3882 21 14.4375 19.9507
-
    14.4375 18.6562C14.4375 17.5274 13.6394 16.5849 12.5766 16.3623C12.6293
-
    15.6119 12.8234 15.0919 13.073 14.7072C13.3696 14.2499 13.7709 13.9421
-
    14.2172 13.6631C14.3538 13.5777 14.4766 13.5056 14.6026 13.4316C14.6978
-
    13.3756 14.7947 13.3187 14.9008 13.2541C15.1272 13.1164 15.3598 12.9643
-
    15.5665 12.7834C16.0015 12.4028 16.3125 11.8999 16.3125
-
    11.1562V10.6406C17.3823 10.4235 18.1875 9.47764 18.1875 8.34375C18.1875
-
    7.04933 17.1382 6 15.8438 6C14.5493 6 13.5 7.04933 13.5 8.34375C13.5
-
    9.47764 14.3052 10.4235 15.375 10.6406V11.1562C15.375 11.5845 15.2173
-
    11.8433 14.9491 12.0779C14.8042 12.2047 14.6267 12.3235 14.4136
-
    12.4532C14.3363 12.5002 14.2463 12.5532 14.1515 12.6091C14.0098 12.6926
-
    13.8574 12.7824 13.7203 12.8681C13.2292 13.1751 12.6929 13.5705 12.2864
-
    14.1971C12.2176 14.3032 12.1532 14.4147 12.0938 14.5323C12.0343 14.4147
-
    11.9699 14.3032 11.9011 14.1971C11.4946 13.5705 10.9583 13.1751 10.4672
-
    12.8681C10.3301 12.7824 10.1776 12.6926 10.036 12.6091C9.94121 12.5532
-
    9.85123 12.5002 9.77389 12.4532C9.56077 12.3235 9.38326 12.2047 9.23836
-
    12.0779C8.97024 11.8433 8.8125 11.5845 8.8125 11.1562V10.6406ZM15.8438
-
    9.75C16.6204 9.75 17.25 9.1204 17.25 8.34375C17.25 7.5671 16.6204 6.9375
-
    15.8438 6.9375C15.0671 6.9375 14.4375 7.5671 14.4375 8.34375C14.4375 9.1204
-
    15.0671 9.75 15.8438 9.75ZM12.0938 17.25C11.3171 17.25 10.6875 17.8796
-
    10.6875 18.6562C10.6875 19.4329 11.3171 20.0625 12.0938 20.0625C12.8704
-
    20.0625 13.5 19.4329 13.5 18.6562C13.5 17.8796 12.8704 17.25 12.0938
-
    17.25Z" />
+
      d="M3.5 6C3.5 4.61929 4.61929 3.5 6 3.5C7.38071 3.5 8.5 4.61929 8.5 6C8.5
+
      7.20948 7.64112 8.21836 6.5 8.44999V11C6.5 11.2761 6.72386 11.5 7
+
      11.5H17C17.2761 11.5 17.5 11.2761 17.5 11V8.44999C16.3589 8.21836 15.5
+
      7.20948 15.5 6C15.5 4.61929 16.6193 3.5 18 3.5C19.3807 3.5 20.5 4.61929
+
      20.5 6C20.5 7.20948 19.6411 8.21836 18.5 8.44999V11C18.5 11.8284 17.8284
+
      12.5 17 12.5H12.5V15.55C13.6411 15.7816 14.5 16.7905 14.5 18C14.5 19.3807
+
      13.3807 20.5 12 20.5C10.6193 20.5 9.5 19.3807 9.5 18C9.5 16.7905 10.3589
+
      15.7816 11.5 15.55V12.5H7C6.17157 12.5 5.5 11.8284 5.5 11V8.44999C4.35888
+
      8.21836 3.5 7.20948 3.5 6ZM6 4.5C5.17157 4.5 4.5 5.17157 4.5 6C4.5
+
      6.82843 5.17157 7.5 6 7.5C6.82843 7.5 7.5 6.82843 7.5 6C7.5 5.17157
+
      6.82843 4.5 6 4.5ZM18 4.5C17.1716 4.5 16.5 5.17157 16.5 6C16.5 6.82843
+
      17.1716 7.5 18 7.5C18.8284 7.5 19.5 6.82843 19.5 6C19.5 5.17157 18.8284
+
      4.5 18 4.5ZM12 16.5C11.1716 16.5 10.5 17.1716 10.5 18C10.5 18.8284
+
      11.1716 19.5 12 19.5C12.8284 19.5 13.5 18.8284 13.5 18C13.5 17.1716
+
      12.8284 16.5 12 16.5Z" />
  {:else if name === "moon"}
    <path
      fill-rule="evenodd"
      clip-rule="evenodd"
-
      d="M15.75 1.5C16.1642 1.5
-
    16.5 1.83579 16.5 2.25V3H17.25C17.6642 3 18 3.33579 18 3.75C18 4.16421
-
    17.6642 4.5 17.25 4.5H16.5V5.25C16.5 5.66421 16.1642 6 15.75 6C15.3358 6 15
-
    5.66421 15 5.25V4.5H14.25C13.8358 4.5 13.5 4.16421 13.5 3.75C13.5 3.33579
-
    13.8358 3 14.25 3H15V2.25C15 1.83579 15.3358 1.5 15.75 1.5ZM10.2246
-
    3.15456C10.4159 3.34623 10.489 3.62613 10.4159 3.88688C9.65641 6.59529
-
    10.4385 9.58344 12.4276 11.5724C14.4166 13.5615 17.4047 14.3436 20.1131
-
    13.5841C20.3739 13.511 20.6538 13.5841 20.8454 13.7754C21.0371 13.9667
-
    21.1108 14.2465 21.0382 14.5074C19.1394 21.3293 10.2649 23.5102 5.37735
-
    18.6226C0.393755 13.639 2.70287 4.85168 9.49264 2.96184C9.75353 2.88923
-
    10.0333 2.96289 10.2246 3.15456ZM8.69851 4.84862C3.95753 7.06376 2.57424
-
    13.6982 6.43801 17.562C10.2273 21.3512 16.9232 20.041 19.1499
-
    15.3017C16.3194 15.6466 13.4079 14.6741 11.3669 12.6331C9.32556 10.5918
-
    8.35305 7.67963 8.69851 4.84862ZM20.25 5.25C20.6642 5.25 21 5.58579 21
-
    6V7.5H22.5C22.9142 7.5 23.25 7.83579 23.25 8.25C23.25 8.66421 22.9142 9
-
    22.5 9H21V10.5C21 10.9142 20.6642 11.25 20.25 11.25C19.8358 11.25 19.5
-
    10.9142 19.5 10.5V9H18C17.5858 9 17.25 8.66421 17.25 8.25C17.25 7.83579
-
    17.5858 7.5 18 7.5H19.5V6C19.5 5.58579 19.8358 5.25 20.25 5.25Z" />
+
      d="M13.5 4.5C9.35786 4.5 6 7.85786 6 12C6 16.1421 9.35786 19.5 13.5
+
      19.5C14.8679 19.5 16.149 19.1343 17.2524 18.4954C13.7773 18.3652 11
+
      15.5069 11 12C11 8.49307 13.7773 5.6348 17.2524 5.50463C16.149 4.86574
+
      14.8679 4.5 13.5 4.5ZM5 12C5 7.30558 8.80558 3.5 13.5 3.5C15.1842 3.5
+
      16.7554 3.99035 18.0768 4.83625C18.5001 5.10719 18.5345 5.58051 18.3915
+
      5.90969C18.2548 6.22425 17.9285 6.5 17.5 6.5C14.4624 6.5 12 8.96243 12
+
      12C12 15.0376 14.4624 17.5 17.5 17.5C17.9285 17.5 18.2548 17.7758 18.3915
+
      18.0903C18.5345 18.4195 18.5001 18.8928 18.0768 19.1638C16.7554 20.0097
+
      15.1842 20.5 13.5 20.5C8.80558 20.5 5 16.6944 5 12Z" />
  {:else if name === "checkmark"}
    <path
      fill-rule="evenodd"
      clip-rule="evenodd"
-
      d="M18.2941 6.59564C18.5174 6.75806 18.5668 7.07076 18.4044 7.29409L11.781
-
      16.4012C10.8775 17.6436 9.07768 17.7848 7.99142 16.6985L5.64645 14.3536C5.45118
-
      14.1583 5.45118 13.8417 5.64645 13.6465C5.84171 13.4512 6.15829 13.4512 6.35355
-
      13.6465L8.69852 15.9914C9.35028 16.6432 10.4302 16.5584 10.9723 15.813L17.5956
-
      6.70592C17.7581 6.48259 18.0708 6.43322 18.2941 6.59564Z" />
+
      d="M18.2941 6.59564C18.5174 6.75806 18.5668 7.07076 18.4044
+
         7.29409L11.781 16.4012C10.8775 17.6436 9.07768 17.7848 7.99142
+
         16.6985L5.64645 14.3536C5.45118 14.1583 5.45118 13.8417 5.64645
+
         13.6465C5.84171 13.4512 6.15829 13.4512 6.35355 13.6465L8.69852
+
         15.9914C9.35028 16.6432 10.4302 16.5584 10.9723 15.813L17.5956
+
         6.70592C17.7581 6.48259 18.0708 6.43322 18.2941 6.59564Z" />
  {:else if name === "checkmark-small"}
    <path
      fill-rule="evenodd"
@@ -257,36 +262,60 @@
    <path
      fill-rule="evenodd"
      clip-rule="evenodd"
-
      d="M12 0.75C12.4142 0.75
-
    12.75 1.08579 12.75 1.5V3.375C12.75 3.78921 12.4142 4.125 12 4.125C11.5858
-
    4.125 11.25 3.78921 11.25 3.375V1.5C11.25 1.08579 11.5858 0.75 12
-
    0.75ZM4.04467 4.04467C4.33756 3.75178 4.81244 3.75178 5.10533
-
    4.04467L6.42721 5.36655C6.7201 5.65944 6.7201 6.13431 6.42721
-
    6.42721C6.13431 6.7201 5.65944 6.7201 5.36655 6.42721L4.04467
-
    5.10533C3.75178 4.81244 3.75178 4.33756 4.04467 4.04467ZM19.9553
-
    4.04467C20.2482 4.33756 20.2482 4.81244 19.9553 5.10533L18.6335
-
    6.42721C18.3406 6.7201 17.8657 6.7201 17.5728 6.42721C17.2799 6.13431
-
    17.2799 5.65944 17.5728 5.36655L18.8947 4.04467C19.1876 3.75178 19.6624
-
    3.75178 19.9553 4.04467ZM12 7.125C9.30761 7.125 7.125 9.30761 7.125
-
    12C7.125 14.6924 9.30761 16.875 12 16.875C14.6924 16.875 16.875 14.6924
-
    16.875 12C16.875 9.30761 14.6924 7.125 12 7.125ZM5.625 12C5.625 8.47918
-
    8.47918 5.625 12 5.625C15.5208 5.625 18.375 8.47918 18.375 12C18.375
-
    15.5208 15.5208 18.375 12 18.375C8.47918 18.375 5.625 15.5208 5.625
-
    12ZM0.75 12C0.75 11.5858 1.08579 11.25 1.5 11.25H3.375C3.78921 11.25 4.125
-
    11.5858 4.125 12C4.125 12.4142 3.78921 12.75 3.375 12.75H1.5C1.08579 12.75
-
    0.75 12.4142 0.75 12ZM19.875 12C19.875 11.5858 20.2108 11.25 20.625
-
    11.25H22.5C22.9142 11.25 23.25 11.5858 23.25 12C23.25 12.4142 22.9142 12.75
-
    22.5 12.75H20.625C20.2108 12.75 19.875 12.4142 19.875 12ZM6.42721
-
    17.5728C6.7201 17.8657 6.7201 18.3406 6.42721 18.6335L5.10533
-
    19.9553C4.81244 20.2482 4.33756 20.2482 4.04467 19.9553C3.75178 19.6624
-
    3.75178 19.1876 4.04467 18.8947L5.36655 17.5728C5.65944 17.2799 6.13431
-
    17.2799 6.42721 17.5728ZM17.5728 17.5728C17.8657 17.2799 18.3406 17.2799
-
    18.6335 17.5728L19.9553 18.8947C20.2482 19.1876 20.2482 19.6624 19.9553
-
    19.9553C19.6624 20.2482 19.1876 20.2482 18.8947 19.9553L17.5728
-
    18.6335C17.2799 18.3406 17.2799 17.8657 17.5728 17.5728ZM12 19.875C12.4142
-
    19.875 12.75 20.2108 12.75 20.625V22.5C12.75 22.9142 12.4142 23.25 12
-
    23.25C11.5858 23.25 11.25 22.9142 11.25 22.5V20.625C11.25 20.2108 11.5858
-
    19.875 12 19.875Z" />
+
      d="M12 8.5C10.067 8.5 8.5 10.067 8.5 12C8.5 13.933 10.067 15.5 12
+
      15.5C13.933 15.5 15.5 13.933 15.5 12C15.5 10.067 13.933 8.5 12 8.5ZM7.5
+
      12C7.5 9.51472 9.51472 7.5 12 7.5C14.4853 7.5 16.5 9.51472 16.5 12C16.5
+
      14.4853 14.4853 16.5 12 16.5C9.51472 16.5 7.5 14.4853 7.5 12Z" />
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M12 18.5C12.2761 18.5 12.5 18.7239 12.5 19V21C12.5 21.2761 12.2761
+
      21.5 12 21.5C11.7239 21.5 11.5 21.2761 11.5 21V19C11.5 18.7239 11.7239
+
      18.5 12 18.5Z" />
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M7.40381 16.5962C7.59907 16.7915 7.59907 17.108 7.40381
+
      17.3033L5.98959 18.7175C5.79433 18.9128 5.47775 18.9128 5.28249
+
      18.7175C5.08722 18.5223 5.08722 18.2057 5.28249 18.0104L6.6967
+
      16.5962C6.89196 16.4009 7.20854 16.4009 7.40381 16.5962Z" />
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M5.5 12C5.5 12.2761 5.27614 12.5 5 12.5H3C2.72386 12.5 2.5 12.2761 2.5
+
      12C2.5 11.7239 2.72386 11.5 3 11.5H5C5.27614 11.5 5.5 11.7239 5.5 12Z" />
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M7.40381 7.40381C7.20854 7.59907 6.89196 7.59907 6.6967
+
      7.40381L5.28249 5.98959C5.08722 5.79433 5.08722 5.47775 5.28249
+
      5.28249C5.47775 5.08722 5.79433 5.08722 5.98959 5.28249L7.40381
+
      6.6967C7.59907 6.89196 7.59907 7.20854 7.40381 7.40381Z" />
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M12 2.5C12.2761 2.5 12.5 2.72386 12.5 3V5C12.5 5.27614 12.2761 5.5 12
+
      5.5C11.7239 5.5 11.5 5.27614 11.5 5V3C11.5 2.72386 11.7239 2.5 12 2.5Z" />
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M18.7175 5.28249C18.9128 5.47775 18.9128 5.79433 18.7175
+
      5.98959L17.3033 7.40381C17.108 7.59907 16.7915 7.59907 16.5962
+
      7.40381C16.4009 7.20854 16.4009 6.89196 16.5962 6.6967L18.0104
+
      5.28249C18.2057 5.08722 18.5223 5.08722 18.7175 5.28249Z" />
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M21.5 12C21.5 12.2761 21.2761 12.5 21 12.5H19C18.7239 12.5 18.5
+
      12.2761 18.5 12C18.5 11.7239 18.7239 11.5 19 11.5H21C21.2761 11.5 21.5
+
      11.7239 21.5 12Z" />
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M18.7175 18.7175C18.5223 18.9128 18.2057 18.9128 18.0104
+
      18.7175L16.5962 17.3033C16.4009 17.108 16.4009 16.7915 16.5962
+
      16.5962C16.7915 16.4009 17.108 16.4009 17.3033 16.5962L18.7175
+
      18.0104C18.9128 18.2057 18.9128 18.5223 18.7175 18.7175Z" />
  {:else if name === "twitter"}
    <path
      clip-rule="evenodd"
modified src/components/Link.svelte
@@ -2,22 +2,25 @@
  import type { Route } from "@app/lib/router/definitions";

  import { createEventDispatcher } from "svelte";
-
  import { push, routeToPath } from "@app/lib/router";
+
  import { push, routeToPath, useDefaultNavigation } from "@app/lib/router";

  export let route: Route;
-
  export let title: string | null = null;
-
  export let id: string | null = null;

  const dispatch = createEventDispatcher<{
-
    click: never;
+
    afterNavigate: never;
  }>();

-
  function onClick(): void {
+
  function navigateToRoute(event: MouseEvent): void {
+
    if (useDefaultNavigation(event)) {
+
      return;
+
    }
+

+
    event.preventDefault();
    push(route);
-
    dispatch("click");
+
    dispatch("afterNavigate");
  }
</script>

-
<a on:click|preventDefault={onClick} {title} {id} href={routeToPath(route)}>
+
<a on:click={navigateToRoute} href={routeToPath(route)}>
  <slot />
</a>
modified src/components/ProjectCard.svelte
@@ -97,8 +97,7 @@
  }
</style>

-
<!-- svelte-ignore a11y-click-events-have-key-events -->
-
<div class="project" on:click class:compact>
+
<div class="project" class:compact>
  <div class="left">
    <div class="id">
      <span class="name">{name}</span>
modified src/components/ProjectLink.svelte
@@ -1,16 +1,29 @@
-
<script lang="ts">
+
<script lang="ts" strictEvents>
  import type { ProjectsParams } from "@app/lib/router/definitions";
-
  import { updateProjectRoute, projectLinkHref } from "@app/lib/router";
+
  import { createEventDispatcher } from "svelte";
+

+
  import {
+
    projectLinkHref,
+
    updateProjectRoute,
+
    useDefaultNavigation,
+
  } from "@app/lib/router";

  export let projectParams: Partial<ProjectsParams>;
-
  export let id: string | undefined = undefined;
-
</script>
+
  export let title: string | undefined = undefined;
+

+
  const dispatch = createEventDispatcher<{ click: never }>();

-
<a
-
  {id}
-
  on:click|preventDefault={() => {
+
  function navigateToRoute(event: MouseEvent): void {
+
    if (useDefaultNavigation(event)) {
+
      return;
+
    }
+

+
    event.preventDefault();
    updateProjectRoute(projectParams);
-
  }}
-
  href={projectLinkHref(projectParams)}>
+
    dispatch("click");
+
  }
+
</script>
+

+
<a {title} on:click={navigateToRoute} href={projectLinkHref(projectParams)}>
  <slot />
</a>
added src/components/SquareButton.svelte
@@ -0,0 +1,74 @@
+
<script lang="ts" strictEvents>
+
  export let active: boolean = false;
+
  export let hoverable: boolean = true;
+
  export let clickable: boolean = false;
+
  export let disabled: boolean = false;
+
  export let size: "small" | "regular" = "regular";
+
</script>
+

+
<style>
+
  .square-button {
+
    display: flex;
+
    align-items: center;
+
    height: 2rem;
+
    padding: 0 0.75rem;
+
    background: var(--color-foreground-1);
+
    border: none;
+
    border-radius: var(--border-radius-small);
+
    color: var(--color-foreground);
+
    font-family: var(--font-family-monospace);
+
    font-size: var(--font-size-tiny);
+
  }
+

+
  .small {
+
    height: var(--button-tiny-height);
+
  }
+

+
  .clickable {
+
    cursor: pointer;
+
    user-select: none;
+
  }
+

+
  .active {
+
    color: var(--color-background);
+
    background: var(--color-foreground);
+
    background-color: var(--color-foreground);
+
  }
+

+
  .active:hover {
+
    background-color: var(--color-foreground);
+
  }
+

+
  .hoverable:hover {
+
    background-color: var(--color-foreground-2);
+
  }
+

+
  .active.hoverable:hover {
+
    background-color: var(--color-foreground);
+
  }
+

+
  .disabled {
+
    cursor: not-allowed;
+
    color: var(--color-foreground-5);
+
  }
+
  .disabled.active {
+
    color: var(--color-background);
+
  }
+
  .disabled:hover {
+
    background: var(--color-foreground-1);
+
  }
+
</style>
+

+
<!-- svelte-ignore a11y-click-events-have-key-events -->
+
<div
+
  on:click
+
  class="square-button"
+
  class:active
+
  class:hoverable
+
  class:disabled
+
  class:small={size === "small"}
+
  class:clickable>
+
  <span>
+
    <slot />
+
  </span>
+
</div>
deleted src/components/TabBar.svelte
@@ -1,68 +0,0 @@
-
<script lang="ts" context="module">
-
  export interface Tab<T> {
-
    title?: string;
-
    disabled: boolean;
-
    value: T;
-
  }
-
</script>
-

-
<script lang="ts" strictEvents>
-
  type T = $$Generic;
-

-
  import capitalize from "lodash/capitalize";
-
  import { createEventDispatcher } from "svelte";
-

-
  export let options: Tab<T>[];
-
  export let active: T;
-

-
  const dispatch = createEventDispatcher<{ select: T }>();
-

-
  function onSelect(option: Tab<T>) {
-
    if (!option.disabled) {
-
      dispatch("select", option.value);
-
    }
-
  }
-
</script>
-

-
<style>
-
  .wrapper {
-
    display: flex;
-
    gap: 1rem;
-
    user-select: none;
-
  }
-
  button {
-
    border-radius: var(--border-radius-small);
-
    color: var(--color-foreground-6);
-
    cursor: pointer;
-
    font-family: var(--font-family-monospace);
-
    font-size: var(--font-size-tiny);
-
    height: var(--button-tiny-height);
-
    padding: 0.25rem 0.5rem;
-
    border: none;
-
    min-width: 0;
-
    background-color: var(--color-background);
-
  }
-
  button:hover,
-
  button.active {
-
    cursor: pointer;
-
    color: var(--color-foreground);
-
    background-color: var(--color-foreground-1);
-
  }
-
  button[disabled],
-
  button[disabled]:hover {
-
    cursor: not-allowed;
-
    color: var(--color-foreground-6);
-
  }
-
</style>
-

-
<div class="wrapper">
-
  {#each options as option}
-
    <button
-
      class="state-toggle"
-
      on:click={() => onSelect(option)}
-
      disabled={option.disabled}
-
      class:active={active === option.value}>
-
      {option.title ?? capitalize(`${option.value}`)}
-
    </button>
-
  {/each}
-
</div>
modified src/lib/router.ts
@@ -16,6 +16,16 @@ export const activeRouteStore: Readable<Route> = derived(
  },
);

+
export function useDefaultNavigation(event: MouseEvent) {
+
  return (
+
    event.button !== 0 ||
+
    event.altKey ||
+
    event.ctrlKey ||
+
    event.metaKey ||
+
    event.shiftKey
+
  );
+
}
+

export const base = import.meta.env.VITE_HASH_ROUTING ? "./" : "/";

// Gets triggered when clicking on an anchor hash tag e.g. <a href="#header"/>
modified src/views/home/Index.svelte
@@ -1,28 +1,13 @@
<script lang="ts">
-
  import type { BaseUrl, Project } from "@httpd-client";
-

-
  import * as router from "@app/lib/router";
  import { config } from "@app/lib/config";
  import { getProjectsFromSeeds } from "@app/lib/search";
  import { loadProjectActivity } from "@app/lib/commit";
  import { twemoji } from "@app/lib/utils";

+
  import Link from "@app/components/Link.svelte";
  import Loading from "@app/components/Loading.svelte";
  import ErrorMessage from "@app/components/ErrorMessage.svelte";
  import ProjectCard from "@app/components/ProjectCard.svelte";
-

-
  function goToProject(project: Project, baseUrl: BaseUrl) {
-
    router.push({
-
      resource: "projects",
-
      params: {
-
        view: { resource: "tree" },
-
        id: project.id,
-
        hostnamePort: baseUrl.hostname,
-
        peer: undefined,
-
        revision: undefined,
-
      },
-
    });
-
  }
</script>

<style>
@@ -97,14 +82,25 @@
        {#each results as result}
          {#await loadProjectActivity(result.project.id, result.baseUrl) then activity}
            <div class="project">
-
              <ProjectCard
-
                compact
-
                description={result.project.description}
-
                head={result.project.head}
-
                id={result.project.id}
-
                name={result.project.name}
-
                {activity}
-
                on:click={() => goToProject(result.project, result.baseUrl)} />
+
              <Link
+
                route={{
+
                  resource: "projects",
+
                  params: {
+
                    view: { resource: "tree" },
+
                    id: result.project.id,
+
                    hostnamePort: result.baseUrl.hostname,
+
                    peer: undefined,
+
                    revision: undefined,
+
                  },
+
                }}>
+
                <ProjectCard
+
                  compact
+
                  description={result.project.description}
+
                  head={result.project.head}
+
                  id={result.project.id}
+
                  name={result.project.name}
+
                  {activity} />
+
              </Link>
            </div>
          {/await}
        {/each}
modified src/views/projects/Blob.svelte
@@ -11,8 +11,8 @@
  import { lineNumbersGutter } from "@app/lib/syntax";
  import { updateProjectRoute } from "@app/lib/router";

-
  import HeaderToggleLabel from "@app/views/projects/HeaderToggleLabel.svelte";
  import Readme from "@app/views/projects/Readme.svelte";
+
  import SquareButton from "@app/components/SquareButton.svelte";

  export let activeRoute: ProjectRoute;
  export let blob: Blob;
@@ -103,7 +103,7 @@
  }

  .last-commit {
-
    padding: 0.5rem;
+
    padding: 0.5rem 0.75rem;
    color: var(--color-secondary);
    background-color: var(--color-secondary-2);
    font-size: var(--font-size-tiny);
@@ -229,12 +229,12 @@
      <div class="right">
        {#if isMarkdown}
          <div class="markdown-toggle">
-
            <HeaderToggleLabel
+
            <SquareButton
              active={!showMarkdown}
              clickable
              on:click={toggleMarkdown}>
              Raw
-
            </HeaderToggleLabel>
+
            </SquareButton>
          </div>
        {/if}
        <div class="last-commit" title={lastCommit.author.name} use:twemoji>
modified src/views/projects/BranchSelector.svelte
@@ -1,20 +1,15 @@
<script lang="ts" strictEvents>
-
  import { createEventDispatcher } from "svelte";
-

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

  import Dropdown from "@app/components/Dropdown.svelte";
  import Floating from "@app/components/Floating.svelte";
+
  import ProjectLink from "@app/components/ProjectLink.svelte";

  export let branches: Record<string, string>;
  export let projectDefaultBranch: string;
  export let projectHead: string | undefined = undefined;
  export let revision: string;

-
  const dispatch = createEventDispatcher<{ branchChanged: string }>();
-
  const switchBranch = (name: string) => {
-
    dispatch("branchChanged", name);
-
  };
-

  let branchLabel: string | null = null;

  $: branchList = Object.keys(branches)
@@ -85,10 +80,13 @@
          {branchLabel}
        </div>
        <svelte:fragment slot="modal">
-
          <Dropdown
-
            items={branchList}
-
            selected={branchLabel}
-
            on:select={e => switchBranch(e.detail.value)} />
+
          <Dropdown items={branchList} selected={branchLabel}>
+
            <div class="branch-item" slot="item" let:item>
+
              <ProjectLink projectParams={{ revision: item.value }} on:click>
+
                {item.value}
+
              </ProjectLink>
+
            </div>
+
          </Dropdown>
        </svelte:fragment>
      </Floating>
      <div class="hash layout-desktop">
modified src/views/projects/Browser.svelte
@@ -12,7 +12,6 @@

  import { onMount } from "svelte";

-
  import * as router from "@app/lib/router";
  import * as utils from "@app/lib/utils";
  import { HttpdClient } from "@httpd-client";

@@ -48,6 +47,11 @@

  const api = new HttpdClient(baseUrl);

+
  // When a user clicks between multiple files, we want to retain the previous
+
  // file contents and show them while the new file is loading to prevent the
+
  // UI from flickering or showing a loading indicator.
+
  let previousBlob: Blob;
+

  const loadBlob = async (path: string) => {
    if (state.status === Status.Loaded && state.path === path) {
      return state.blob;
@@ -63,6 +67,7 @@
    }

    state = { status: Status.Loaded, path, blob };
+
    previousBlob = blob;
    return blob;
  };

@@ -70,31 +75,6 @@
    browserErrorStore.set(undefined);
  });

-
  const onSelect = async (newPath: string) => {
-
    browserErrorStore.set(undefined);
-
    // Ensure we don't spend any time in a "loading" state. This means
-
    // the loading spinner won't be shown, and instead the blob will be
-
    // displayed once loaded.
-
    const blob = await loadBlob(newPath).catch(() => {
-
      browserErrorStore.set({
-
        message: "Not able to load selected file",
-
        path: newPath,
-
      });
-
      return undefined;
-
    });
-
    if (blob) {
-
      getBlob = new Promise(resolve => resolve(blob));
-
    }
-

-
    // Close mobile tree if user navigates to other file
-
    mobileFileTree = false;
-

-
    router.updateProjectRoute({
-
      view: { resource: "tree" },
-
      path: newPath,
-
    });
-
  };
-

  const fetchTree = async (path: string) => {
    return api.project.getTree(project.id, commit, path).catch(() => {
      browserErrorStore.set({
@@ -105,10 +85,6 @@
    });
  };

-
  const toggleMobileFileTree = () => {
-
    mobileFileTree = !mobileFileTree;
-
  };
-

  $: getBlob = loadBlob(path).catch(() => {
    browserErrorStore.set({ message: "Not able to load file", path });
    return undefined;
@@ -203,7 +179,9 @@
      <Button
        style="width: 100%;"
        variant="secondary"
-
        on:click={toggleMobileFileTree}>
+
        on:click={() => {
+
          mobileFileTree = !mobileFileTree;
+
        }}>
        Browse
      </Button>
    </nav>
@@ -218,8 +196,9 @@
            {path}
            {fetchTree}
            {loadingPath}
-
            on:select={e => {
-
              onSelect(e.detail);
+
            on:select={() => {
+
              // Close mobile tree if user navigates to other file
+
              mobileFileTree = false;
            }} />
        </div>
      </div>
@@ -241,7 +220,20 @@
          </Placeholder>
        {:else}
          {#await getBlob}
-
            <Loading small center />
+
            {#if previousBlob}
+
              <div class="layout-desktop">
+
                <BlobComponent
+
                  {line}
+
                  blob={previousBlob}
+
                  {activeRoute}
+
                  rawPath={utils.getRawBasePath(project.id, baseUrl, commit)} />
+
              </div>
+
              <div class="layout-mobile">
+
                <Loading small center />
+
              </div>
+
            {:else}
+
              <Loading small center />
+
            {/if}
          {:then blob}
            {#if blob}
              <BlobComponent
modified src/views/projects/Cob/AssigneeInput.svelte
@@ -48,7 +48,7 @@
  .metadata-section {
    margin-bottom: 4rem;
  }
-
  .metadata-section-header {
+
  .header {
    display: flex;
    gap: 1rem;
    align-items: center;
@@ -56,20 +56,27 @@
    margin-bottom: 0.75rem;
    color: var(--color-foreground-6);
  }
-
  .metadata-section-body {
+
  .body {
    display: flex;
    flex-wrap: wrap;
    flex-direction: row;
    gap: 0.5rem;
    margin-bottom: 1.25rem;
  }
-
  .metadata-section-empty {
+
  .empty {
    color: var(--color-foreground-5);
  }
+

+
  .chip-content {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.5rem;
+
    white-space: nowrap;
+
  }
</style>

<div class="metadata-section">
-
  <div class="metadata-section-header">
+
  <div class="header">
    <span>Assignees</span>
    {#if action === "edit"}
      {#if !edit}
@@ -94,17 +101,19 @@
      {/if}
    {/if}
  </div>
-
  <div class="metadata-section-body">
+
  <div class="body">
    {#each updatedAssignees as assignee, key (assignee)}
      <Chip
        on:remove={removeAssignee}
        removeable={edit || action === "create"}
        {key}>
-
        <Avatar inline nodeId={assignee} />
-
        <span>{formatNodeId(assignee)}</span>
+
        <div class="chip-content">
+
          <Avatar inline nodeId={assignee} />
+
          <span>{formatNodeId(assignee)}</span>
+
        </div>
      </Chip>
    {:else}
-
      <div class="metadata-section-empty">No assignees</div>
+
      <div class="empty">No assignees</div>
    {/each}
  </div>
  {#if edit || action === "create"}
modified src/views/projects/Cob/TagInput.svelte
@@ -66,6 +66,7 @@
  .tag {
    overflow: hidden;
    text-overflow: ellipsis;
+
    white-space: nowrap;
  }
</style>

modified src/views/projects/Commit.svelte
@@ -6,8 +6,8 @@
  import * as router from "@app/lib/router";

  import Changeset from "@app/views/projects/SourceBrowser/Changeset.svelte";
-
  import CommitAuthorship from "@app/views/projects/Commit/CommitAuthorship.svelte";
  import Clipboard from "@app/components/Clipboard.svelte";
+
  import CommitAuthorship from "@app/views/projects/Commit/CommitAuthorship.svelte";

  export let commit: Commit;

@@ -24,7 +24,7 @@
  .commit {
    padding: 0 2rem 0 8rem;
  }
-
  header {
+
  .header {
    padding: 1rem;
    background: var(--color-background-1);
    border-radius: var(--border-radius);
@@ -54,7 +54,7 @@
</style>

<div class="commit">
-
  <header>
+
  <div class="header">
    <div class="summary">
      <div class="txt-medium txt-bold" use:twemoji>{header.summary}</div>
      <div class="layout-desktop-flex txt-monospace sha1">
@@ -68,6 +68,6 @@
    </div>
    <pre class="description txt-small">{header.description}</pre>
    <CommitAuthorship {header} />
-
  </header>
-
  <Changeset diff={commit.diff} on:browse={onBrowse} />
+
  </div>
+
  <Changeset diff={commit.diff} revision={commit.commit.id} />
</div>
modified src/views/projects/Commit/CommitAuthorship.svelte
@@ -14,7 +14,6 @@
    align-items: center;
    gap: 0.25rem;
    color: var(--color-foreground-6);
-
    padding: 0.125rem 0;
  }
  .authorship .author,
  .authorship .committer {
modified src/views/projects/Commit/CommitTeaser.svelte
@@ -1,67 +1,48 @@
-
<script lang="ts" strictEvents>
+
<script lang="ts">
  import type { CommitHeader } from "@httpd-client";

-
  import { createEventDispatcher } from "svelte";
-

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

  import CommitAuthorship from "./CommitAuthorship.svelte";
  import Icon from "@app/components/Icon.svelte";
+
  import ProjectLink from "@app/components/ProjectLink.svelte";

  export let commit: CommitHeader;
-

-
  const dispatch = createEventDispatcher<{ browseCommit: string }>();
-

-
  function browseCommit(commit: string) {
-
    dispatch("browseCommit", commit);
-
  }
</script>

<style>
-
  .hash {
-
    font-family: var(--font-family-monospace);
-
    font-size: var(--font-size-tiny);
-
    padding: 0 1.5rem;
-
  }
-
  .commit-teaser {
+
  .teaser {
    background-color: var(--color-background-1);
    padding: 0.75rem 0rem;
+
    display: flex;
+
    align-items: center;
  }
-
  .commit-teaser:hover {
+
  .teaser:hover {
    background-color: var(--color-foreground-2);
-
    cursor: pointer;
-
  }
-
  .commit-teaser:first-child {
-
    border-top-left-radius: var(--border-radius-small);
-
    border-top-right-radius: var(--border-radius-small);
  }
-
  .commit-teaser:last-child {
-
    border-bottom-left-radius: var(--border-radius-small);
-
    border-bottom-right-radius: var(--border-radius-small);
-
  }
-
  .commit-teaser:not(:last-child) {
-
    border-bottom: 1px solid var(--color-background);
-
  }
-
  .commit-teaser {
-
    display: flex;
-
    align-items: center;
-
    justify-content: space-between;
+
  .hash {
+
    font-family: var(--font-family-monospace);
+
    font-size: var(--font-size-tiny);
+
    padding: 0 1.5rem;
  }
-

-
  .column-left {
+
  .left {
    padding-left: 1rem;
-
    flex: min-content;
  }
-
  .commit-teaser .column-right {
+
  .right {
    display: flex;
    align-items: center;
    padding-right: 1.5rem;
+
    margin-left: auto;
  }
  .summary {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
-
    padding-right: 1rem;
+
    margin-bottom: 0.25rem;
+
    font-size: var(--font-size-small);
+
  }
+
  .summary:hover {
+
    color: var(--color-secondary);
  }
  .browse {
    display: flex;
@@ -74,7 +55,7 @@
    .hash {
      padding-right: 0;
    }
-
    .column-left {
+
    .left {
      overflow: hidden;
    }
    .browse {
@@ -84,29 +65,36 @@
      overflow: hidden;
      white-space: nowrap;
      text-overflow: ellipsis;
-
      padding-right: 1rem;
    }
  }
</style>

-
<!-- svelte-ignore a11y-click-events-have-key-events -->
-
<div class="commit-teaser" on:click>
-
  <div class="column-left">
-
    <div class="header">
+
<div class="teaser">
+
  <div class="left">
+
    <ProjectLink
+
      projectParams={{
+
        view: { resource: "commits" },
+
        revision: commit.id,
+
        search: undefined,
+
      }}>
      <div class="summary" use:twemoji>
        {commit.summary}
      </div>
-
    </div>
+
    </ProjectLink>
    <CommitAuthorship header={commit} />
  </div>
-
  <div class="column-right">
+
  <div class="right">
    <span class="hash txt-highlight">{formatCommit(commit.id)}</span>
-
    <!-- svelte-ignore a11y-click-events-have-key-events -->
    <div
      class="browse"
-
      title="Browse the repository at this point in the history"
-
      on:click|stopPropagation={() => browseCommit(commit.id)}>
-
      <Icon name="browse" />
+
      title="Browse the repository at this point in the history">
+
      <ProjectLink
+
        projectParams={{
+
          view: { resource: "tree" },
+
          revision: commit.id,
+
        }}>
+
        <Icon name="browse" />
+
      </ProjectLink>
    </div>
  </div>
</div>
modified src/views/projects/Header.svelte
@@ -2,16 +2,16 @@
  import type { BaseUrl, Project, Remote, Tree } from "@httpd-client";
  import type { ProjectRoute } from "@app/lib/router/definitions";

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

  import { closeFocused } from "@app/components/Floating.svelte";
  import { config } from "@app/lib/config";
  import { pluralize } from "@app/lib/pluralize";

  import BranchSelector from "@app/views/projects/BranchSelector.svelte";
  import CloneButton from "@app/views/projects/CloneButton.svelte";
-
  import HeaderToggleLabel from "@app/views/projects/HeaderToggleLabel.svelte";
+
  import Link from "@app/components/Link.svelte";
  import PeerSelector from "@app/views/projects/PeerSelector.svelte";
+
  import ProjectLink from "@app/components/ProjectLink.svelte";
+
  import SquareButton from "@app/components/SquareButton.svelte";

  export let project: Project;
  export let activeRoute: ProjectRoute;
@@ -22,55 +22,10 @@
  export let baseUrl: BaseUrl;

  $: revision = activeRoute.params.revision ?? commit;
-

-
  // Switches between project views.
-
  const toggleContent = (
-
    input: "issues" | "patches" | "history",
-
    keepSourceInPath: boolean,
-
  ) => {
-
    router.updateProjectRoute({
-
      view: {
-
        resource: activeRoute.params.view.resource === input ? "tree" : input,
-
      },
-
      id: project.id,
-
      revision: revision,
-
      search: undefined,
-
      ...(keepSourceInPath ? null : { revision: undefined, path: undefined }),
-
    });
-
  };
-

-
  const updatePeer = (peer: string) => {
-
    router.updateProjectRoute({
-
      peer,
-
      revision: undefined,
-
    });
-
    closeFocused();
-
  };
-

-
  const updateRevision = (revision: string) => {
-
    router.updateProjectRoute({
-
      revision,
-
    });
-
    closeFocused();
-
  };
-

-
  function goToSeed() {
-
    if (baseUrl.port !== config.seeds.defaultHttpdPort) {
-
      router.push({
-
        resource: "seeds",
-
        params: { hostnamePort: `${baseUrl.hostname}:${baseUrl.port}` },
-
      });
-
    } else {
-
      router.push({
-
        resource: "seeds",
-
        params: { hostnamePort: baseUrl.hostname },
-
      });
-
    }
-
  }
</script>

<style>
-
  header {
+
  .header {
    font-size: var(--font-size-tiny);
    padding: 0 2rem 0 8rem;
    margin-bottom: 2rem;
@@ -82,21 +37,21 @@
  }

  @media (max-width: 960px) {
-
    header {
+
    .header {
      padding-left: 2rem;
    }
-
    header {
+
    .header {
      margin-bottom: 1.5rem;
    }
  }
</style>

-
<header>
+
<div class="header">
  {#if peers.length > 0}
    <PeerSelector
      {peers}
      peer={activeRoute.params.peer}
-
      on:peerChanged={event => updatePeer(event.detail)} />
+
      on:click={() => closeFocused()} />
  {/if}

  <BranchSelector
@@ -104,45 +59,77 @@
    projectHead={project.head}
    {branches}
    {revision}
-
    on:branchChanged={event => updateRevision(event.detail)} />
+
    on:click={() => closeFocused()} />

  <CloneButton {baseUrl} id={project.id} name={project.name} />

-
  <span>
-
    <HeaderToggleLabel
-
      clickable
-
      ariaLabel="Seed"
-
      title="Project data is fetched from this seed"
-
      on:click={goToSeed}>
-
      <span>{baseUrl.hostname}</span>
-
    </HeaderToggleLabel>
-
  </span>
-
  <HeaderToggleLabel
-
    ariaLabel="Commit count"
-
    clickable
-
    active={activeRoute.params.view.resource === "history"}
-
    on:click={() => toggleContent("history", true)}>
-
    <span class="txt-bold">{tree.stats.commits}</span>
-
    {pluralize("commit", tree.stats.commits)}
-
  </HeaderToggleLabel>
-
  <HeaderToggleLabel
-
    ariaLabel="Issue count"
-
    active={activeRoute.params.view.resource === "issues"}
-
    clickable
-
    on:click={() => toggleContent("issues", false)}>
-
    <span class="txt-bold">{project.issues.open}</span>
-
    {pluralize("issue", project.issues.open)}
-
  </HeaderToggleLabel>
-
  <HeaderToggleLabel
-
    ariaLabel="Patch count"
-
    active={activeRoute.params.view.resource === "patches"}
-
    clickable
-
    on:click={() => toggleContent("patches", false)}>
-
    <span class="txt-bold">{project.patches.open}</span>
-
    {pluralize("patch", project.patches.open)}
-
  </HeaderToggleLabel>
-
  <HeaderToggleLabel ariaLabel="Contributor count">
+
  <Link
+
    route={{
+
      resource: "seeds",
+
      params: {
+
        hostnamePort:
+
          baseUrl.port === config.seeds.defaultHttpdPort
+
            ? baseUrl.hostname
+
            : `${baseUrl.hostname}:${baseUrl.port}`,
+
      },
+
    }}>
+
    <SquareButton>
+
      {baseUrl.hostname}
+
    </SquareButton>
+
  </Link>
+

+
  <ProjectLink
+
    projectParams={{
+
      id: project.id,
+
      view: {
+
        resource:
+
          activeRoute.params.view.resource === "history" ? "tree" : "history",
+
      },
+
      revision: revision,
+
      search: undefined,
+
    }}>
+
    <SquareButton active={activeRoute.params.view.resource === "history"}>
+
      <span class="txt-bold">{tree.stats.commits}</span>
+
      {pluralize("commit", tree.stats.commits)}
+
    </SquareButton>
+
  </ProjectLink>
+

+
  <ProjectLink
+
    projectParams={{
+
      id: project.id,
+
      view: {
+
        resource:
+
          activeRoute.params.view.resource === "issues" ? "tree" : "issues",
+
      },
+
      search: undefined,
+
      revision: undefined,
+
      path: undefined,
+
    }}>
+
    <SquareButton active={activeRoute.params.view.resource === "issues"}>
+
      <span class="txt-bold">{project.issues.open}</span>
+
      {pluralize("issue", project.issues.open)}
+
    </SquareButton>
+
  </ProjectLink>
+

+
  <ProjectLink
+
    projectParams={{
+
      id: project.id,
+
      view: {
+
        resource:
+
          activeRoute.params.view.resource === "patches" ? "tree" : "patches",
+
      },
+
      search: undefined,
+
      revision: undefined,
+
      path: undefined,
+
    }}>
+
    <SquareButton active={activeRoute.params.view.resource === "patches"}>
+
      <span class="txt-bold">{project.patches.open}</span>
+
      {pluralize("patch", project.patches.open)}
+
    </SquareButton>
+
  </ProjectLink>
+

+
  <SquareButton hoverable={false}>
    <span class="txt-bold">{tree.stats.contributors}</span>
    {pluralize("contributor", tree.stats.contributors)}
-
  </HeaderToggleLabel>
-
</header>
+
  </SquareButton>
+
</div>
deleted src/views/projects/HeaderToggleLabel.svelte
@@ -1,49 +0,0 @@
-
<script lang="ts">
-
  export let title: string | undefined = undefined;
-
  export let ariaLabel: string | undefined = undefined;
-
  export let active = false;
-
  export let clickable = false;
-
  export let disabled = false;
-
</script>
-

-
<style>
-
  .stat {
-
    font-family: var(--font-family-monospace);
-
    font-size: var(--font-size-tiny);
-
    padding: 0.5rem 0.75rem;
-
    padding-bottom: 1rem; /* moving the content a tad higher to match the previous span usage */
-
    height: 2rem;
-
    background: var(--color-foreground-1);
-
    border: none;
-
    color: var(--color-foreground);
-
    border-radius: var(--border-radius-small);
-
    min-width: max-content;
-
  }
-
  .active {
-
    color: var(--color-background);
-
    background: var(--color-foreground) !important;
-
    background-color: var(--color-foreground);
-
  }
-
  .clickable {
-
    cursor: pointer;
-
  }
-
  .clickable:hover {
-
    background-color: var(--color-foreground-2);
-
  }
-
  .not-allowed {
-
    cursor: not-allowed;
-
    color: var(--color-foreground-5);
-
  }
-
</style>
-

-
<button
-
  {title}
-
  {disabled}
-
  class:active
-
  class:clickable
-
  class:not-allowed={disabled}
-
  class="stat"
-
  aria-label={ariaLabel}
-
  on:click>
-
  <slot />
-
</button>
modified src/views/projects/History.svelte
@@ -1,7 +1,6 @@
<script lang="ts">
  import type { BaseUrl, CommitHeader } from "@httpd-client";

-
  import * as router from "@app/lib/router";
  import { HttpdClient } from "@httpd-client";
  import { groupCommits } from "@app/lib/commit";

@@ -42,20 +41,6 @@
    }
  }

-
  function goToSourceTreeAtCommit(event: { detail: string }) {
-
    router.updateProjectRoute({
-
      view: { resource: "tree" },
-
      revision: event.detail,
-
    });
-
  }
-

-
  function goToCommit(revision: string) {
-
    router.updateProjectRoute({
-
      view: { resource: "commits" },
-
      revision,
-
    });
-
  }
-

  $: showMoreButton =
    !loading && !error && totalCommitCount && history.length < totalCommitCount;

@@ -67,11 +52,13 @@
    padding: 0 2rem 0 8rem;
    font-size: var(--font-size-small);
  }
-
  .commit-group header {
-
    color: var(--color-foreground-6);
-
  }
-
  .commit-group-headers {
+
  .group {
    margin-bottom: 2rem;
+
    border-radius: var(--border-radius);
+
    overflow: hidden;
+
  }
+
  .teaser-wrapper:not(:last-child) {
+
    border-bottom: 1px solid var(--color-background);
  }
  .more {
    margin-top: 2rem;
@@ -88,18 +75,13 @@
{#if history}
  <div class="history">
    {#each groupCommits(history) as group (group.time)}
-
      <div class="commit-group">
-
        <header class="commit-date">
-
          <p>{group.date}</p>
-
        </header>
-
        <div class="commit-group-headers">
-
          {#each group.commits as commit (commit.id)}
-
            <CommitTeaser
-
              {commit}
-
              on:click={() => goToCommit(commit.id)}
-
              on:browseCommit={goToSourceTreeAtCommit} />
-
          {/each}
-
        </div>
+
      <p style:color="var(--color-foreground-6)">{group.date}</p>
+
      <div class="group">
+
        {#each group.commits as commit (commit.id)}
+
          <div class="teaser-wrapper">
+
            <CommitTeaser {commit} />
+
          </div>
+
        {/each}
      </div>
    {/each}
    <div class="more">
modified src/views/projects/Issue/IssueTeaser.svelte
@@ -6,6 +6,7 @@
  import Authorship from "@app/components/Authorship.svelte";
  import Badge from "@app/components/Badge.svelte";
  import Icon from "@app/components/Icon.svelte";
+
  import ProjectLink from "@app/components/ProjectLink.svelte";

  export let issue: Issue;

@@ -25,14 +26,12 @@

<style>
  .issue-teaser {
-
    display: grid;
-
    grid-template-columns: 3rem minmax(0, 1fr) auto;
+
    display: flex;
    padding: 0.75rem 0;
    background-color: var(--color-foreground-1);
  }
  .issue-teaser:hover {
    background-color: var(--color-foreground-2);
-
    cursor: pointer;
  }
  .subtitle {
    color: var(--color-foreground-6);
@@ -51,6 +50,10 @@
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
+
    cursor: pointer;
+
  }
+
  .issue-title:hover {
+
    color: var(--color-secondary);
  }
  .comment-count {
    display: flex;
@@ -70,14 +73,15 @@
    text-overflow: ellipsis;
  }

-
  .column-right {
+
  .right {
    align-self: center;
    justify-self: center;
+
    margin-left: auto;
  }
-

  .state {
    justify-self: center;
    align-self: center;
+
    margin: 0 1.25rem;
  }
  .state-icon {
    width: 0.5rem;
@@ -105,9 +109,17 @@
      class:closed={issue.state.status === "closed"}
      class:open={issue.state.status === "open"} />
  </div>
-
  <div class="column-left">
+
  <div>
    <div class="summary">
-
      <span class="issue-title">{issue.title}</span>
+
      <ProjectLink
+
        projectParams={{
+
          view: {
+
            resource: "issue",
+
            params: { issue: issue.id },
+
          },
+
        }}>
+
        <span class="issue-title">{issue.title}</span>
+
      </ProjectLink>
      <span class="tags">
        {#each issue.tags.slice(0, 4) as tag}
          <Badge style="max-width:7rem" variant="secondary">
@@ -129,7 +141,7 @@
    </div>
  </div>
  {#if commentCount > 0}
-
    <div class="column-right">
+
    <div class="right">
      <div class="comment-count">
        <Icon name="chat" />
        <span>{commentCount}</span>
modified src/views/projects/Issues.svelte
@@ -6,10 +6,8 @@

<script lang="ts">
  import type { Issue } from "@httpd-client";
-
  import type { Tab } from "@app/components/TabBar.svelte";
  import type { BaseUrl } from "@httpd-client";

-
  import * as router from "@app/lib/router";
  import * as utils from "@app/lib/utils";
  import capitalize from "lodash/capitalize";
  import { HttpdClient } from "@httpd-client";
@@ -17,11 +15,11 @@

  import Button from "@app/components/Button.svelte";
  import ErrorMessage from "@app/components/ErrorMessage.svelte";
-
  import HeaderToggleLabel from "@app/views/projects/HeaderToggleLabel.svelte";
  import IssueTeaser from "@app/views/projects/Issue/IssueTeaser.svelte";
  import Loading from "@app/components/Loading.svelte";
  import Placeholder from "@app/components/Placeholder.svelte";
-
  import TabBar from "@app/components/TabBar.svelte";
+
  import ProjectLink from "@app/components/ProjectLink.svelte";
+
  import SquareButton from "@app/components/SquareButton.svelte";

  export let projectId: string;
  export let state: IssueStatus;
@@ -30,9 +28,6 @@

  const perPage = 10;

-
  // Keeping it true, to avoid an initial flash
-
  // of EmptyState Placeholder
-
  let refresh = true;
  let loading = false;
  let page = 0;
  let error: any;
@@ -40,7 +35,7 @@

  const api = new HttpdClient(baseUrl);

-
  async function loadIssues(): Promise<void> {
+
  async function loadIssues(state: IssueStatus): Promise<void> {
    loading = true;
    try {
      const response = await api.project.getAllIssues(projectId, {
@@ -54,26 +49,17 @@
      error = e;
    } finally {
      loading = false;
-
      refresh = false;
    }
  }

-
  function switchState(e: CustomEvent<IssueStatus>): void {
-
    refresh = true;
-
    // Update state to be used in the query
-
    state = e.detail;
-
    // Reset page to 0 to load the first page for a new state
-
    page = 0;
-
    // Remove all existing patches with old state
-
    issues = [];
-
    loadIssues();
-
    router.updateProjectRoute({
-
      search: `state=${state}`,
-
    });
+
  interface Tab {
+
    value: IssueStatus;
+
    title: string;
+
    disabled: boolean;
  }

  const stateOptions: IssueStatus[] = ["open", "closed"];
-
  const options = stateOptions.map<Tab<IssueStatus>>(s => ({
+
  const options = stateOptions.map<Tab>(s => ({
    value: s,
    title: `${issueCounters[s]} ${s}`,
    disabled: issueCounters[s] === 0,
@@ -85,7 +71,11 @@
    issueCounters[state] &&
    issues.length < issueCounters[state];

-
  loadIssues();
+
  $: {
+
    page = 0;
+
    issues = [];
+
    loadIssues(state);
+
  }
</script>

<style>
@@ -98,7 +88,7 @@
    overflow: hidden;
  }
  .teaser:not(:last-child) {
-
    border-bottom: 1px dashed var(--color-background);
+
    border-bottom: 1px solid var(--color-background);
  }
  .section-header {
    display: flex;
@@ -106,9 +96,6 @@
    justify-content: space-between;
    width: 100%;
  }
-
  .loader {
-
    margin-top: 8rem;
-
  }
  .more {
    margin-top: 2rem;
    text-align: center;
@@ -125,61 +112,72 @@
<div class="issues">
  <div class="section-header">
    <div style="margin-bottom: 1rem;">
-
      <TabBar {options} on:select={switchState} active={state} />
+
      <div style="display: flex; gap: 0.5rem;">
+
        {#each options as option}
+
          {#if !option.disabled}
+
            <ProjectLink
+
              projectParams={{
+
                search: `state=${option.value}`,
+
              }}>
+
              <SquareButton
+
                size="small"
+
                clickable={option.disabled}
+
                active={option.value === state}
+
                disabled={option.disabled}>
+
                {option.title}
+
              </SquareButton>
+
            </ProjectLink>
+
          {:else}
+
            <SquareButton
+
              size="small"
+
              clickable={option.disabled}
+
              active={option.value === state}
+
              disabled={option.disabled}>
+
              {option.title}
+
            </SquareButton>
+
          {/if}
+
        {/each}
+
      </div>
    </div>
-
    <HeaderToggleLabel
-
      disabled={!$sessionStore || !utils.isLocal(baseUrl.hostname)}
-
      on:click={() => {
-
        router.updateProjectRoute({
+
    {#if $sessionStore && utils.isLocal(baseUrl.hostname)}
+
      <ProjectLink
+
        projectParams={{
          view: {
            resource: "issues",
            params: { view: { resource: "new" } },
          },
-
        });
-
      }}
-
      clickable>
-
      New issue
-
    </HeaderToggleLabel>
+
        }}>
+
        <SquareButton size="small">New issue</SquareButton>
+
      </ProjectLink>
+
    {/if}
  </div>
  <div class="issues-list">
-
    {#if refresh}
-
      <div class="loader">
-
        <Loading center />
+
    {#each issues as issue}
+
      <div class="teaser">
+
        <IssueTeaser {issue} />
      </div>
    {:else}
-
      {#each issues as issue}
-
        <!-- svelte-ignore a11y-click-events-have-key-events -->
-
        <div
-
          class="teaser"
-
          on:click={() => {
-
            router.updateProjectRoute({
-
              view: {
-
                resource: "issue",
-
                params: { issue: issue.id },
-
              },
-
            });
-
          }}>
-
          <IssueTeaser {issue} />
-
        </div>
+
      {#if error}
+
        <ErrorMessage message="Couldn't load issues." stackTrace={error} />
+
      {:else if loading}
+
        <!-- We already show a loader below. -->
      {:else}
-
        {#if error}
-
          <ErrorMessage message="Couldn't load issues." stackTrace={error} />
-
        {:else}
-
          <Placeholder emoji="🍂">
-
            <div slot="title">{capitalize(state)} issues</div>
-
            <div slot="body">No issues matched the current filter</div>
-
          </Placeholder>
-
        {/if}
-
      {/each}
-
      <div class="more">
-
        {#if loading}
-
          <Loading small={page !== 0} center />
-
        {/if}
-

-
        {#if showMoreButton}
-
          <Button variant="foreground" on:click={loadIssues}>More</Button>
-
        {/if}
-
      </div>
+
        <Placeholder emoji="🍂">
+
          <div slot="title">{capitalize(state)} issues</div>
+
          <div slot="body">No issues matched the current filter</div>
+
        </Placeholder>
+
      {/if}
+
    {/each}
+
  </div>
+
  <div class="more">
+
    {#if loading}
+
      <Loading small={page !== 0} center />
+
    {/if}
+

+
    {#if showMoreButton}
+
      <Button variant="foreground" on:click={() => loadIssues(state)}>
+
        More
+
      </Button>
    {/if}
  </div>
</div>
modified src/views/projects/Patch.svelte
@@ -40,8 +40,8 @@
  import CommitTeaser from "./Commit/CommitTeaser.svelte";
  import Dropdown from "@app/components/Dropdown.svelte";
  import Floating from "@app/components/Floating.svelte";
-
  import HeaderToggleLabel from "./HeaderToggleLabel.svelte";
-
  import TabBar from "@app/components/TabBar.svelte";
+
  import ProjectLink from "@app/components/ProjectLink.svelte";
+
  import SquareButton from "@app/components/SquareButton.svelte";
  import TagInput from "./Cob/TagInput.svelte";
  import ThreadComponent from "@app/components/Thread.svelte";

@@ -54,14 +54,6 @@

  const api = new HttpdClient(baseUrl);

-
  const browseCommit = (event: { detail: string }) => {
-
    router.updateProjectRoute({
-
      view: { resource: "tree" },
-
      search: undefined,
-
      revision: event.detail,
-
    });
-
  };
-

  async function createReply({
    detail: reply,
  }: CustomEvent<{ id: string; body: string }>) {
@@ -193,6 +185,11 @@
    margin: 1rem;
    color: var(--color-foreground-5);
  }
+
  .commit-list {
+
    border-radius: var(--border-radius);
+
    overflow: hidden;
+
    margin-top: 1rem;
+
  }

  @media (max-width: 1092px) {
    .patch {
@@ -214,14 +211,13 @@
  <div>
    <CobHeader id={patch.id} title={patch.title}>
      <span slot="revision" class="txt-monospace txt-tiny">
-
        <Floating>
+
        <Floating disabled={patch.revisions.length === 1}>
          <svelte:fragment slot="toggle">
-
            <HeaderToggleLabel
+
            <SquareButton
              clickable={patch.revisions.length > 1}
-
              disabled={patch.revisions.length === 1}
-
              title="Toggle revision">
+
              disabled={patch.revisions.length === 1}>
              Revision {currentRevisionIndex}
-
            </HeaderToggleLabel>
+
            </SquareButton>
          </svelte:fragment>
          <svelte:fragment slot="modal">
            <Dropdown
@@ -263,13 +259,32 @@
        </div>
      </svelte:fragment>
    </CobHeader>
-
    <TabBar
-
      {options}
-
      active={currentTab}
-
      on:select={({ detail: tab }) =>
-
        router.updateProjectRoute({
-
          search: `tab=${tab}`,
-
        })} />
+
    <div style="display: flex; gap: 0.5rem;">
+
      {#each options as option}
+
        {#if !option.disabled}
+
          <ProjectLink
+
            projectParams={{
+
              search: `tab=${option.value}`,
+
            }}>
+
            <SquareButton
+
              size="small"
+
              clickable={option.disabled}
+
              active={option.value === currentTab}
+
              disabled={option.disabled}>
+
              {option.title}
+
            </SquareButton>
+
          </ProjectLink>
+
        {:else}
+
          <SquareButton
+
            size="small"
+
            clickable={option.disabled}
+
            active={option.value === currentTab}
+
            disabled={option.disabled}>
+
            {option.title}
+
          </SquareButton>
+
        {/if}
+
      {/each}
+
    </div>
    {#if currentTab === "activity"}
      <div style:margin-top="1rem">
        <div class="txt-tiny">
@@ -332,34 +347,16 @@
      </div>
    {:else if currentTab === "commits"}
      {#await diffPromise then diff}
-
        <div style:margin-top="1rem">
+
        <div class="commit-list">
          {#each diff.commits as commit}
-
            <CommitTeaser
-
              {commit}
-
              on:click={() => {
-
                router.updateProjectRoute({
-
                  view: { resource: "commits" },
-
                  revision: commit.id,
-
                  search: undefined,
-
                });
-
              }}
-
              on:browseCommit={browseCommit} />
+
            <CommitTeaser {commit} />
          {/each}
        </div>
      {/await}
    {:else if currentTab === "files"}
      {#await diffPromise then diff}
        <div style:margin-top="1rem">
-
          <Changeset
-
            diff={diff.diff}
-
            on:browse={({ detail: path }) => {
-
              router.updateProjectRoute({
-
                view: { resource: "tree" },
-
                search: undefined,
-
                revision: currentRevision.oid,
-
                path,
-
              });
-
            }} />
+
          <Changeset revision={currentRevision.oid} diff={diff.diff} />
        </div>
      {/await}
    {/if}
modified src/views/projects/Patch/PatchTeaser.svelte
@@ -9,6 +9,7 @@
  import Badge from "@app/components/Badge.svelte";
  import DiffStatBadge from "@app/components/DiffStatBadge.svelte";
  import Icon from "@app/components/Icon.svelte";
+
  import ProjectLink from "@app/components/ProjectLink.svelte";

  export let projectId: string;
  export let baseUrl: BaseUrl;
@@ -27,14 +28,12 @@

<style>
  .patch-teaser {
-
    display: grid;
-
    grid-template-columns: 3rem minmax(0, 1fr) auto;
+
    display: flex;
    padding: 0.75rem 0;
    background-color: var(--color-foreground-1);
  }
  .patch-teaser:hover {
    background-color: var(--color-foreground-2);
-
    cursor: pointer;
  }
  .meta {
    display: flex;
@@ -57,23 +56,25 @@
    text-overflow: ellipsis;
    white-space: nowrap;
  }
+
  .patch-title:hover {
+
    color: var(--color-secondary);
+
  }
  .comment-count {
-
    display: flex;
-
    flex-direction: row;
    align-items: center;
    padding-right: 1rem;
    gap: 0.5rem;
    color: var(--color-foreground-5);
  }
-

-
  .column-right {
+
  .right {
    align-self: center;
    justify-self: center;
    padding-right: 1rem;
+
    margin-left: auto;
  }
  .state {
    justify-self: center;
    align-self: center;
+
    margin: 0 1.25rem;
  }
  .tags {
    display: flex;
@@ -117,9 +118,17 @@
      class:merged={patch.state.status === "merged"}
      class:archived={patch.state.status === "archived"} />
  </div>
-
  <div class="column-left">
+
  <div>
    <div class="summary">
-
      <span class="patch-title">{patch.title}</span>
+
      <ProjectLink
+
        projectParams={{
+
          view: {
+
            resource: "patch",
+
            params: { patch: patch.id },
+
          },
+
        }}>
+
        <span class="patch-title">{patch.title}</span>
+
      </ProjectLink>
      <span class="tags">
        {#each patch.tags.slice(0, 4) as tag}
          <Badge style="max-width:7rem" variant="secondary">
@@ -142,7 +151,7 @@
      </span>
    </div>
  </div>
-
  <div class="column-right">
+
  <div class="right">
    <div class="comment-count">
      {#await diffPromise then { diff }}
        <DiffStatBadge
modified src/views/projects/Patches.svelte
@@ -5,10 +5,8 @@
</script>

<script lang="ts">
-
  import type { Tab } from "@app/components/TabBar.svelte";
  import type { BaseUrl } from "@httpd-client";

-
  import * as router from "@app/lib/router";
  import capitalize from "lodash/capitalize";
  import { HttpdClient } from "@httpd-client";

@@ -17,7 +15,8 @@
  import Loading from "@app/components/Loading.svelte";
  import PatchTeaser from "./Patch/PatchTeaser.svelte";
  import Placeholder from "@app/components/Placeholder.svelte";
-
  import TabBar from "@app/components/TabBar.svelte";
+
  import ProjectLink from "@app/components/ProjectLink.svelte";
+
  import SquareButton from "@app/components/SquareButton.svelte";

  export let projectId: string;
  export let state: PatchStatus;
@@ -31,9 +30,6 @@

  const perPage = 10;

-
  // Keeping it true, to avoid an initial flash
-
  // of EmptyState Placeholder
-
  let refresh = true;
  let loading = false;
  let page = 0;
  let error: any;
@@ -41,7 +37,7 @@

  const api = new HttpdClient(baseUrl);

-
  async function loadPatches(): Promise<void> {
+
  async function loadPatches(state: PatchStatus): Promise<void> {
    loading = true;
    try {
      const response = await api.project.getAllPatches(projectId, {
@@ -55,37 +51,33 @@
      error = e;
    } finally {
      loading = false;
-
      refresh = false;
    }
  }

-
  function switchState(e: CustomEvent<PatchStatus>): void {
-
    refresh = true;
-
    // Update state to be used in the query
-
    state = e.detail;
-
    // Reset page to 0 to load the first page for a new state
-
    page = 0;
-
    // Remove all existing patches with old state
-
    patches = [];
-
    loadPatches();
-
    router.updateProjectRoute({
-
      search: `state=${state}`,
-
    });
+
  interface Tab {
+
    value: PatchStatus;
+
    title: string;
+
    disabled: boolean;
  }

  const stateOptions: PatchStatus[] = ["draft", "open", "archived", "merged"];
-
  const options = stateOptions.map<Tab<PatchStatus>>(s => ({
+
  const options = stateOptions.map<Tab>(s => ({
    value: s,
    title: `${patchCounters[s]} ${s}`,
    disabled: patchCounters[s] === 0,
  }));
+

  $: showMoreButton =
    !loading &&
    !error &&
    patchCounters[state] &&
    patches.length < patchCounters[state];

-
  loadPatches();
+
  $: {
+
    page = 0;
+
    patches = [];
+
    loadPatches(state);
+
  }
</script>

<style>
@@ -97,9 +89,6 @@
    border-radius: var(--border-radius);
    overflow: hidden;
  }
-
  .loader {
-
    margin-top: 8rem;
-
  }
  .more {
    margin-top: 2rem;
    text-align: center;
@@ -118,47 +107,62 @@

<div class="patches">
  <div style="margin-bottom: 1rem;">
-
    <TabBar {options} on:select={switchState} active={state} />
+
    <div style="display: flex; gap: 0.5rem;">
+
      {#each options as option}
+
        {#if option.disabled}
+
          <SquareButton
+
            size="small"
+
            clickable={option.disabled}
+
            active={option.value === state}
+
            disabled={option.disabled}>
+
            {option.title}
+
          </SquareButton>
+
        {:else}
+
          <ProjectLink
+
            projectParams={{
+
              search: `state=${option.value}`,
+
            }}>
+
            <SquareButton
+
              size="small"
+
              clickable={option.disabled}
+
              active={option.value === state}
+
              disabled={option.disabled}>
+
              {option.title}
+
            </SquareButton>
+
          </ProjectLink>
+
        {/if}
+
      {/each}
+
    </div>
  </div>
  <div class="patches-list">
-
    {#if refresh}
-
      <div class="loader">
-
        <Loading center />
+
    {#each patches as patch}
+
      <div class="teaser">
+
        <PatchTeaser {baseUrl} {projectId} {patch} />
      </div>
    {:else}
-
      {#each patches as patch}
-
        <!-- svelte-ignore a11y-click-events-have-key-events -->
-
        <div
-
          class="teaser"
-
          on:click={() => {
-
            router.updateProjectRoute({
-
              view: {
-
                resource: "patch",
-
                params: { patch: patch.id },
-
              },
-
            });
-
          }}>
-
          <PatchTeaser {baseUrl} {projectId} {patch} />
-
        </div>
+
      {#if error}
+
        <ErrorMessage message="Couldn't load patches." stackTrace={error} />
+
      {:else if loading}
+
        <!-- We already show a loader below. -->
      {:else}
-
        {#if error}
-
          <ErrorMessage message="Couldn't load patches." stackTrace={error} />
-
        {:else}
-
          <Placeholder emoji="🍂">
-
            <div slot="title">{capitalize(state)} patches</div>
-
            <div slot="body">No patches matched the current filter</div>
-
          </Placeholder>
-
        {/if}
-
      {/each}
-
      <div class="more">
-
        {#if loading}
-
          <Loading small={page !== 0} center />
-
        {/if}
-

-
        {#if showMoreButton}
-
          <Button variant="foreground" on:click={loadPatches}>More</Button>
-
        {/if}
+
        <Placeholder emoji="🍂">
+
          <div slot="title">{capitalize(state)} patches</div>
+
          <div slot="body">No patches matched the current filter</div>
+
        </Placeholder>
+
      {/if}
+
    {/each}
+
  </div>
+
  <div class="more">
+
    {#if loading}
+
      <div style:margin-top={page === 0 ? "8rem" : ""}>
+
        <Loading small={page !== 0} center />
      </div>
    {/if}
+

+
    {#if showMoreButton}
+
      <Button variant="foreground" on:click={() => loadPatches(state)}>
+
        More
+
      </Button>
+
    {/if}
  </div>
</div>
modified src/views/projects/PeerSelector.svelte
@@ -2,7 +2,7 @@
  import type { Item } from "@app/components/Dropdown.svelte";
  import type { Remote } from "@httpd-client";

-
  import { createEventDispatcher, onMount } from "svelte";
+
  import { onMount } from "svelte";

  import { formatNodeId, truncateId } from "@app/lib/utils";

@@ -11,8 +11,9 @@
  import Dropdown from "@app/components/Dropdown.svelte";
  import Floating from "@app/components/Floating.svelte";
  import Icon from "@app/components/Icon.svelte";
+
  import ProjectLink from "@app/components/ProjectLink.svelte";

-
  export let peer: string | null = null;
+
  export let peer: string | undefined = undefined;
  export let peers: Remote[];

  let meta: Remote | undefined;
@@ -36,11 +37,6 @@
      };
    });
  });
-

-
  const dispatch = createEventDispatcher<{ peerChanged: string }>();
-
  const switchPeer = (peer: string) => {
-
    dispatch("peerChanged", peer);
-
  };
</script>

<style>
@@ -69,7 +65,7 @@
    display: flex;
    flex-direction: row;
    align-items: center;
-
    gap: 0;
+
    gap: 0.5rem;
  }
  .prefix {
    display: inline-block;
@@ -83,6 +79,12 @@
    height: 2rem;
    line-height: initial;
    background: var(--color-foreground-1);
+
    gap: 0.5rem;
+
  }
+

+
  .avatar-id {
+
    display: flex;
+
    gap: 0.25rem;
  }
</style>

@@ -93,39 +95,53 @@
        <Icon name="fork" />
      {/if}
      {#if meta}
-
        <span style:display="flex">
+
        <span class="avatar-id">
          <Avatar nodeId={meta.id} inline />
-
          <span style:color="var(--color-secondary-5)">did:key:</span>
-
          {truncateId(meta.id)}
+
          <!-- Ignore prettier to avoid getting a whitespace between
+
             did:key: and the nid due to a newline. -->
+
          <!-- prettier-ignore -->
+
          <span><span style:color="var(--color-secondary-5)">did:key:</span>{truncateId(meta.id)}</span>
        </span>
        {#if meta.delegate}
          <Badge variant="primary">delegate</Badge>
        {/if}
      {:else if peer}
-
        <span style:display="flex">
+
        <span class="avatar-id">
          <Avatar nodeId={peer} inline />
-
          <span style:color="var(--color-secondary-5)">did:key:</span>
-
          {truncateId(peer)}
+
          <!-- prettier-ignore -->
+
          <span><span style:color="var(--color-secondary-5)">did:key:</span>{truncateId(peer)}</span>
        </span>
      {/if}
    </div>
  </div>

  <svelte:fragment slot="modal">
-
    <Dropdown
-
      {items}
-
      selected={peer}
-
      on:select={e => switchPeer(e.detail.value)}>
-
      <div class="peer-item" slot="item" let:item>
-
        <Avatar nodeId={item.value} inline />
-
        <!-- We ignore prettier here for the following line
-
             to avoid getting a whitespace between did:key: and the nid due to a newline -->
-
        <!-- prettier-ignore -->
-
        <span><span class="prefix">did:key:</span>{item.value}</span>
-
        {#if item.badge}
-
          <Badge variant="primary">{item.badge}</Badge>
-
        {/if}
-
      </div>
+
    <Dropdown {items} selected={peer}>
+
      <svelte:fragment slot="item" let:item>
+
        <ProjectLink
+
          on:click
+
          projectParams={{
+
            peer: item.value,
+
            revision: undefined,
+
          }}>
+
          <div class="peer-item">
+
            <span class="avatar-id">
+
              <Avatar nodeId={item.value} inline />
+
              <div class="layout-desktop">
+
                <!-- prettier-ignore -->
+
                <span><span class="prefix">did:key:</span>{item.value}</span>
+
              </div>
+
              <div class="layout-mobile">
+
                <!-- prettier-ignore -->
+
                <span><span class="prefix">did:key:</span>{truncateId(item.value)}</span>
+
              </div>
+
            </span>
+
            {#if item.badge}
+
              <Badge variant="primary">{item.badge}</Badge>
+
            {/if}
+
          </div>
+
        </ProjectLink>
+
      </svelte:fragment>
    </Dropdown>
  </svelte:fragment>
</Floating>
modified src/views/projects/ProjectMeta.svelte
@@ -16,22 +16,28 @@

<style>
  .title {
-
    display: flex;
    align-items: center;
-
    justify-content: left;
+
    color: var(--color-secondary);
+
    display: flex;
    font-size: var(--font-size-x-large);
+
    font-weight: var(--font-weight-bold);
+
    justify-content: left;
    margin-bottom: 0.5rem;
+
    overflow-x: hidden;
+
    text-align: left;
+
    text-overflow: ellipsis;
  }
-
  .title .divider {
+
  .divider {
    color: var(--color-foreground-4);
    margin: 0 0.5rem;
    font-weight: var(--font-weight-normal);
  }
-
  .title .node-id {
+
  .node-id {
    color: var(--color-foreground-5);
    font-weight: var(--font-weight-normal);
    display: flex;
    align-items: center;
+
    white-space: nowrap;
  }
  .project-name:hover {
    color: inherit;
@@ -68,11 +74,15 @@
    .content {
      padding-left: 2rem;
    }
+
    .title {
+
      font-size: var(--font-size-medium);
+
      font-weight: var(--font-weight-bold);
+
    }
  }
</style>

<header class="content">
-
  <div class="title txt-bold txt-title">
+
  <div class="title">
    <span class="truncate">
      <ProjectLink
        projectParams={{
modified src/views/projects/SourceBrowser/Changeset.svelte
@@ -6,6 +6,7 @@
  import FileDiff from "@app/views/projects/SourceBrowser/FileDiff.svelte";

  export let diff: Diff;
+
  export let revision: string;

  const diffDescription = ({ modified, added, deleted }: Diff): string => {
    const s = [];
@@ -53,12 +54,12 @@
</div>
<div class="diff-listing">
  {#each diff.added as file}
-
    <FileDiff on:browse {file} mode="added" />
+
    <FileDiff on:browse {file} {revision} mode="added" />
  {/each}
  {#each diff.deleted as file}
-
    <FileDiff on:browse {file} mode="deleted" />
+
    <FileDiff on:browse {file} {revision} mode="deleted" />
  {/each}
  {#each diff.modified as file}
-
    <FileDiff on:browse {file} />
+
    <FileDiff on:browse {file} {revision} />
  {/each}
</div>
modified src/views/projects/SourceBrowser/FileDiff.svelte
@@ -1,23 +1,17 @@
-
<script lang="ts" strictEvents>
+
<script lang="ts">
  import type {
    DiffAddedDeletedModifiedChangeset,
    HunkLine,
  } from "@httpd-client";

-
  import { createEventDispatcher } from "svelte";
-

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

-
  const dispatch = createEventDispatcher<{ browse: string }>();
+
  import ProjectLink from "@app/components/ProjectLink.svelte";

  export let file: DiffAddedDeletedModifiedChangeset;
+
  export let revision: string;
  export let mode: string | null = null;

-
  function collapse() {
-
    collapsed = !collapsed;
-
  }
-

  let collapsed = false;

  function lineNumberR(line: HunkLine): string | number {
@@ -64,19 +58,19 @@
</script>

<style>
-
  .changeset-file {
+
  .wrapper {
    border: 1px solid var(--color-foreground-4);
    border-radius: var(--border-radius-small);
    margin-bottom: 2rem;
    line-height: 1.5rem;
  }
-
  .changeset-file header {
-
    cursor: pointer;
-
    height: 3rem;
-
    display: flex;
+
  .header {
    align-items: center;
    background: none;
    border-radius: 0;
+
    display: flex;
+
    flex-direction: row;
+
    height: 3rem;
    padding: 1rem;
  }
  main {
@@ -84,15 +78,8 @@
    border-top: 1px dashed var(--color-foreground-4);
    background: var(--color-background-1);
    border-radius: 0 0 var(--border-radius-small) var(--border-radius-small);
-
  }
-
  .changeset-file main {
    overflow-x: auto;
  }
-
  header {
-
    display: flex;
-
    flex-direction: row;
-
    justify-content: space-between;
-
  }
  .actions {
    display: flex;
    flex-direction: row;
@@ -105,69 +92,82 @@
    text-align: center;
    background-color: var(--color-foreground-2);
  }
-
  table.diff {
+
  .browse {
+
    margin-left: auto;
+
    cursor: pointer;
+
  }
+
  .expand-button {
+
    cursor: pointer;
+
    user-select: none;
+
    margin-right: 0.5rem;
+
  }
+
  .diff {
    font-family: var(--font-family-monospace);
    table-layout: fixed;
    border-collapse: collapse;
    margin: 0.5rem 0;
  }
-
  tr.diff-line {
+
  .diff-line {
    vertical-align: top;
  }
-
  tr.diff-line[data-type="+"] > * {
+
  .diff-line[data-type="+"] > * {
    color: var(--color-positive);
  }
-
  tr.diff-line[data-type="-"] > * {
+
  .diff-line[data-type="-"] > * {
    color: var(--color-negative);
  }
-
  td.diff-line-number {
+
  .diff-line-number {
    text-align: right;
    user-select: none;
    line-height: 1.5rem;
    min-width: 3rem;
  }
-
  td.diff-line-number[data-type="+"],
-
  td.diff-line-type[data-type="+"] {
+
  .diff-line-number[data-type="+"],
+
  .diff-line-type[data-type="+"] {
    background-color: var(--color-positive-2);
    color: var(--color-positive-6);
  }
-
  td.diff-line-number[data-type="-"],
-
  td.diff-line-type[data-type="-"] {
+
  .diff-line-number[data-type="-"],
+
  .diff-line-type[data-type="-"] {
    background-color: var(--color-negative-2);
    color: var(--color-negative-6);
  }
-
  td.diff-line-number.left {
+
  .diff-line-number.left {
    padding: 0 0.5rem 0 0.75rem;
  }
-
  td.diff-line-number.right {
+
  .diff-line-number.right {
    padding: 0 0.75rem 0 0.5rem;
  }
-
  td.diff-line-content {
+
  .diff-line-content {
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    width: 100%;
    padding-right: 0.5rem;
  }
-
  td.diff-line-type {
+
  .diff-line-type {
    text-align: center;
    padding-left: 0.75rem;
    padding-right: 0.75rem;
  }
-
  td.diff-expand-header {
+
  .diff-expand-header {
    padding-left: 0.5rem;
    color: var(--color-foreground-5);
  }
-
  td.diff-line-number {
+
  .diff-line-number {
    color: var(--color-foreground-5);
  }
-
  .browse {
-
    display: flex;
-
  }
</style>

-
<article id={file.path} class="changeset-file">
-
  <!-- svelte-ignore a11y-click-events-have-key-events -->
-
  <header class="file-header" on:click={collapse}>
+
<div id={file.path} class="wrapper">
+
  <header class="header">
+
    <!-- svelte-ignore a11y-click-events-have-key-events -->
+
    <div class="expand-button" on:click={() => (collapsed = !collapsed)}>
+
      {#if collapsed}
+
        <Icon name="chevron-right" />
+
      {:else}
+
        <Icon name="chevron-down" />
+
      {/if}
+
    </div>
    <div class="actions">
      <p class="txt-regular">{file.path}</p>
      {#if mode === "added"}
@@ -176,12 +176,16 @@
        <Badge variant="negative">deleted</Badge>
      {/if}
    </div>
-
    <div
-
      class="browse clickable"
-
      on:click|stopPropagation={() => dispatch("browse", file.path)}>
-
      <span title="View file" style="transform: scale(1.25);">
+
    <div class="browse" title="View file">
+
      <ProjectLink
+
        projectParams={{
+
          view: { resource: "tree" },
+
          path: file.path,
+
          revision,
+
          search: undefined,
+
        }}>
        <Icon name="browse" />
-
      </span>
+
      </ProjectLink>
    </div>
  </header>
  {#if !collapsed}
@@ -216,4 +220,4 @@
      {/if}
    </main>
  {/if}
-
</article>
+
</div>
modified src/views/projects/Tree.svelte
@@ -5,6 +5,7 @@

  import File from "./Tree/File.svelte";
  import Folder from "./Tree/Folder.svelte";
+
  import ProjectLink from "@app/components/ProjectLink.svelte";

  export let fetchTree: (path: string) => Promise<Tree | undefined>;
  export let path: string;
@@ -27,10 +28,16 @@
      currentPath={path}
      on:select={onSelect} />
  {:else}
-
    <File
-
      active={entry.path === path}
-
      loading={entry.path === loadingPath}
-
      name={entry.name}
-
      on:click={() => onSelect({ detail: entry.path })} />
+
    <ProjectLink
+
      projectParams={{
+
        view: { resource: "tree" },
+
        path: entry.path,
+
      }}
+
      on:click={() => onSelect({ detail: entry.path })}>
+
      <File
+
        active={entry.path === path}
+
        loading={entry.path === loadingPath}
+
        name={entry.name} />
+
    </ProjectLink>
  {/if}
{/each}
modified src/views/projects/Tree/File.svelte
@@ -46,8 +46,7 @@
  }
</style>

-
<!-- svelte-ignore a11y-click-events-have-key-events -->
-
<div class="file" class:active on:click>
+
<div class="file" class:active>
  <span class="name">{name}</span>
  <div class="spinner">
    {#if loading}
modified src/views/projects/Tree/Folder.svelte
@@ -4,6 +4,7 @@
  import { createEventDispatcher } from "svelte";

  import Loading from "@app/components/Loading.svelte";
+
  import ProjectLink from "@app/components/ProjectLink.svelte";

  import File from "./File.svelte";

@@ -84,13 +85,17 @@
              {loadingPath}
              {currentPath} />
          {:else}
-
            <File
-
              active={entry.path === currentPath}
-
              loading={entry.path === loadingPath}
-
              name={entry.name}
-
              on:click={() => {
-
                onSelectFile({ detail: entry.path });
-
              }} />
+
            <ProjectLink
+
              projectParams={{
+
                view: { resource: "tree" },
+
                path: entry.path,
+
              }}
+
              on:click={() => onSelectFile({ detail: entry.path })}>
+
              <File
+
                active={entry.path === currentPath}
+
                loading={entry.path === loadingPath}
+
                name={entry.name} />
+
            </ProjectLink>
          {/if}
        {/each}
      {/if}
modified src/views/seeds/View.svelte
@@ -2,7 +2,6 @@
  import type { Project, NodeStats } from "@httpd-client";
  import type { WeeklyActivity } from "@app/lib/commit";

-
  import * as router from "@app/lib/router";
  import { HttpdClient } from "@httpd-client";
  import { config } from "@app/lib/config";
  import { extractBaseUrl, isLocal, truncateId } from "@app/lib/utils";
@@ -11,6 +10,7 @@
  import Button from "@app/components/Button.svelte";
  import Clipboard from "@app/components/Clipboard.svelte";
  import ErrorMessage from "@app/components/ErrorMessage.svelte";
+
  import Link from "@app/components/Link.svelte";
  import Loading from "@app/components/Loading.svelte";
  import ProjectCard from "@app/components/ProjectCard.svelte";

@@ -58,23 +58,6 @@
    }
  }

-
  function goToProject(project: Project) {
-
    router.push({
-
      resource: "projects",
-
      params: {
-
        view: { resource: "tree" },
-
        id: project.id,
-
        hostnamePort:
-
          baseUrl.port === config.seeds.defaultHttpdPort
-
            ? baseUrl.hostname
-
            : `${baseUrl.hostname}:${baseUrl.port}`,
-
        revision: undefined,
-
        hash: undefined,
-
        search: undefined,
-
      },
-
    });
-
  }
-

  $: showMoreButton =
    !loadingProjects &&
    !error &&
@@ -90,12 +73,18 @@
    margin: 5rem 0;
  }
  .header {
+
    align-items: center;
+
    color: var(--color-secondary);
    display: flex;
-
    width: 100%;
    flex-direction: row;
-
    align-items: center;
+
    font-size: var(--font-size-large);
+
    font-weight: var(--font-weight-bold);
    justify-content: space-between;
    margin-bottom: 2rem;
+
    overflow-x: hidden;
+
    text-align: left;
+
    text-overflow: ellipsis;
+
    width: 100%;
  }
  table {
    border-collapse: collapse;
@@ -128,9 +117,7 @@

<div class="wrapper">
  <div class="header">
-
    <span class="txt-title txt-bold">
-
      {hostName}
-
    </span>
+
    {hostName}
  </div>

  {#await api.getRoot()}
@@ -168,13 +155,28 @@
      <div style:margin-top="1rem">
        {#each projectsWithActivity as { project, activity }}
          <div style:margin-bottom="0.5rem">
-
            <ProjectCard
-
              {activity}
-
              id={project.id}
-
              name={project.name}
-
              description={project.description}
-
              head={project.head}
-
              on:click={() => goToProject(project)} />
+
            <Link
+
              route={{
+
                resource: "projects",
+
                params: {
+
                  view: { resource: "tree" },
+
                  id: project.id,
+
                  hostnamePort:
+
                    baseUrl.port === config.seeds.defaultHttpdPort
+
                      ? baseUrl.hostname
+
                      : `${baseUrl.hostname}:${baseUrl.port}`,
+
                  revision: undefined,
+
                  hash: undefined,
+
                  search: undefined,
+
                },
+
              }}>
+
              <ProjectCard
+
                {activity}
+
                id={project.id}
+
                name={project.name}
+
                description={project.description}
+
                head={project.head} />
+
            </Link>
          </div>
        {/each}
      </div>
modified tests/e2e/hashRouter.spec.ts
@@ -34,7 +34,7 @@ test("navigation between seed and project pages", async ({ page }) => {
  await expectBackAndForwardNavigationWorks("/#/seeds/radicle.local", page);
  await expectUrlPersistsReload(page);

-
  await page.locator('role=button[name="Seed"]').click();
+
  await page.locator('role=link[name="127.0.0.1"]').click();
  await expect(page).toHaveURL("/#/seeds/127.0.0.1");
});

@@ -60,7 +60,7 @@ test.describe("project page navigation", () => {
    await page.goto(projectTreeURL);
    await expect(page).toHaveURL(projectTreeURL);

-
    await page.locator('role=button[name="Commit count"]').click();
+
    await page.locator('role=link[name="8 commits"]').click();
    await expect(page).toHaveURL(`/#${projectFixtureUrl}/history/main`);

    await expectBackAndForwardNavigationWorks(projectTreeURL, page);
modified tests/e2e/historyRouter.spec.ts
@@ -34,7 +34,7 @@ test("navigation between seed and project pages", async ({ page }) => {
  await expectBackAndForwardNavigationWorks("/seeds/radicle.local", page);
  await expectUrlPersistsReload(page);

-
  await page.locator('role=button[name="Seed"]').click();
+
  await page.locator('role=link[name="127.0.0.1"]').click();
  await expect(page).toHaveURL("/seeds/127.0.0.1");
});

@@ -60,7 +60,7 @@ test.describe("project page navigation", () => {
    await page.goto(projectTreeURL);
    await expect(page).toHaveURL(projectTreeURL);

-
    await page.locator('role=button[name="Commit count"]').click();
+
    await page.locator('role=link[name="8 commits"]').click();
    await expect(page).toHaveURL(
      `${projectFixtureUrl}/history/${aliceMainHead}`,
    );
modified tests/e2e/project.spec.ts
@@ -14,16 +14,21 @@ async function expectCounts(
  params: { commits: number; contributors: number },
  page: Page,
) {
-
  await expect(page.locator('role=button[name="Commit count"]')).toContainText(
-
    `${params.commits} ${params.commits === 1 ? "commit" : "commits"}`,
-
  );
  await expect(
-
    page.locator('role=button[name="Contributor count"]'),
-
  ).toContainText(
-
    `${params.contributors} ${
-
      params.contributors === 1 ? "contributor" : "contributors"
-
    }`,
-
  );
+
    page.locator(
+
      `role=link[name="${params.commits} ${
+
        params.commits === 1 ? "commit" : "commits"
+
      }"]`,
+
    ),
+
  ).toBeVisible();
+

+
  await expect(
+
    page.locator(
+
      `text=${params.contributors} ${
+
        params.contributors === 1 ? "contributor" : "contributors"
+
      }`,
+
    ),
+
  ).toBeVisible();
}

test("navigate to project", async ({ page }) => {
@@ -62,10 +67,10 @@ test("navigate to project", async ({ page }) => {

test("show source tree at specific revision", async ({ page }) => {
  await page.goto(projectFixtureUrl);
-
  await page.locator('role=button[name="Commit count"]').click();
+
  await page.locator('role=link[name="8 commits"]').click();

  await page
-
    .locator(".commit-teaser", { hasText: "335dd6d" })
+
    .locator(".teaser", { hasText: "335dd6d" })
    .getByTitle("Browse the repository at this point in the history")
    .click();

@@ -90,7 +95,7 @@ test("source file highlighting", async ({ page }) => {

test("navigate line numbers", async ({ page }) => {
  await page.goto(`${projectFixtureUrl}/tree/main/markdown/cheatsheet.md`);
-
  await page.locator('role=button[name="Raw"]').click();
+
  await page.locator(".markdown-toggle").click();

  await page.locator('[href="#L5"]').click();
  await expect(page.locator("#L5")).toHaveClass("line highlight");
@@ -225,7 +230,7 @@ test("markdown files", async ({ page }) => {

  // Switch between raw and rendered modes.
  {
-
    const rawButton = page.locator('role=button[name="Raw"]');
+
    const rawButton = page.locator(".markdown-toggle .square-button");

    await rawButton.click();
    await expect(rawButton).toHaveClass(/active/);
@@ -262,9 +267,9 @@ test("peer and branch switching", async ({ page }) => {
    await page.getByTitle("Change peer").click();
    await page.locator(`text=${aliceRemote}`).click();
    await expect(page.getByTitle("Change peer")).toHaveText(
-
      ` did:key: ${aliceRemote
-
        .substring(8)
-
        .substring(0, 6)}…${aliceRemote.slice(-6)} `,
+
      ` did:key:${aliceRemote.substring(8).substring(0, 6)}…${aliceRemote.slice(
+
        -6,
+
      )} `,
    );
    await expect(
      page.locator(
@@ -327,7 +332,7 @@ test("peer and branch switching", async ({ page }) => {
    await page.getByTitle("Change peer").click();
    await page.locator(`text=${bobRemote}`).click();
    await expect(page.getByTitle("Change peer")).toHaveText(
-
      ` did:key: ${bobRemote.substring(8).substring(0, 6)}…${bobRemote.slice(
+
      ` did:key:${bobRemote.substring(8).substring(0, 6)}…${bobRemote.slice(
        -6,
      )} `,
    );
modified tests/e2e/project/commit.spec.ts
@@ -13,7 +13,7 @@ test("navigation from commit list", async ({ page }) => {
  await page.goto(projectFixtureUrl);
  await page.getByTitle("Change peer").click();
  await page.locator(`text=${bobRemote}`).click();
-
  await page.locator('role=button[name="Commit count"]').click();
+
  await page.locator('role=link[name="9 commits"]').click();

  await page.locator("text=Update readme").click();
  await expect(page).toHaveURL(modifiedFileFixture);
@@ -31,7 +31,7 @@ test("relative timestamps", async ({ page }) => {
  });
  await page.goto(modifiedFileFixture);
  await expect(
-
    page.locator(`.commit header >> text=${"Bob Belcher committed now"}`),
+
    page.locator(`.commit .header >> text=${"Bob Belcher committed now"}`),
  ).toBeVisible();
});

@@ -40,7 +40,7 @@ test("modified file", async ({ page }) => {

  // Commit header.
  {
-
    const header = page.locator(".commit header");
+
    const header = page.locator(".commit .header");
    await expect(header.locator("text=Update readme")).toBeVisible();
    await expect(
      header.locator("text=1e0bb83a89b63da815f2fc24e7ae3c5ceb30e0eb"),
modified tests/e2e/project/commits.spec.ts
@@ -8,30 +8,28 @@ import {

test("peer and branch switching", async ({ page }) => {
  await page.goto(projectFixtureUrl);
-
  await page.locator('role=button[name="Commit count"]').click();
+
  await page.locator('role=link[name="8 commits"]').click();

  // Alice's peer.
  {
    await page.getByTitle("Change peer").click();
    await page.locator(`text=${aliceRemote}`).click();
    await expect(page.getByTitle("Change peer")).toHaveText(
-
      ` did:key: ${aliceRemote
-
        .substring(8)
-
        .substring(0, 6)}…${aliceRemote.slice(-6)} `,
+
      ` did:key:${aliceRemote.substring(8).substring(0, 6)}…${aliceRemote.slice(
+
        -6,
+
      )} `,
    );

    await expect(page.getByText("Thursday, November 17, 2022")).toBeVisible();
-
    await expect(
-
      page.locator(".commit-group-headers .commit-teaser"),
-
    ).toHaveCount(8);
+
    await expect(page.locator(".history .teaser")).toHaveCount(8);

-
    const latestCommit = page.locator(".commit-teaser").first();
+
    const latestCommit = page.locator(".teaser").first();
    await expect(latestCommit).toContainText(
      "Adds a new markdown file with an image with a relative path",
    );
    await expect(latestCommit).toContainText("fcc9294");

-
    const earliestCommit = page.locator(".commit-teaser").last();
+
    const earliestCommit = page.locator(".teaser").last();
    await expect(earliestCommit).toContainText(
      "Initialize an empty git repository",
    );
@@ -44,9 +42,7 @@ test("peer and branch switching", async ({ page }) => {
      "feature/branch d6318f7",
    );
    await expect(page.getByText("Thursday, November 17, 2022")).toBeVisible();
-
    await expect(
-
      page.locator(".commit-group-headers .commit-teaser"),
-
    ).toHaveCount(10);
+
    await expect(page.locator(".history .teaser")).toHaveCount(10);

    await page.getByTitle("Change branch").click();
    await page.locator("text=orphaned-branch").click();
@@ -55,9 +51,7 @@ test("peer and branch switching", async ({ page }) => {
      "orphaned-branch af3641c",
    );
    await expect(page.getByText("Thursday, November 17, 2022")).toBeVisible();
-
    await expect(
-
      page.locator(".commit-group-headers .commit-teaser"),
-
    ).toHaveCount(1);
+
    await expect(page.locator(".group .teaser")).toHaveCount(1);
  }

  // Bob's peer.
@@ -65,27 +59,26 @@ test("peer and branch switching", async ({ page }) => {
    await page.getByTitle("Change peer").click();
    await page.locator(`text=${bobRemote}`).click();
    await expect(page.getByTitle("Change peer")).toContainText(
-
      ` did:key: ${bobRemote.substring(8).substring(0, 6)}…${bobRemote.slice(
+
      ` did:key:${bobRemote.substring(8).substring(0, 6)}…${bobRemote.slice(
        -6,
      )} `,
    );

    await expect(page.getByText("Tuesday, December 20, 2022")).toBeVisible();
-
    await expect(
-
      page.locator(".commit-group-headers").first().locator(".commit-teaser"),
-
    ).toHaveCount(1);
+
    await expect(page.locator(".group").first().locator(".teaser")).toHaveCount(
+
      1,
+
    );

    await expect(page.getByText("Thursday, November 17, 2022")).toBeVisible();
-
    await expect(
-
      page.locator(".commit-group-headers").last().locator(".commit-teaser"),
-
    ).toHaveCount(6);
+
    await expect(page.locator(".group").last().locator(".teaser")).toHaveCount(
+
      6,
+
    );

-
    await page.pause();
-
    const latestCommit = page.locator(".commit-teaser").first();
+
    const latestCommit = page.locator(".teaser").first();
    await expect(latestCommit).toContainText("Update readme");
    await expect(latestCommit).toContainText("1e0bb83");

-
    const earliestCommit = page.locator(".commit-teaser").last();
+
    const earliestCommit = page.locator(".teaser").last();
    await expect(earliestCommit).toContainText(
      "Initialize an empty git repository",
    );
@@ -105,19 +98,19 @@ test("relative timestamps", async ({ page }) => {
  });

  await page.goto(projectFixtureUrl);
-
  await page.locator('role=button[name="Commit count"]').click();
+
  await page.locator('role=link[name="8 commits"]').click();

  await page.getByTitle("Change peer").click();
  await page.locator(`text=${bobRemote}`).click();
  await expect(page.getByTitle("Change peer")).toHaveText(
-
    ` did:key: ${bobRemote.substring(8).substring(0, 6)}…${bobRemote.slice(
+
    ` did:key:${bobRemote.substring(8).substring(0, 6)}…${bobRemote.slice(
      -6,
    )} `,
  );
-
  const latestCommit = page.locator(".commit-teaser").first();
+
  const latestCommit = page.locator(".teaser").first();
  await expect(latestCommit).toContainText("Bob Belcher committed now");
  await expect(latestCommit).toContainText("1e0bb83");
-
  const earliestCommit = page.locator(".commit-teaser").last();
+
  const earliestCommit = page.locator(".teaser").last();
  await expect(earliestCommit).toContainText(
    "Alice Liddell committed last month",
  );