Radish alpha
r
rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5
Radicle web interface
Radicle
Git
Improve layout responsiveness
Merged rudolfs opened 2 years ago
52 files changed +969 -672 9404c309 047ef049
modified public/index.css
@@ -97,17 +97,50 @@ pre {
  background-color: var(--color-fill-ghost);
}

-
@media (max-width: 720px) {
+
/*
+
  Breakpoints
+
  ===========
+
    mobile             0px -  719.98px
+
    small desktop    720px - 1010.98px
+
    medium desktop  1011px - 1349.98px
+
    desktop         1350px -      ∞ px
+
*/
+

+
@media (max-width: 719.98px) {
  body {
    min-width: 0;
  }
-
  .global-hide-on-mobile {
+
  .global-hide-on-mobile-down {
+
    display: none !important;
+
  }
+
}
+

+
@media (max-width: 1010.98px) {
+
  .global-hide-on-small-desktop-down {
+
    display: none !important;
+
  }
+
}
+

+
@media (max-width: 1349.98px) {
+
  .global-hide-on-medium-desktop-down {
+
    display: none !important;
+
  }
+
}
+

+
@media (min-width: 720px) {
+
  .global-hide-on-small-desktop-up {
+
    display: none !important;
+
  }
+
}
+

+
@media (min-width: 1011px) {
+
  .global-hide-on-medium-desktop-up {
    display: none !important;
  }
}

-
@media (min-width: 721px) {
-
  .global-hide-on-desktop {
+
@media (min-width: 1350px) {
+
  .global-hide-on-desktop-up {
    display: none !important;
  }
}
modified src/App/AppLayout.svelte
@@ -17,7 +17,7 @@
    height: 100%;
    overflow-y: scroll;
  }
-
  @media (max-width: 720px) {
+
  @media (max-width: 719.98px) {
    .app {
      display: grid;
      grid-template-rows: 1fr auto;
@@ -30,17 +30,17 @@
</style>

<div class="app">
-
  <div class="global-hide-on-mobile header">
+
  <div class="global-hide-on-mobile-down header">
    <Header />
  </div>
  <div class="content">
    <slot />
  </div>
  <div style:margin-top="auto">
-
    <div class="global-hide-on-mobile">
+
    <div class="global-hide-on-mobile-down">
      <Footer />
    </div>
-
    <div class="global-hide-on-desktop">
+
    <div class="global-hide-on-small-desktop-up">
      <MobileFooter />
    </div>
  </div>
modified src/App/Footer.svelte
@@ -40,7 +40,7 @@

<div class="footer">
  <div class="left">
-
    <Popover popoverPositionBottom="2rem" popoverPositionLeft="0">
+
    <Popover popoverPositionBottom="3rem" popoverPositionLeft="0">
      <IconButton slot="toggle" let:toggle on:click={toggle}>
        <IconSmall name="settings" />
        Settings
modified src/App/Header/Authenticate.svelte
@@ -41,7 +41,7 @@
</style>

{#if $httpdStore.state === "authenticated"}
-
  <Popover popoverPositionTop="3rem" popoverPositionRight="0">
+
  <Popover popoverPositionTop="2.5rem" popoverPositionRight="0">
    <Button slot="toggle" let:toggle on:click={toggle} variant="naked-toggle">
      <div class="peer-info">
        <div style:height="1.25rem" style:margin-right="0.5rem">
modified src/App/Header/Breadcrumbs.svelte
@@ -17,6 +17,7 @@
    font-weight: var(--font-weight-semibold);
    font-size: var(--font-size-small);
    white-space: nowrap;
+
    flex-wrap: wrap;
  }
</style>

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

-
<Popover popoverPositionTop="3rem" popoverPositionRight="0">
+
<Popover popoverPositionTop="2.5rem" popoverPositionRight="0">
  <Button slot="toggle" let:toggle on:click={toggle} variant={"naked-toggle"}>
    {#if node.state === "running"}
      <IconSmall name="online" />
modified src/App/Help.svelte
@@ -7,9 +7,7 @@

<style>
  .help {
-
    width: 18.5rem;
    font-size: var(--font-size-small);
-
    color: var(--color-foreground-dim);
    display: flex;
    flex-direction: column;
    gap: 1rem;
@@ -18,6 +16,8 @@
    display: flex;
    justify-content: space-between;
    width: 100%;
+
    white-space: nowrap;
+
    gap: 2rem;
  }
  .divider {
    border-bottom: 1px solid var(--color-fill-separator);
modified src/App/Settings.svelte
@@ -16,7 +16,6 @@

<style>
  .settings {
-
    width: 24rem;
    display: flex;
    flex-direction: column;
    align-items: center;
@@ -28,6 +27,8 @@
    display: flex;
    width: 100%;
    align-items: center;
+
    gap: 2rem;
+
    white-space: nowrap;
  }

  .right {
@@ -78,7 +79,7 @@
      </Radio>
    </div>
  </div>
-
  <div class="item">
+
  <div class="item global-hide-on-mobile-down">
    <div
      style="display: flex; flex-direction: row; align-items: center; gap: 0.5rem;">
      Make changes on the web (experimental)
modified src/components/Comment.svelte
@@ -54,8 +54,10 @@
  .card-header {
    display: flex;
    align-items: center;
+
    white-space: nowrap;
+
    flex-wrap: wrap;
    padding: 0 0.75rem;
-
    height: 1.5rem;
+
    min-height: 1.5rem;
    gap: 0.5rem;
    font-size: var(--font-size-small);
  }
@@ -143,19 +145,18 @@
        {utils.formatTimestamp(timestamp)}
      </span>
      {#if lastEdit}
-
        <div class="card-metadata">•</div>
        <div
          class="card-metadata"
          title={utils.formatEditedCaption(
            lastEdit.author,
            lastEdit.timestamp,
          )}>
-
          edited
+
          • edited
        </div>
      {/if}
      <div class="header-right">
        {#if id && editComment && state === "read"}
-
          <div class="edit-buttons">
+
          <div class="edit-buttons global-hide-on-mobile-down">
            <IconButton title="edit comment" on:click={() => (state = "edit")}>
              <IconSmall name={"edit"} />
            </IconButton>
@@ -199,15 +200,17 @@
    <div class="actions">
      {#if id && reactOnComment}
        {@const reactOnComment_ = reactOnComment}
-
        <ReactionSelector
-
          {reactions}
-
          on:select={async ({ detail: { authors, emoji } }) => {
-
            try {
-
              await reactOnComment_(authors, emoji);
-
            } finally {
-
              closeFocused();
-
            }
-
          }} />
+
        <div class="global-hide-on-mobile-down">
+
          <ReactionSelector
+
            {reactions}
+
            on:select={async ({ detail: { authors, emoji } }) => {
+
              try {
+
                await reactOnComment_(authors, emoji);
+
              } finally {
+
                closeFocused();
+
              }
+
            }} />
+
        </div>
      {/if}
      {#if id && reactions && reactions.length > 0}
        <Reactions handleReaction={reactOnComment} {reactions} />
modified src/components/ErrorMessage.svelte
@@ -42,6 +42,14 @@
    font-size: var(--font-size-small);
    text-align: center;
  }
+
  .command {
+
    max-width: 25rem;
+
  }
+
  @media (max-width: 719.98px) {
+
    .command {
+
      max-width: 20rem;
+
    }
+
  }
</style>

<div class="error">
@@ -54,13 +62,13 @@
  {#if error}
    <div class="help">
      If you need help resolving this issue, copy the error message
-
      <br />
+
      <br class="global-hide-on-mobile-down" />
      below and send it to us on
      <ExternalLink href={config.supportWebsite}>
        {config.supportWebsite}
      </ExternalLink>
    </div>
-
    <div style:max-width="25rem">
+
    <div class="command">
      <Command
        command={JSON.stringify(error, Object.getOwnPropertyNames(error))}
        fullWidth
modified src/components/File.svelte
@@ -56,6 +56,19 @@
    border-bottom-left-radius: var(--border-radius-small);
    border-bottom-right-radius: var(--border-radius-small);
  }
+
  @media (max-width: 719.98px) {
+
    .header {
+
      border-radius: 0;
+
      border-left: 0;
+
      border-right: 0;
+
      padding: 0 1rem 0 1rem;
+
    }
+
    .container {
+
      border-radius: 0;
+
      border-left: 0;
+
      border-right: 0;
+
    }
+
  }
</style>

<div bind:this={header} class="header" class:collapsed={!expanded} class:sticky>
modified src/components/InlineMarkdown.svelte
@@ -42,4 +42,5 @@
  class:txt-small={fontSize === "small"}
  class:txt-tiny={fontSize === "tiny"}>
  {@html render(content)}
+
  <slot />
</span>
modified src/components/Markdown.svelte
@@ -215,6 +215,7 @@
  }
  .markdown {
    max-width: 1024px;
+
    word-break: break-word;
  }
  .front-matter {
    font-size: var(--font-size-tiny);
modified src/components/NodeId.svelte
@@ -34,6 +34,7 @@
    font-family: var(--font-family-monospace);
    font-weight: var(--font-weight-semibold);
    font-size: var(--font-size-small);
+
    white-space: nowrap;
  }
  .popover-container {
    display: flex;
modified src/components/Thread.svelte
@@ -67,6 +67,11 @@
  .reply {
    padding: 1rem;
  }
+
  @media (max-width: 719.98px) {
+
    .comments {
+
      border-radius: 0;
+
    }
+
  }
</style>

<div class="comments">
@@ -115,7 +120,7 @@
    </div>
  {/if}
  {#if createReply}
-
    <div id={`reply-${root.id}`} class="reply">
+
    <div id={`reply-${root.id}`} class="reply global-hide-on-mobile-down">
      <CommentToggleInput
        {rawPath}
        focus
modified src/views/home/Index.svelte
@@ -143,7 +143,7 @@
    text-overflow: ellipsis;
  }

-
  @media (max-width: 720px) {
+
  @media (max-width: 719.98px) {
    .wrapper {
      width: 100%;
      padding: 1rem;
@@ -156,9 +156,9 @@
</style>

<AppLayout>
-
  <div class="wrapper">
+
  <div class="wrapper" style:padding-bottom="2.5rem">
    {#if nodeId}
-
      <div class="global-hide-on-mobile">
+
      <div class="global-hide-on-mobile-down">
        <HomepageSection
          loading={$httpdStore.state !== "stopped" &&
            localProjects === undefined}
@@ -235,18 +235,15 @@
          </div>
        {/if}
        {#if !nodeId}
-
          <div class="global-hide-on-mobile">
-
            <Popover
-
              popoverPositionTop="1.5rem"
-
              popoverPositionLeft="0"
-
              popoverPositionRight="-15rem">
+
          <div class="global-hide-on-mobile-down">
+
            <Popover popoverPositionTop="2.5rem" popoverPositionLeft="0">
              <IconButton slot="toggle" let:toggle on:click={toggle}>
                <span style:color="var(--color-fill-gray)">
                  <IconSmall name="info" />
                </span>
              </IconButton>

-
              <div slot="popover" class="popover txt-small">
+
              <div slot="popover" class="popover txt-small" style:width="15rem">
                <div style:padding-bottom="0.5rem">
                  To browse your local projects, run:
                </div>
modified src/views/nodes/ScopePolicyPopover.svelte
@@ -47,7 +47,7 @@
    <Popover
      {popoverPositionRight}
      {popoverPositionLeft}
-
      popoverPositionBottom="1.5rem">
+
      popoverPositionBottom="2.5rem">
      <IconButton slot="toggle" let:toggle on:click={toggle}>
        <span style:color="var(--color-fill-gray)">
          <IconSmall name="info" />
modified src/views/nodes/View.svelte
@@ -116,7 +116,7 @@
    font-size: var(--font-size-small);
    font-weight: var(--font-weight-regular);
  }
-
  @media (max-width: 720px) {
+
  @media (max-width: 719.98px) {
    .wrapper {
      width: 100%;
      padding: 1rem;
@@ -147,10 +147,10 @@
              <!-- else this is probably a local node -->
              <!-- So we show only the nid -->
              <CopyableId id={nid} style="oid">
-
                <div class="global-hide-on-desktop">
+
                <div class="global-hide-on-small-desktop-up">
                  {truncateId(nid)}
                </div>
-
                <div class="global-hide-on-mobile">
+
                <div class="global-hide-on-mobile-down">
                  {nid}
                </div>
              </CopyableId>
@@ -166,19 +166,19 @@
        <div class="txt-semibold">
          {isLocal(baseUrl.hostname) ? "Seeded" : "Pinned"} projects
        </div>
-
        <div class="global-hide-on-mobile" style:margin-left="auto">
+
        <div class="global-hide-on-mobile-down" style:margin-left="auto">
          {#if policy && scope}
            <ScopePolicyPopover {scope} {policy} popoverPositionRight="0" />
          {/if}
        </div>
      </div>
-
      <div class="subtitle global-hide-on-desktop">
+
      <div class="subtitle global-hide-on-small-desktop-up">
        {#if policy && scope}
-
          <ScopePolicyPopover {scope} {policy} popoverPositionLeft="-5.5rem" />
+
          <ScopePolicyPopover {scope} {policy} popoverPositionRight="-4.5rem" />
        {/if}
      </div>

-
      <div style:margin-top="1rem">
+
      <div style:margin-top="1rem" style:padding-bottom="2.5rem">
        {#await fetchProjectInfos( baseUrl, { show: isLocal(baseUrl.hostname) ? "all" : "pinned", perPage: stats.repos.total }, )}
          <div style:height="35vh">
            <Loading small center />
modified src/views/projects/Changeset.svelte
@@ -3,8 +3,9 @@

  import FileDiff from "@app/views/projects/Changeset/FileDiff.svelte";
  import FileLocationChange from "@app/views/projects/Changeset/FileLocationChange.svelte";
-
  import Observer, { intersection } from "@app/components/Observer.svelte";
  import IconButton from "@app/components/IconButton.svelte";
+
  import IconSmall from "@app/components/IconSmall.svelte";
+
  import Observer, { intersection } from "@app/components/Observer.svelte";

  export let diff: Diff;
  export let files: Record<string, CommitBlob>;
@@ -58,12 +59,21 @@
  .summary {
    font-size: var(--font-size-small);
  }
+
  @media (max-width: 719.98px) {
+
    .diff-list {
+
      padding: 1rem 0;
+
    }
+
    .header {
+
      align-items: flex-start;
+
    }
+
  }
</style>

<div class="header">
  <div class="summary">
    <span>{diffDescription(diff.files)}</span>
    with
+
    <br class="global-hide-on-small-desktop-up" />
    <span class:additions={diff.stats.insertions > 0}>
      {diff.stats.insertions}
      {pluralize("insertion", diff.stats.insertions)}
@@ -75,15 +85,15 @@
    </span>
  </div>
  {#if diff.stats.filesChanged > 1}
-
    <div style:display="flex" style:gap="1rem">
-
      <IconButton on:click={() => (expanded = !expanded)}>
-
        {#if expanded === true}
-
          Collapse all
-
        {:else}
-
          Expand all
-
        {/if}
-
      </IconButton>
-
    </div>
+
    <IconButton on:click={() => (expanded = !expanded)}>
+
      {#if expanded === true}
+
        <IconSmall name="collapse" />
+
        <span class="global-hide-on-mobile-down">Collapse all</span>
+
      {:else}
+
        <IconSmall name="expand" />
+
        <span class="global-hide-on-mobile-down">Expand all</span>
+
      {/if}
+
    </IconButton>
  {/if}
</div>

modified src/views/projects/Cob/AssigneeInput.svelte
@@ -81,6 +81,7 @@
    flex-wrap: wrap;
    flex-direction: row;
    gap: 0.5rem;
+
    font-size: var(--font-size-small);
  }
  .assignee {
    display: flex;
@@ -96,9 +97,29 @@
    position: relative;
    margin-top: 0.5rem;
  }
+

+
  @media (max-width: 1349.98px) {
+
    .wrapper {
+
      display: flex;
+
      flex-direction: row;
+
      gap: 1rem;
+
      align-items: flex-start;
+
    }
+
    .header {
+
      margin-bottom: 0;
+
      height: 2rem;
+
      display: flex;
+
      align-items: center;
+
    }
+
    .no-assignees {
+
      height: 2rem;
+
      display: flex;
+
      align-items: center;
+
    }
+
  }
</style>

-
<div>
+
<div class="wrapper">
  <div class="header">Assignees</div>
  <div class="body">
    {#if locallyAuthenticated}
@@ -150,14 +171,16 @@
          </div>
        {/if}
      {:else}
-
        <Badge
-
          variant="outline"
-
          size="small"
-
          title="add assignee"
-
          round
-
          on:click={() => (showInput = true)}>
-
          <IconSmall name="plus" />
-
        </Badge>
+
        <div class="global-hide-on-mobile-down">
+
          <Badge
+
            variant="outline"
+
            size="small"
+
            title="add assignee"
+
            round
+
            on:click={() => (showInput = true)}>
+
            <IconSmall name="plus" />
+
          </Badge>
+
        </div>
      {/if}
    {:else}
      {#each updatedAssignees as assignee}
@@ -168,7 +191,7 @@
          </div>
        </Badge>
      {:else}
-
        <div class="txt-missing">No assignees</div>
+
        <div class="txt-missing no-assignees">No assignees</div>
      {/each}
    {/if}
  </div>
modified src/views/projects/Cob/CobCommitTeaser.svelte
@@ -50,6 +50,10 @@
    margin: 0.5rem 0;
    font-size: var(--font-size-tiny);
  }
+
  pre {
+
    white-space: pre-wrap;
+
    word-wrap: break-word;
+
  }
</style>

<div class="teaser" aria-label="commit-teaser">
@@ -81,10 +85,15 @@
        <pre>{commit.description.trim()}</pre>
      </div>
    {/if}
+
    <div class="global-hide-on-small-desktop-up">
+
      <CompactCommitAuthorship {commit} />
+
    </div>
  </div>
  <div class="right">
    <div style="display: flex; gap: 0.5rem; height: 21px; align-items: center;">
-
      <CompactCommitAuthorship {commit} />
+
      <div class="global-hide-on-mobile-down">
+
        <CompactCommitAuthorship {commit} />
+
      </div>
      <IconButton title="Browse repo at this commit">
        <Link
          route={{
modified src/views/projects/Cob/CobHeader.svelte
@@ -15,7 +15,7 @@
  }
  .summary {
    display: flex;
-
    align-items: center;
+
    align-items: flex-start;
    justify-content: space-between;
    gap: 0.5rem;
    margin-bottom: 0.5rem;
@@ -36,8 +36,8 @@
  </div>
  <div class="subtitle">
    <slot name="state" />
-
    <slot name="author" />
  </div>
+
  <slot name="subtitle" />
  <div class="description">
    <slot name="description" />
  </div>
modified src/views/projects/Cob/Embeds.svelte
@@ -9,9 +9,6 @@

<style>
  .header {
-
    display: flex;
-
    gap: 1rem;
-
    align-items: center;
    font-size: var(--font-size-small);
    margin-bottom: 0.75rem;
  }
@@ -20,14 +17,26 @@
    flex-wrap: wrap;
    flex-direction: row;
    gap: 0.5rem;
-
    margin-bottom: 1.25rem;
+
    font-size: var(--font-size-small);
+
  }
+

+
  @media (max-width: 1349.98px) {
+
    .wrapper {
+
      display: flex;
+
      flex-direction: row;
+
      gap: 1rem;
+
      align-items: flex-start;
+
    }
+
    .header {
+
      margin-bottom: 0;
+
      display: flex;
+
      align-items: center;
+
    }
  }
</style>

-
<div>
-
  <div class="header">
-
    <span>Attachments</span>
-
  </div>
+
<div class="wrapper">
+
  <div class="header">Attachments</div>
  <div class="body">
    {#each embeds as embed}
      <Badge variant="neutral">
modified src/views/projects/Cob/LabelInput.svelte
@@ -57,16 +57,17 @@
</script>

<style>
-
  .metadata-section-header {
+
  .header {
    font-size: var(--font-size-small);
    margin-bottom: 0.75rem;
  }
-
  .metadata-section-body {
+
  .body {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    flex-direction: row;
    gap: 0.5rem;
+
    font-size: var(--font-size-small);
  }
  .validation-message {
    display: flex;
@@ -76,11 +77,30 @@
    position: relative;
    margin-top: 0.5rem;
  }
+
  @media (max-width: 1349.98px) {
+
    .wrapper {
+
      display: flex;
+
      flex-direction: row;
+
      gap: 1rem;
+
      align-items: flex-start;
+
    }
+
    .header {
+
      margin-bottom: 0;
+
      height: 2rem;
+
      display: flex;
+
      align-items: center;
+
    }
+
    .no-labels {
+
      height: 2rem;
+
      display: flex;
+
      align-items: center;
+
    }
+
  }
</style>

-
<div>
-
  <div class="metadata-section-header">Labels</div>
-
  <div class="metadata-section-body">
+
<div class="wrapper">
+
  <div class="header">Labels</div>
+
  <div class="body">
    {#if locallyAuthenticated}
      {#each updatedLabels as label}
        <Badge
@@ -124,14 +144,16 @@
          </div>
        {/if}
      {:else}
-
        <Badge
-
          variant="outline"
-
          size="small"
-
          title="add labels"
-
          round
-
          on:click={() => (showInput = true)}>
-
          <IconSmall name="plus"></IconSmall>
-
        </Badge>
+
        <div class="global-hide-on-mobile-down">
+
          <Badge
+
            variant="outline"
+
            size="small"
+
            title="add labels"
+
            round
+
            on:click={() => (showInput = true)}>
+
            <IconSmall name="plus"></IconSmall>
+
          </Badge>
+
        </div>
      {/if}
    {:else}
      {#each updatedLabels as label}
@@ -139,7 +161,7 @@
          {label}
        </Badge>
      {:else}
-
        <div class="txt-missing">No labels</div>
+
        <div class="txt-missing no-labels">No labels</div>
      {/each}
    {/if}
  </div>
added src/views/projects/Cob/Labels.svelte
@@ -0,0 +1,25 @@
+
<script lang="ts">
+
  import Badge from "@app/components/Badge.svelte";
+

+
  export let labels: string[];
+
</script>
+

+
<style>
+
  .label {
+
    overflow: hidden;
+
    text-overflow: ellipsis;
+
  }
+
</style>
+

+
{#each labels.slice(0, 4) as label}
+
  <Badge style="max-width:7rem" variant="neutral">
+
    <span class="label">{label}</span>
+
  </Badge>
+
{/each}
+
{#if labels.length > 4}
+
  <Badge title={labels.slice(4, undefined).join(" ")} variant="neutral">
+
    <span class="label">
+
      +{labels.length - 4} more label{labels.length > 5 ? "s" : ""}
+
    </span>
+
  </Badge>
+
{/if}
added src/views/projects/Cob/Reviews.svelte
@@ -0,0 +1,75 @@
+
<script lang="ts">
+
  import type { PatchReviews } from "../Patch.svelte";
+

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

+
  export let reviews: PatchReviews;
+
</script>
+

+
<style>
+
  .header {
+
    font-size: var(--font-size-small);
+
    margin-bottom: 0.75rem;
+
  }
+
  .body {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 0.5rem;
+
    font-size: var(--font-size-small);
+
  }
+
  .review {
+
    color: var(--color-fill-gray);
+
    display: inline-flex;
+
    align-items: center;
+
    gap: 0.5rem;
+
  }
+
  .review-accept {
+
    color: var(--color-foreground-success);
+
  }
+
  .review-reject {
+
    color: var(--color-foreground-red);
+
  }
+
  @media (max-width: 1349.98px) {
+
    .wrapper {
+
      display: flex;
+
      flex-direction: row;
+
      gap: 1rem;
+
      align-items: center;
+
    }
+
    .header {
+
      margin-bottom: 0;
+
    }
+
    .no-reviews {
+
      display: flex;
+
      align-items: center;
+
    }
+
    .body {
+
      flex-direction: row;
+
    }
+
  }
+
</style>
+

+
<div class="wrapper">
+
  <div class="header">Reviews</div>
+
  <div class="body">
+
    {#each Object.values(reviews) as { latest, review }}
+
      <div class="review" class:txt-missing={!latest}>
+
        <span
+
          class:review-accept={review.verdict === "accept"}
+
          class:review-reject={review.verdict === "reject"}>
+
          {#if review.verdict === "accept"}
+
            <IconSmall name="checkmark" />
+
          {:else if review.verdict === "reject"}
+
            <IconSmall name="cross" />
+
          {:else}
+
            <IconSmall name="chat" />
+
          {/if}
+
        </span>
+
        <NodeId nodeId={review.author.id} alias={review.author.alias} />
+
      </div>
+
    {:else}
+
      <div class="txt-missing no-reviews">No reviews</div>
+
    {/each}
+
  </div>
+
</div>
modified src/views/projects/Cob/Revision.svelte
@@ -201,6 +201,7 @@
  }
  .revision-description {
    margin-left: 2.75rem;
+
    padding-right: 0.5rem;
  }
  .author-metadata {
    color: var(--color-fill-gray);
@@ -216,16 +217,18 @@
    display: flex;
    flex-direction: column;
    justify-content: center;
-
    gap: 0.5rem;
    min-height: 2.5rem;
    padding: 0.5rem 0;
    font-size: var(--font-size-small);
+
    gap: 0.5rem;
  }
  .authorship-header {
-
    display: flex;
+
    display: inline-flex;
+
    white-space: nowrap;
+
    flex-wrap: wrap;
    align-items: center;
    padding: 0 0.5rem;
-
    height: 1.5rem;
+
    min-height: 1.5rem;
    gap: 0.5rem;
    font-size: var(--font-size-small);
  }
@@ -276,6 +279,14 @@
    margin-left: 1.25rem;
    background-color: var(--color-fill-separator);
  }
+
  @media (max-width: 719.98px) {
+
    .revision-box {
+
      border-radius: 0;
+
    }
+
    .action {
+
      border-radius: 0;
+
    }
+
  }
</style>

<div class="revision" style:margin-bottom={expanded ? "2rem" : "0.5rem"}>
@@ -289,7 +300,9 @@
        </span>
      </div>
      <div class="revision-data">
-
        <span title={utils.absoluteTimestamp(revisionTimestamp)}>
+
        <span
+
          class="global-hide-on-mobile-down"
+
          title={utils.absoluteTimestamp(revisionTimestamp)}>
          {utils.formatTimestamp(revisionTimestamp)}
        </span>
        {#if loading}
@@ -313,7 +326,7 @@
        {/if}
        <Popover
          popoverPadding="0"
-
          popoverPositionTop="2.5rem"
+
          popoverPositionTop={expanded ? "3rem" : "2.5rem"}
          popoverPositionRight="0"
          popoverBorderRadius="var(--border-radius-small)">
          <IconButton
@@ -419,17 +432,18 @@
              {utils.formatTimestamp(revisionTimestamp)}
            </span>
            {#if revisionEdits.length > 1 && lastEdit}
-
              <div class="author-metadata">•</div>
              <div
                class="author-metadata"
                title={utils.formatEditedCaption(
                  lastEdit.author,
                  lastEdit.timestamp,
                )}>
-
                edited
+
                • edited
              </div>
            {/if}
-
            <div style="display: flex; gap: 0.5rem; margin-left: auto;">
+
            <div
+
              class="global-hide-on-mobile-down"
+
              style="display: flex; gap: 0.5rem; margin-left: auto;">
              {#if canEdit(revisionAuthor.id) && editRevision && revisionState === "read"}
                <IconButton
                  title="edit revision"
@@ -470,15 +484,17 @@
            <div class="actions">
              {#if reactOnRevision}
                {@const reactOnRevision_ = reactOnRevision}
-
                <ReactionSelector
-
                  reactions={revisionReactions}
-
                  on:select={async ({ detail: { emoji, authors } }) => {
-
                    try {
-
                      await reactOnRevision_(authors, emoji);
-
                    } finally {
-
                      closeFocused();
-
                    }
-
                  }} />
+
                <div class="global-hide-on-mobile-down">
+
                  <ReactionSelector
+
                    reactions={revisionReactions}
+
                    on:select={async ({ detail: { emoji, authors } }) => {
+
                      try {
+
                        await reactOnRevision_(authors, emoji);
+
                      } finally {
+
                        closeFocused();
+
                      }
+
                    }} />
+
                </div>
              {/if}
              {#if revisionReactions && revisionReactions.length > 0}
                <Reactions
@@ -507,7 +523,7 @@
      {#if error}
        <div
          class="diff-error txt-monospace txt-small"
-
          style:border-radius="var(--border-radius-small">
+
          style:border-radius="var(--border-radius-small)">
          <ErrorMessage
            title="Failed to load diff for this revision"
            description="Make sure you are able to connect to the seed <code>${utils.baseUrlToString(
added src/views/projects/CommentCounter.svelte
@@ -0,0 +1,20 @@
+
<script lang="ts">
+
  import IconSmall from "@app/components/IconSmall.svelte";
+

+
  export let commentCount: number;
+
</script>
+

+
<style>
+
  .comments {
+
    color: var(--color-foreground-dim);
+
    font-size: var(--font-size-tiny);
+
    display: flex;
+
    align-items: center;
+
    gap: 0.25rem;
+
  }
+
</style>
+

+
<div class="comments">
+
  <IconSmall name="chat" />
+
  <span>{commentCount}</span>
+
</div>
modified src/views/projects/Commit.svelte
@@ -39,6 +39,11 @@
    white-space: pre-wrap;
    margin-top: 1.5rem;
  }
+
  .button-container {
+
    margin-left: auto;
+
    display: flex;
+
    gap: 0.5rem;
+
  }
</style>

<Layout {baseUrl} {project}>
@@ -50,7 +55,7 @@
            stripEmphasizedStyling
            fontSize="large"
            content={header.summary} />
-
          <div style:margin-left="auto" style:display="flex" style:gap="0.5rem">
+
          <div class="button-container">
            <Link
              route={{
                resource: "project.source",
modified src/views/projects/Commit/CommitTeaser.svelte
@@ -64,6 +64,10 @@
    margin: 0.5rem 0;
    font-size: var(--font-size-small);
  }
+
  pre {
+
    white-space: pre-wrap;
+
    word-wrap: break-word;
+
  }
</style>

<div class="teaser">
added src/views/projects/DiffStatBadgeLoader.svelte
@@ -0,0 +1,48 @@
+
<script lang="ts">
+
  import type { BaseUrl, Patch, Revision } from "@httpd-client";
+

+
  import { HttpdClient } from "@httpd-client";
+
  import { formatCommit } from "@app/lib/utils";
+

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

+
  export let projectId: string;
+
  export let baseUrl: BaseUrl;
+
  export let patch: Patch;
+
  export let latestRevision: Revision;
+

+
  $: diffPromise = api.project.getDiff(
+
    projectId,
+
    latestRevision.base,
+
    latestRevision.oid,
+
  );
+

+
  const api = new HttpdClient(baseUrl);
+
</script>
+

+
{#await diffPromise}
+
  <Loading small />
+
{:then { diff }}
+
  <Link
+
    title="Compare {formatCommit(latestRevision.base)}..{formatCommit(
+
      latestRevision.oid,
+
    )}"
+
    route={{
+
      resource: "project.patch",
+
      project: projectId,
+
      node: baseUrl,
+
      patch: patch.id,
+
      view: {
+
        name: "diff",
+
        fromCommit: latestRevision.base,
+
        toCommit: latestRevision.oid,
+
      },
+
    }}>
+
    <DiffStatBadge
+
      hoverable
+
      insertions={diff.stats.insertions}
+
      deletions={diff.stats.deletions} />
+
  </Link>
+
{/await}
modified src/views/projects/Header/CloneButton.svelte
@@ -42,7 +42,7 @@

<Popover popoverPositionTop="2.5rem" popoverPositionRight="0">
  <Button slot="toggle" let:toggle on:click={toggle} variant="outline">
-
    Clone
+
    <span class="global-hide-on-small-desktop-down">Clone</span>
    <Icon name="download" />
  </Button>

modified src/views/projects/History.svelte
@@ -87,7 +87,7 @@

<style>
  .more {
-
    margin: 2rem 0;
+
    margin-top: 2rem;
    min-height: 3rem;
    display: flex;
    align-items: center;
modified src/views/projects/Issue.svelte
@@ -249,14 +249,17 @@
    }
  }

-
  async function saveLabels(sessionId: string, labels: string[]) {
+
  async function saveLabels(labels: string[]) {
    try {
-
      await api.project.updateIssue(
-
        project.id,
-
        issue.id,
-
        { type: "label", labels },
-
        sessionId,
-
      );
+
      if (session) {
+
        labelState = "submit";
+
        await api.project.updateIssue(
+
          project.id,
+
          issue.id,
+
          { type: "label", labels },
+
          session.id,
+
        );
+
      }
    } catch (error) {
      if (error instanceof Error) {
        modal.show({
@@ -275,21 +278,22 @@
        });
      }
    } finally {
+
      labelState = "read";
      await refreshIssue();
    }
  }

-
  async function saveAssignees(
-
    sessionId: string,
-
    assignees: Reaction["authors"],
-
  ) {
+
  async function saveAssignees(assignees: Reaction["authors"]) {
    try {
-
      await api.project.updateIssue(
-
        project.id,
-
        issue.id,
-
        { type: "assign", assignees: assignees.map(({ id }) => id) },
-
        sessionId,
-
      );
+
      if (session) {
+
        assigneeState = "submit";
+
        await api.project.updateIssue(
+
          project.id,
+
          issue.id,
+
          { type: "assign", assignees: assignees.map(({ id }) => id) },
+
          session.id,
+
        );
+
      }
    } catch (error) {
      if (error instanceof Error) {
        modal.show({
@@ -308,6 +312,7 @@
        });
      }
    } finally {
+
      assigneeState = "read";
      await refreshIssue();
    }
  }
@@ -421,6 +426,7 @@
    display: flex;
    flex: 1;
    flex-direction: column;
+
    min-width: 0;
    background-color: var(--color-background-float);
  }
  .bottom {
@@ -438,7 +444,6 @@
  .metadata {
    display: flex;
    flex-direction: column;
-
    font-size: var(--font-size-small);
    padding: 1rem;
    border-left: 1px solid var(--color-border-hint);
    gap: 1.5rem;
@@ -450,14 +455,6 @@
    flex-direction: column;
  }

-
  .author {
-
    display: flex;
-
    align-items: center;
-
    flex-wrap: nowrap;
-
    gap: 0.5rem;
-
    font-family: var(--font-family-sans-serif);
-
    font-size: var(--font-size-small);
-
  }
  .author-metadata {
    color: var(--color-fill-gray);
    font-size: var(--font-size-small);
@@ -477,10 +474,9 @@
    align-items: center;
    margin-left: -0.25rem;
  }
-

-
  @media (max-width: 720px) {
-
    .issue {
-
      display: block;
+
  @media (max-width: 719.98px) {
+
    .bottom {
+
      padding: 0;
    }
  }
</style>
@@ -509,24 +505,28 @@
          </div>
          <div style="display: flex; gap: 0.5rem;">
            {#if session && role.isDelegateOrAuthor(session.publicKey, delegates, issue.author.id) && issueState === "read"}
-
              <Button
-
                variant="outline"
-
                title="edit issue"
-
                on:click={() => (issueState = "edit")}>
-
                <IconSmall name={"edit"} />
-
                Edit
-
              </Button>
+
              <div class="global-hide-on-mobile-down">
+
                <Button
+
                  variant="outline"
+
                  title="edit issue"
+
                  on:click={() => (issueState = "edit")}>
+
                  <IconSmall name={"edit"} />
+
                  Edit
+
                </Button>
+
              </div>
            {/if}
            {#if issueState === "read"}
              <Share {baseUrl} />
              {#if session && role.isDelegateOrAuthor(session.publicKey, delegates, issue.author.id)}
-
                <CobStateButton
-
                  items={items.filter(
-
                    ([, state]) => !isEqual(state, issue.state),
-
                  )}
-
                  {selectedItem}
-
                  state={issue.state}
-
                  save={partial(saveStatus, session.id)} />
+
                <div class="global-hide-on-small-desktop-down">
+
                  <CobStateButton
+
                    items={items.filter(
+
                      ([, state]) => !isEqual(state, issue.state),
+
                    )}
+
                    {selectedItem}
+
                    state={issue.state}
+
                    save={partial(saveStatus, session.id)} />
+
                </div>
              {/if}
            {/if}
          </div>
@@ -544,7 +544,55 @@
              {issue.state.reason}
            </Badge>
          {/if}
+
          <NodeId
+
            stylePopoverPositionLeft="-13px"
+
            nodeId={issue.author.id}
+
            alias={issue.author.alias} />
+
          opened
+
          <CopyableId id={issue.id} style="oid">
+
            {utils.formatObjectId(issue.id)}
+
          </CopyableId>
+
          <span title={utils.absoluteTimestamp(issue.discussion[0].timestamp)}>
+
            {utils.formatTimestamp(issue.discussion[0].timestamp)}
+
          </span>
+
          {#if lastDescriptionEdit}
+
            <div
+
              class="author-metadata"
+
              title={utils.formatEditedCaption(
+
                lastDescriptionEdit.author,
+
                lastDescriptionEdit.timestamp,
+
              )}>
+
              • edited
+
            </div>
+
          {/if}
        </svelte:fragment>
+
        <div slot="subtitle" class="global-hide-on-desktop-up">
+
          <div
+
            style:margin-top="2rem"
+
            style="display: flex; flex-direction: column; gap: 0.5rem;">
+
            <AssigneeInput
+
              locallyAuthenticated={role.isDelegate(
+
                session?.publicKey,
+
                delegates,
+
              )}
+
              assignees={issue.assignees}
+
              submitInProgress={assigneeState === "submit"}
+
              on:save={({ detail: newAssignees }) => {
+
                void saveAssignees(newAssignees);
+
              }} />
+
            <LabelInput
+
              locallyAuthenticated={role.isDelegate(
+
                session?.publicKey,
+
                delegates,
+
              )}
+
              labels={issue.labels}
+
              submitInProgress={labelState === "submit"}
+
              on:save={({ detail: newLabels }) => void saveLabels(newLabels)} />
+
            <div class="global-hide-on-mobile-down">
+
              <Embeds embeds={uniqueEmbeds} />
+
            </div>
+
          </div>
+
        </div>
        <svelte:fragment slot="description">
          {#if $experimental && issueState !== "read"}
            <ExtendedTextarea
@@ -589,17 +637,19 @@
          {/if}
          <div class="reactions">
            {#if $experimental && session}
-
              <ReactionSelector
-
                reactions={issue.discussion[0].reactions}
-
                on:select={async ({ detail: { authors, emoji } }) => {
-
                  try {
-
                    if (session) {
-
                      await reactOnComment(session, issue.id, authors, emoji);
+
              <div class="global-hide-on-mobile-down">
+
                <ReactionSelector
+
                  reactions={issue.discussion[0].reactions}
+
                  on:select={async ({ detail: { authors, emoji } }) => {
+
                    try {
+
                      if (session) {
+
                        await reactOnComment(session, issue.id, authors, emoji);
+
                      }
+
                    } finally {
+
                      closeFocused();
                    }
-
                  } finally {
-
                    closeFocused();
-
                  }
-
                }} />
+
                  }} />
+
              </div>
            {/if}
            {#if issue.discussion[0].reactions.length > 0}
              <Reactions
@@ -609,30 +659,6 @@
            {/if}
          </div>
        </svelte:fragment>
-
        <div class="author" slot="author">
-
          <NodeId
-
            stylePopoverPositionLeft="-13px"
-
            nodeId={issue.author.id}
-
            alias={issue.author.alias} />
-
          opened
-
          <CopyableId id={issue.id} style="oid">
-
            {utils.formatObjectId(issue.id)}
-
          </CopyableId>
-
          <span title={utils.absoluteTimestamp(issue.discussion[0].timestamp)}>
-
            {utils.formatTimestamp(issue.discussion[0].timestamp)}
-
          </span>
-
          {#if lastDescriptionEdit}
-
            <div class="author-metadata">•</div>
-
            <div
-
              class="author-metadata"
-
              title={utils.formatEditedCaption(
-
                lastDescriptionEdit.author,
-
                lastDescriptionEdit.timestamp,
-
              )}>
-
              edited
-
            </div>
-
          {/if}
-
        </div>
      </CobHeader>
      <div class="bottom">
        {#if threads.length > 0}
@@ -664,46 +690,40 @@
          </div>
        {/if}
        {#if $experimental && session}
-
          <div class="connector" />
-
          <CommentToggleInput
-
            focus
-
            rawPath={rawPath(project.head)}
-
            placeholder="Leave your comment"
-
            enableAttachments
-
            submit={partial(createComment, session.id)} />
-
          <div
-
            style="display:flex; flex-direction: column; align-items: flex-start;">
-
            {#if role.isDelegateOrAuthor(session.publicKey, delegates, issue.author.id)}
-
              <div class="connector" />
-
              <CobStateButton
-
                items={items.filter(
-
                  ([, state]) => !isEqual(state, issue.state),
-
                )}
-
                {selectedItem}
-
                state={issue.state}
-
                save={partial(saveStatus, session.id)} />
-
            {/if}
+
          <div class="global-hide-on-mobile-down">
+
            <div class="connector" />
+
            <CommentToggleInput
+
              focus
+
              rawPath={rawPath(project.head)}
+
              placeholder="Leave your comment"
+
              enableAttachments
+
              submit={partial(createComment, session.id)} />
+
            <div
+
              style="display:flex; flex-direction: column; align-items: flex-start;">
+
              {#if role.isDelegateOrAuthor(session.publicKey, delegates, issue.author.id)}
+
                <div class="connector" />
+
                <CobStateButton
+
                  items={items.filter(
+
                    ([, state]) => !isEqual(state, issue.state),
+
                  )}
+
                  {selectedItem}
+
                  state={issue.state}
+
                  save={partial(saveStatus, session.id)} />
+
              {/if}
+
            </div>
          </div>
        {/if}
      </div>
    </div>
-
    <div class="metadata global-hide-on-mobile">
+
    <div class="metadata global-hide-on-medium-desktop-down">
      <AssigneeInput
        locallyAuthenticated={Boolean(
          role.isDelegate(session?.publicKey, delegates),
        )}
        assignees={issue.assignees}
        submitInProgress={assigneeState === "submit"}
-
        on:save={async ({ detail: newAssignees }) => {
-
          if (session) {
-
            assigneeState = "submit";
-
            try {
-
              await saveAssignees(session.id, newAssignees);
-
            } finally {
-
              assigneeState = "read";
-
            }
-
          }
-
          await refreshIssue();
+
        on:save={({ detail: newAssignees }) => {
+
          void saveAssignees(newAssignees);
        }} />
      <LabelInput
        locallyAuthenticated={Boolean(
@@ -711,17 +731,7 @@
        )}
        labels={issue.labels}
        submitInProgress={labelState === "submit"}
-
        on:save={async ({ detail: newLabels }) => {
-
          if (session) {
-
            labelState = "submit";
-
            try {
-
              await saveLabels(session.id, newLabels);
-
            } finally {
-
              labelState = "read";
-
            }
-
          }
-
          await refreshIssue();
-
        }} />
+
        on:save={({ detail: newLabels }) => void saveLabels(newLabels)} />
      <Embeds embeds={uniqueEmbeds} />
    </div>
  </div>
modified src/views/projects/Issue/IssueTeaser.svelte
@@ -7,12 +7,14 @@
    formatTimestamp,
  } from "@app/lib/utils";

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

+
  import CommentCounter from "../CommentCounter.svelte";
+
  import Labels from "../Cob/Labels.svelte";
+

  export let baseUrl: BaseUrl;
  export let issue: Issue;
  export let projectId: string;
@@ -23,8 +25,6 @@
    }
    return acc;
  }, 0);
-

-
  let hover = false;
</script>

<style>
@@ -57,25 +57,11 @@
  .issue-title:hover {
    text-decoration: underline;
  }
-
  .comment-count {
-
    display: flex;
-
    flex-direction: row;
-
    align-items: center;
-
    gap: 0.5rem;
-
    height: 1.5rem;
-
  }
-
  .labels {
-
    display: flex;
-
    flex-direction: row;
-
    gap: 0.5rem;
-
    flex-wrap: wrap;
-
  }
  .right {
    display: flex;
-
    gap: 1rem;
    margin-left: auto;
-
    color: var(--color-foreground-dim);
-
    font-size: var(--font-size-tiny);
+
    min-height: 1.5rem;
+
    align-items: center;
  }
  .state {
    justify-self: center;
@@ -91,12 +77,7 @@
  }
</style>

-
<div
-
  role="button"
-
  tabindex="0"
-
  class="issue-teaser"
-
  on:mouseenter={() => (hover = true)}
-
  on:mouseleave={() => (hover = false)}>
+
<div role="button" tabindex="0" class="issue-teaser">
  <div
    class="state"
    class:closed={issue.state.status === "closed"}
@@ -116,32 +97,20 @@
          {#if !issue.title}
            <span class="txt-missing">No title</span>
          {:else}
-
            <InlineMarkdown fontSize="regular" content={issue.title} />
+
            <InlineMarkdown fontSize="regular" content={issue.title}>
+
              {#if issue.labels.length > 0}
+
                <span
+
                  style="display: inline-flex; gap: 0.5rem; flex-wrap: wrap;">
+
                  <Labels labels={issue.labels} />
+
                </span>
+
              {/if}
+
            </InlineMarkdown>
          {/if}
        </span>
      </Link>
-
      <span class="labels">
-
        {#each issue.labels.slice(0, 4) as label}
-
          <Badge variant={hover ? "background" : "neutral"}>
-
            {label}
-
          </Badge>
-
        {/each}
-
        {#if issue.labels.length > 4}
-
          <Badge
-
            title={issue.labels.slice(4, undefined).join(" ")}
-
            variant={hover ? "background" : "neutral"}>
-
            +{issue.labels.length - 4} more labels
-
          </Badge>
-
        {/if}
-
      </span>
      <div class="right">
        {#if commentCount > 0}
-
          <div style:display="flex" style:gap="1rem">
-
            <div class="comment-count">
-
              <IconSmall name="chat" />
-
              <span>{commentCount}</span>
-
            </div>
-
          </div>
+
          <CommentCounter {commentCount} />
        {/if}
      </div>
    </div>
modified src/views/projects/Issues.svelte
@@ -69,7 +69,7 @@

<style>
  .more {
-
    margin: 2rem 0;
+
    margin-top: 2rem;
    min-height: 3rem;
    display: flex;
    align-items: center;
@@ -148,17 +148,19 @@
    <div style="margin-left: auto; display: flex; gap: 1rem;">
      <Share {baseUrl} />
      {#if $experimental && $httpdStore.state === "authenticated" && isLocal(baseUrl.hostname)}
-
        <Link
-
          route={{
-
            resource: "project.newIssue",
-
            project: project.id,
-
            node: baseUrl,
-
          }}>
-
          <Button variant="secondary">
-
            <IconSmall name="plus" />
-
            New Issue
-
          </Button>
-
        </Link>
+
        <div class="global-hide-on-mobile-down">
+
          <Link
+
            route={{
+
              resource: "project.newIssue",
+
              project: project.id,
+
              node: baseUrl,
+
            }}>
+
            <Button variant="secondary">
+
              <IconSmall name="plus" />
+
              New Issue
+
            </Button>
+
          </Link>
+
        </div>
      {/if}
    </div>
  </div>
modified src/views/projects/Layout.svelte
@@ -13,6 +13,7 @@
  export let activeTab: ActiveTab | undefined = undefined;
  export let baseUrl: BaseUrl;
  export let project: Project;
+
  export let stylePaddingBottom: string = "2.5rem";
</script>

<style>
@@ -41,13 +42,10 @@
    display: none;
  }

-
  @media (max-width: 720px) {
+
  @media (max-width: 719.98px) {
    .desktop-header {
      display: none;
    }
-
    .sidebar {
-
      display: none;
-
    }
    .content {
      overflow-y: scroll;
      overflow-x: hidden;
@@ -66,11 +64,15 @@
    <AppHeader />
  </div>

-
  <div class="sidebar">
+
  <div class="sidebar global-hide-on-medium-desktop-down">
    <Sidebar {activeTab} {baseUrl} {project} />
  </div>

-
  <div class="content">
+
  <div class="sidebar global-hide-on-mobile-down global-hide-on-desktop-up">
+
    <Sidebar {activeTab} {baseUrl} {project} collapsedOnly />
+
  </div>
+

+
  <div class="content" style:padding-bottom={stylePaddingBottom}>
    <slot name="header" />
    <slot name="subheader" />
    <slot />
@@ -90,7 +92,7 @@
          <Button
            variant={activeTab === "source" ? "secondary" : "secondary-mobile"}
            styleWidth="100%">
-
            <IconSmall name="home" />
+
            <IconSmall name="chevron-left-right" />
          </Button>
        </Link>
      </div>
modified src/views/projects/Patch.svelte
@@ -34,6 +34,10 @@
  }

  export type Timeline = TimelineMerge | TimelineReview | TimelineThread;
+
  export type PatchReviews = Record<
+
    string,
+
    { latest: boolean; review: Review }
+
  >;
</script>

<script lang="ts">
@@ -64,8 +68,6 @@
  import CommentToggleInput from "@app/components/CommentToggleInput.svelte";
  import CopyableId from "@app/components/CopyableId.svelte";
  import DiffStatBadge from "@app/components/DiffStatBadge.svelte";
-
  import DropdownList from "@app/components/DropdownList.svelte";
-
  import DropdownListItem from "@app/components/DropdownList/DropdownListItem.svelte";
  import Embeds from "@app/views/projects/Cob/Embeds.svelte";
  import ErrorModal from "@app/modals/ErrorModal.svelte";
  import ExtendedTextarea from "@app/components/ExtendedTextarea.svelte";
@@ -77,13 +79,15 @@
  import Markdown from "@app/components/Markdown.svelte";
  import NodeId from "@app/components/NodeId.svelte";
  import Placeholder from "@app/components/Placeholder.svelte";
-
  import Popover, { closeFocused } from "@app/components/Popover.svelte";
  import Radio from "@app/components/Radio.svelte";
  import ReactionSelector from "@app/components/ReactionSelector.svelte";
  import Reactions from "@app/components/Reactions.svelte";
+
  import Reviews from "@app/views/projects/Cob/Reviews.svelte";
  import RevisionComponent from "@app/views/projects/Cob/Revision.svelte";
+
  import RevisionSelector from "@app/views/projects/Patch/RevisionSelector.svelte";
  import Share from "@app/views/projects/Share.svelte";
  import TextInput from "@app/components/TextInput.svelte";
+
  import { closeFocused } from "@app/components/Popover.svelte";

  export let baseUrl: BaseUrl;
  export let patch: Patch;
@@ -411,14 +415,17 @@
    }
  }

-
  async function saveLabels(sessionId: string, labels: string[]) {
+
  async function saveLabels(labels: string[]) {
    try {
-
      await api.project.updatePatch(
-
        project.id,
-
        patch.id,
-
        { type: "label", labels },
-
        sessionId,
-
      );
+
      if (session) {
+
        labelState = "submit";
+
        await api.project.updatePatch(
+
          project.id,
+
          patch.id,
+
          { type: "label", labels },
+
          session.id,
+
        );
+
      }
    } catch (error) {
      if (error instanceof Error) {
        modal.show({
@@ -437,6 +444,7 @@
        });
      }
    } finally {
+
      labelState = "read";
      await refreshPatch();
    }
  }
@@ -550,7 +558,7 @@
  $: description = patch.revisions[0].description;
  $: lastEdit = patch.revisions[0].edits.at(-1);
  $: newDescription = description;
-
  $: patchReviews = computeReviews(patch);
+
  $: reviews = computeReviews(patch);
  $: selectedItem = patch.state.status === "open" ? items[1] : items[0];
  $: timelineTuple = patch.revisions.map<
    [
@@ -623,6 +631,7 @@
    display: flex;
    flex: 1;
    flex-direction: column;
+
    min-width: 0;
    background-color: var(--color-background-float);
  }
  .metadata {
@@ -646,7 +655,7 @@
  }
  .bottom {
    background-color: var(--color-background-default);
-
    padding: 1rem;
+
    padding: 1rem 1rem 0 1rem;
    height: 100%;
  }
  .actions {
@@ -669,39 +678,10 @@
    width: 1rem;
    height: 100%;
  }
-
  .author {
-
    display: flex;
-
    align-items: center;
-
    flex-wrap: nowrap;
-
    font-family: var(--font-family-sans-serif);
-
    font-size: var(--font-size-small);
-
    gap: 0.5rem;
-
  }
-
  .metadata-section-header {
-
    font-size: var(--font-size-small);
-
    margin-bottom: 0.75rem;
-
  }
-
  .metadata-section-body {
-
    display: flex;
-
    flex-direction: column;
-
    gap: 0.5rem;
-
  }
  .author-metadata {
    color: var(--color-fill-gray);
    font-size: var(--font-size-small);
  }
-
  .review {
-
    color: var(--color-fill-gray);
-
    display: inline-flex;
-
    align-items: center;
-
    gap: 0.5rem;
-
  }
-
  .review-accept {
-
    color: var(--color-foreground-success);
-
  }
-
  .review-reject {
-
    color: var(--color-foreground-red);
-
  }
  .revision-description {
    display: flex;
    flex-direction: column;
@@ -718,10 +698,13 @@
    margin-left: 1.25rem;
    background-color: var(--color-fill-separator);
  }
-
  @media (max-width: 720px) {
+
  @media (max-width: 719.98px) {
    .patch {
      display: block;
    }
+
    .bottom {
+
      padding: 1rem 0 0 0;
+
    }
  }
</style>

@@ -749,24 +732,28 @@
            {/if}
          </div>
          {#if session && role.isDelegateOrAuthor(session.publicKey, delegates, patch.author.id) && patchState === "read"}
-
            <Button
-
              variant="outline"
-
              title="edit patch"
-
              on:click={() => (patchState = "edit")}>
-
              <IconSmall name={"edit"} />
-
              Edit
-
            </Button>
+
            <div class="global-hide-on-mobile-down">
+
              <Button
+
                variant="outline"
+
                title="edit patch"
+
                on:click={() => (patchState = "edit")}>
+
                <IconSmall name={"edit"} />
+
                Edit
+
              </Button>
+
            </div>
          {/if}
          {#if patchState === "read"}
            <Share {baseUrl} />
            {#if session && role.isDelegateOrAuthor(session.publicKey, delegates, patch.author.id)}
-
              <CobStateButton
-
                items={items.filter(
-
                  ([, state]) => !isEqual(state, patch.state),
-
                )}
-
                {selectedItem}
-
                state={patch.state}
-
                save={partial(saveStatus, session.id)} />
+
              <div class="global-hide-on-small-desktop-down">
+
                <CobStateButton
+
                  items={items.filter(
+
                    ([, state]) => !isEqual(state, patch.state),
+
                  )}
+
                  {selectedItem}
+
                  state={patch.state}
+
                  save={partial(saveStatus, session.id)} />
+
              </div>
            {/if}
          {/if}
        </svelte:fragment>
@@ -788,7 +775,46 @@
              insertions={stats.insertions}
              deletions={stats.deletions} />
          </Link>
+
          <NodeId
+
            stylePopoverPositionLeft="-13px"
+
            nodeId={patch.author.id}
+
            alias={patch.author.alias} />
+
          opened
+
          <CopyableId id={patch.id} style="oid">
+
            {utils.formatObjectId(patch.id)}
+
          </CopyableId>
+
          <span title={utils.absoluteTimestamp(patch.revisions[0].timestamp)}>
+
            {utils.formatTimestamp(patch.revisions[0].timestamp)}
+
          </span>
+
          {#if patch.revisions[0].edits.length > 1 && lastEdit}
+
            <div
+
              class="author-metadata"
+
              title={utils.formatEditedCaption(
+
                lastEdit.author,
+
                lastEdit.timestamp,
+
              )}>
+
              • edited
+
            </div>
+
          {/if}
        </svelte:fragment>
+
        <div slot="subtitle" class="global-hide-on-desktop-up">
+
          <div
+
            style:margin-top="2rem"
+
            style="display: flex; flex-direction: column; gap: 0.5rem;">
+
            <Reviews {reviews} />
+
            <LabelInput
+
              locallyAuthenticated={role.isDelegate(
+
                session?.publicKey,
+
                delegates,
+
              )}
+
              submitInProgress={labelState === "submit"}
+
              labels={patch.labels}
+
              on:save={({ detail: newLabels }) => {
+
                void saveLabels(newLabels);
+
              }} />
+
            <Embeds embeds={uniqueEmbeds} />
+
          </div>
+
        </div>
        <svelte:fragment slot="description">
          <div class="revision-description">
            {#if $experimental && session && patchState !== "read" && lastEdit}
@@ -832,22 +858,24 @@
            {#if ($experimental && session) || (firstRevision.revisionReactions && firstRevision.revisionReactions.length > 0)}
              <div class="actions">
                {#if session}
-
                  <ReactionSelector
-
                    reactions={firstRevision.revisionReactions}
-
                    on:select={async ({ detail: { emoji, authors } }) => {
-
                      if (session) {
-
                        try {
-
                          await reactOnRevision(
-
                            session,
-
                            patch.id,
-
                            authors,
-
                            emoji,
-
                          );
-
                        } finally {
-
                          closeFocused();
+
                  <div class="global-hide-on-mobile-down">
+
                    <ReactionSelector
+
                      reactions={firstRevision.revisionReactions}
+
                      on:select={async ({ detail: { emoji, authors } }) => {
+
                        if (session) {
+
                          try {
+
                            await reactOnRevision(
+
                              session,
+
                              patch.id,
+
                              authors,
+
                              emoji,
+
                            );
+
                          } finally {
+
                            closeFocused();
+
                          }
                        }
-
                      }
-
                    }} />
+
                      }} />
+
                  </div>
                {/if}
                {#if firstRevision.revisionReactions.length > 0}
                  <Reactions
@@ -859,30 +887,6 @@
            {/if}
          </div>
        </svelte:fragment>
-
        <div class="author" slot="author">
-
          <NodeId
-
            stylePopoverPositionLeft="-13px"
-
            nodeId={patch.author.id}
-
            alias={patch.author.alias} />
-
          opened
-
          <CopyableId id={patch.id} style="oid">
-
            {utils.formatObjectId(patch.id)}
-
          </CopyableId>
-
          <span title={utils.absoluteTimestamp(patch.revisions[0].timestamp)}>
-
            {utils.formatTimestamp(patch.revisions[0].timestamp)}
-
          </span>
-
          {#if patch.revisions[0].edits.length > 1 && lastEdit}
-
            <div class="author-metadata">•</div>
-
            <div
-
              class="author-metadata"
-
              title={utils.formatEditedCaption(
-
                lastEdit.author,
-
                lastEdit.timestamp,
-
              )}>
-
              edited
-
            </div>
-
          {/if}
-
        </div>
      </CobHeader>

      <div class="tabs">
@@ -923,67 +927,27 @@
          {/if}
        </Radio>

-
        <div style="margin-left: auto; margin-top: -0.5rem;">
-
          {#if view.name === "changes"}
-
            <div style="margin-left: auto; ">
-
              <Popover
-
                popoverPadding="0"
-
                popoverPositionTop="2.5rem"
-
                popoverBorderRadius="var(--border-radius-small)">
-
                <Button
-
                  let:expanded
-
                  slot="toggle"
-
                  let:toggle
-
                  on:click={toggle}
-
                  size="regular"
-
                  disabled={patch.revisions.length === 1}>
-
                  <span style:color="var(--color-foreground-contrast)">
-
                    Revision
-
                  </span>
-
                  <span
-
                    style:color="var(--color-fill-secondary)"
-
                    style:font-family="var(--font-family-monospace)">
-
                    {utils.formatObjectId(view.revision)}
-
                  </span>
-
                  <IconSmall name={expanded ? "chevron-up" : "chevron-down"} />
-
                </Button>
-
                <DropdownList slot="popover" items={patch.revisions}>
-
                  <svelte:fragment slot="item" let:item>
-
                    <Link
-
                      on:afterNavigate={closeFocused}
-
                      route={{
-
                        resource: "project.patch",
-
                        project: project.id,
-
                        node: baseUrl,
-
                        patch: patch.id,
-
                        view: {
-
                          name: view.name,
-
                          revision: item.id,
-
                        },
-
                      }}>
-
                      <DropdownListItem selected={item.id === view.revision}>
-
                        <span
-
                          style:color={item.id === view.revision
-
                            ? "var(--color-foreground-contrast)"
-
                            : "var(--color-fill-gray)"}>
-
                          Revision
-
                        </span>
-
                        <span
-
                          style:color="var(--color-fill-secondary)"
-
                          style:font-family="var(--font-family-monospace)">
-
                          {utils.formatObjectId(item.id)}
-
                        </span>
-
                      </DropdownListItem>
-
                    </Link>
-
                  </svelte:fragment>
-
                </DropdownList>
-
              </Popover>
-
            </div>
-
          {/if}
-
        </div>
+
        {#if view.name === "changes"}
+
          <div
+
            class="global-hide-on-mobile-down"
+
            style="margin-left: auto; margin-top: -0.5rem;">
+
            <RevisionSelector {view} {baseUrl} {patch} {project} />
+
          </div>
+
        {/if}
        <div class="tabs-spacer" />
      </div>
      <div class="bottom">
+
        {#if view.name === "changes"}
+
          <div
+
            style:width="100%"
+
            style:padding="0 1rem"
+
            style:display="flex"
+
            class="global-hide-on-small-desktop-up">
+
            <div style:margin-left="auto">
+
              <RevisionSelector {view} {baseUrl} {patch} {project} />
+
            </div>
+
          </div>
+
        {/if}
        {#if view.name === "diff"}
          <Changeset
            {baseUrl}
@@ -1030,29 +994,31 @@
              previousRevOid={previousRevision?.oid}>
              {#if index === patch.revisions.length - 1}
                {#if $experimental && session && view.name === "activity"}
-
                  <div class="connector" />
-
                  <CommentToggleInput
-
                    rawPath={rawPath(patch.revisions[0].id)}
-
                    focus
-
                    enableAttachments
-
                    placeholder="Leave your comment"
-
                    submit={partial(
-
                      createComment,
-
                      session.id,
-
                      revision.revisionId,
-
                    )} />
-
                  {#if role.isDelegateOrAuthor(session.publicKey, delegates, patch.author.id)}
+
                  <div class="global-hide-on-mobile-down">
                    <div class="connector" />
-
                    <div style="display: flex;">
-
                      <CobStateButton
-
                        items={items.filter(
-
                          ([, state]) => !isEqual(state, patch.state),
-
                        )}
-
                        {selectedItem}
-
                        state={patch.state}
-
                        save={partial(saveStatus, session.id)} />
-
                    </div>
-
                  {/if}
+
                    <CommentToggleInput
+
                      rawPath={rawPath(patch.revisions[0].id)}
+
                      focus
+
                      enableAttachments
+
                      placeholder="Leave your comment"
+
                      submit={partial(
+
                        createComment,
+
                        session.id,
+
                        revision.revisionId,
+
                      )} />
+
                    {#if role.isDelegateOrAuthor(session.publicKey, delegates, patch.author.id)}
+
                      <div class="connector" />
+
                      <div style="display: flex;">
+
                        <CobStateButton
+
                          items={items.filter(
+
                            ([, state]) => !isEqual(state, patch.state),
+
                          )}
+
                          {selectedItem}
+
                          state={patch.state}
+
                          save={partial(saveStatus, session.id)} />
+
                      </div>
+
                    {/if}
+
                  </div>
                {/if}
              {/if}
            </RevisionComponent>
@@ -1076,43 +1042,14 @@
      </div>
    </div>

-
    <div class="metadata global-hide-on-mobile">
-
      <div>
-
        <div class="metadata-section-header">Reviews</div>
-
        <div class="metadata-section-body">
-
          {#each Object.values(patchReviews) as { latest, review }}
-
            <div class="review" class:txt-missing={!latest}>
-
              <span
-
                class:review-accept={review.verdict === "accept"}
-
                class:review-reject={review.verdict === "reject"}>
-
                {#if review.verdict === "accept"}
-
                  <IconSmall name="checkmark" />
-
                {:else if review.verdict === "reject"}
-
                  <IconSmall name="cross" />
-
                {:else}
-
                  <IconSmall name="chat" />
-
                {/if}
-
              </span>
-
              <NodeId nodeId={review.author.id} alias={review.author.alias} />
-
            </div>
-
          {:else}
-
            <div class="txt-missing">No reviews</div>
-
          {/each}
-
        </div>
-
      </div>
+
    <div class="metadata global-hide-on-medium-desktop-down">
+
      <Reviews {reviews} />
      <LabelInput
        locallyAuthenticated={role.isDelegate(session?.publicKey, delegates)}
        submitInProgress={labelState === "submit"}
        labels={patch.labels}
-
        on:save={async ({ detail: newLabels }) => {
-
          if (session) {
-
            labelState = "submit";
-
            try {
-
              await saveLabels(session.id, newLabels);
-
            } finally {
-
              labelState = "read";
-
            }
-
          }
+
        on:save={({ detail: newLabels }) => {
+
          void saveLabels(newLabels);
        }} />
      <Embeds embeds={uniqueEmbeds} />
    </div>
modified src/views/projects/Patch/PatchTeaser.svelte
@@ -2,42 +2,32 @@
  import type { BaseUrl } from "@httpd-client";
  import type { Patch } from "@httpd-client";

-
  import { HttpdClient } from "@httpd-client";
  import {
    absoluteTimestamp,
    formatObjectId,
    formatTimestamp,
-
    formatCommit,
  } from "@app/lib/utils";

-
  import Badge from "@app/components/Badge.svelte";
-
  import DiffStatBadge from "@app/components/DiffStatBadge.svelte";
  import IconSmall from "@app/components/IconSmall.svelte";
  import InlineMarkdown from "@app/components/InlineMarkdown.svelte";
  import Link from "@app/components/Link.svelte";
  import NodeId from "@app/components/NodeId.svelte";
-
  import Loading from "@app/components/Loading.svelte";
+

+
  import Labels from "../Cob/Labels.svelte";
+
  import DiffStatBadgeLoader from "../DiffStatBadgeLoader.svelte";
+
  import CommentCounter from "../CommentCounter.svelte";

  export let projectId: string;
  export let baseUrl: BaseUrl;
  export let patch: Patch;

-
  const api = new HttpdClient(baseUrl);
-

  const latestRevisionIndex = patch.revisions.length - 1;
  const latestRevision = patch.revisions[latestRevisionIndex];
-
  $: diffPromise = api.project.getDiff(
-
    projectId,
-
    latestRevision.base,
-
    latestRevision.oid,
-
  );

-
  // Counts the amount of comments in all the discussions over all revisions.
  $: commentCount = patch.revisions.reduce(
    (acc, curr) => acc + curr.discussions.reduce(acc => acc + 1, 0),
    0,
  );
-
  let hover = false;
</script>

<style>
@@ -71,10 +61,9 @@
    text-decoration: underline;
  }
  .right {
+
    margin-left: auto;
    display: flex;
    align-items: flex-start;
-
    gap: 1rem;
-
    margin-left: auto;
  }
  .state {
    justify-self: center;
@@ -82,22 +71,6 @@
    margin-right: 0.5rem;
    padding: 0.25rem 0;
  }
-
  .labels {
-
    display: flex;
-
    flex-direction: row;
-
    gap: 0.5rem;
-
  }
-
  .comments {
-
    color: var(--color-foreground-dim);
-
    font-size: var(--font-size-tiny);
-
    display: flex;
-
    align-items: center;
-
    gap: 0.25rem;
-
  }
-
  .label {
-
    overflow: hidden;
-
    text-overflow: ellipsis;
-
  }
  .draft {
    color: var(--color-foreground-dim);
  }
@@ -110,14 +83,22 @@
  .merged {
    color: var(--color-fill-primary);
  }
+
  .diff-comment {
+
    display: flex;
+
    flex-direction: row;
+
    gap: 0.5rem;
+
    margin-left: 0.5rem;
+
    min-height: 1.5rem;
+
  }
+
  @media (max-width: 719.98px) {
+
    .diff-comment {
+
      flex-direction: column-reverse;
+
      align-items: flex-end;
+
    }
+
  }
</style>

-
<div
-
  role="button"
-
  tabindex="0"
-
  class="patch-teaser"
-
  on:mouseenter={() => (hover = true)}
-
  on:mouseleave={() => (hover = false)}>
+
<div role="button" tabindex="0" class="patch-teaser">
  <div
    class="state"
    class:draft={patch.state.status === "draft"}
@@ -136,25 +117,15 @@
          patch: patch.id,
        }}>
        <span class="patch-title">
-
          <InlineMarkdown fontSize="regular" content={patch.title} />
+
          <InlineMarkdown fontSize="regular" content={patch.title}>
+
            {#if patch.labels.length > 0}
+
              <span style="display: inline-flex; gap: 0.5rem; flex-wrap: wrap;">
+
                <Labels labels={patch.labels} />
+
              </span>
+
            {/if}
+
          </InlineMarkdown>
        </span>
      </Link>
-
      <span class="labels">
-
        {#each patch.labels.slice(0, 4) as label}
-
          <Badge
-
            style="max-width:7rem"
-
            variant={hover ? "background" : "neutral"}>
-
            <span class="label">{label}</span>
-
          </Badge>
-
        {/each}
-
        {#if patch.labels.length > 4}
-
          <Badge
-
            title={patch.labels.slice(4, undefined).join(" ")}
-
            variant={hover ? "background" : "neutral"}>
-
            <span class="label">+{patch.labels.length - 4} more labels</span>
-
          </Badge>
-
        {/if}
-
      </span>
    </div>
    <div class="summary">
      <span class="subtitle">
@@ -165,8 +136,10 @@
        {patch.revisions.length > 1 ? "updated" : "opened"}
        <span class="global-oid">{formatObjectId(patch.id)}</span>
        {#if patch.revisions.length > 1}
-
          to <span class="global-oid">
-
            {formatObjectId(patch.revisions[patch.revisions.length - 1].id)}
+
          <span class="global-hide-on-mobile-down">
+
            to <span class="global-oid">
+
              {formatObjectId(patch.revisions[patch.revisions.length - 1].id)}
+
            </span>
          </span>
        {/if}
        <span title={absoluteTimestamp(latestRevision.timestamp)}>
@@ -176,37 +149,11 @@
    </div>
  </div>
  <div class="right">
-
    <div style:display="flex" style:gap="1rem">
+
    <div class="diff-comment">
      {#if commentCount > 0}
-
        <div class="comments">
-
          <IconSmall name="chat" />
-
          <span>{commentCount}</span>
-
        </div>
+
        <CommentCounter {commentCount} />
      {/if}
-
      {#await diffPromise}
-
        <Loading small />
-
      {:then { diff }}
-
        <Link
-
          title="Compare {formatCommit(latestRevision.base)}..{formatCommit(
-
            latestRevision.oid,
-
          )}"
-
          route={{
-
            resource: "project.patch",
-
            project: projectId,
-
            node: baseUrl,
-
            patch: patch.id,
-
            view: {
-
              name: "diff",
-
              fromCommit: latestRevision.base,
-
              toCommit: latestRevision.oid,
-
            },
-
          }}>
-
          <DiffStatBadge
-
            hoverable
-
            insertions={diff.stats.insertions}
-
            deletions={diff.stats.deletions} />
-
        </Link>
-
      {/await}
+
      <DiffStatBadgeLoader {projectId} {baseUrl} {patch} {latestRevision} />
    </div>
  </div>
</div>
added src/views/projects/Patch/RevisionSelector.svelte
@@ -0,0 +1,69 @@
+
<script lang="ts">
+
  import type { PatchView } from "../router";
+
  import type { BaseUrl, Patch, Project } from "@httpd-client";
+
  import * as utils from "@app/lib/utils";
+

+
  import Button from "@app/components/Button.svelte";
+
  import DropdownList from "@app/components/DropdownList.svelte";
+
  import DropdownListItem from "@app/components/DropdownList/DropdownListItem.svelte";
+
  import IconSmall from "@app/components/IconSmall.svelte";
+
  import Link from "@app/components/Link.svelte";
+
  import Popover from "@app/components/Popover.svelte";
+
  import { closeFocused } from "@app/components/Popover.svelte";
+

+
  export let view: Extract<PatchView, { name: "changes" }>;
+
  export let baseUrl: BaseUrl;
+
  export let patch: Patch;
+
  export let project: Project;
+
</script>
+

+
<Popover
+
  popoverPadding="0"
+
  popoverPositionTop="3rem"
+
  popoverBorderRadius="var(--border-radius-small)">
+
  <Button
+
    let:expanded
+
    slot="toggle"
+
    let:toggle
+
    on:click={toggle}
+
    size="regular"
+
    disabled={patch.revisions.length === 1}>
+
    <span style:color="var(--color-foreground-contrast)">Revision</span>
+
    <span
+
      style:color="var(--color-fill-secondary)"
+
      style:font-family="var(--font-family-monospace)">
+
      {utils.formatObjectId(view.revision)}
+
    </span>
+
    <IconSmall name={expanded ? "chevron-up" : "chevron-down"} />
+
  </Button>
+
  <DropdownList slot="popover" items={patch.revisions}>
+
    <svelte:fragment slot="item" let:item>
+
      <Link
+
        on:afterNavigate={closeFocused}
+
        route={{
+
          resource: "project.patch",
+
          project: project.id,
+
          node: baseUrl,
+
          patch: patch.id,
+
          view: {
+
            name: view.name,
+
            revision: item.id,
+
          },
+
        }}>
+
        <DropdownListItem selected={item.id === view.revision}>
+
          <span
+
            style:color={item.id === view.revision
+
              ? "var(--color-foreground-contrast)"
+
              : "var(--color-fill-gray)"}>
+
            Revision
+
          </span>
+
          <span
+
            style:color="var(--color-fill-secondary)"
+
            style:font-family="var(--font-family-monospace)">
+
            {utils.formatObjectId(item.id)}
+
          </span>
+
        </DropdownListItem>
+
      </Link>
+
    </svelte:fragment>
+
  </DropdownList>
+
</Popover>
modified src/views/projects/Patches.svelte
@@ -78,7 +78,7 @@

<style>
  .more {
-
    margin: 2rem 0;
+
    margin-top: 2rem;
    min-height: 3rem;
    display: flex;
    align-items: center;
@@ -162,22 +162,24 @@
    <div style="margin-left: auto; display: flex; gap: 1rem;">
      <Share {baseUrl} />
      {#if $experimental && $httpdStore.state === "authenticated" && isLocal(baseUrl.hostname)}
-
        <Popover popoverPositionTop="2.5rem" popoverPositionRight="0">
-
          <Button
-
            slot="toggle"
-
            let:toggle
-
            on:click={toggle}
-
            variant="secondary">
-
            <IconSmall name="plus" />
-
            New Patch
-
          </Button>
-

-
          <div slot="popover" class="popover txt-small">
-
            To create a patch, first checkout a new branch and commit your
-
            changes, then run the following command.
-
            <Command command="git push rad HEAD:refs/patches" />
-
          </div>
-
        </Popover>
+
        <div class="global-hide-on-mobile-down">
+
          <Popover popoverPositionTop="2.5rem" popoverPositionRight="0">
+
            <Button
+
              slot="toggle"
+
              let:toggle
+
              on:click={toggle}
+
              variant="secondary">
+
              <IconSmall name="plus" />
+
              New Patch
+
            </Button>
+

+
            <div slot="popover" class="popover txt-small">
+
              To create a patch, first checkout a new branch and commit your
+
              changes, then run the following command.
+
              <Command command="git push rad HEAD:refs/patches" />
+
            </div>
+
          </Popover>
+
        </div>
      {/if}
    </div>
  </div>
modified src/views/projects/Share.svelte
@@ -54,13 +54,15 @@
      let:toggle
      on:click={toggle}>
      <IconSmall name="link" />
-
      {caption}
+
      <span class="global-hide-on-small-desktop-down">
+
        {caption}
+
      </span>
    </Button>
    <ShareButton slot="popover" />
  </Popover>
{:else}
  <Button variant="outline" size="regular" on:click={copy}>
    <IconSmall name={shareIcon} />
-
    Copy link
+
    <span class="global-hide-on-small-desktop-down">Copy link</span>
  </Button>
{/if}
modified src/views/projects/Sidebar.svelte
@@ -23,8 +23,9 @@
  export let activeTab: ActiveTab | undefined = undefined;
  export let baseUrl: BaseUrl;
  export let project: Project;
+
  export let collapsedOnly = false;

-
  let expanded = loadSidebarState();
+
  let expanded = collapsedOnly ? false : loadSidebarState();

  export function storeSidebarState(expanded: boolean): void {
    window.localStorage.setItem(
@@ -338,40 +339,45 @@
        {/if}
      {/if}
    </div>
-
    <div class="sidebar-footer" style:flex-direction="row">
-
      <Button title={"Collapse"} on:click={toggleSidebar} variant="background">
-
        <div class="icon" class:expanded>
-
          <IconSmall name="chevron-left" />
-
        </div>
-
      </Button>
-
      <div style:width="1.5rem" />
-
      <div class="horizontal-buttons" class:expanded>
-
        <Popover popoverPositionBottom="2.5rem" popoverPositionLeft="0">
-
          <Button
-
            variant="background"
-
            title="Settings"
-
            slot="toggle"
-
            let:toggle
-
            on:click={toggle}>
-
            <IconSmall name="settings" />
-
            Settings
-
          </Button>
+
    {#if !collapsedOnly}
+
      <div class="sidebar-footer" style:flex-direction="row">
+
        <Button
+
          title={"Collapse"}
+
          on:click={toggleSidebar}
+
          variant="background">
+
          <div class="icon" class:expanded>
+
            <IconSmall name="chevron-left" />
+
          </div>
+
        </Button>
+
        <div style:width="1.5rem" />
+
        <div class="horizontal-buttons" class:expanded>
+
          <Popover popoverPositionBottom="2.5rem" popoverPositionLeft="0">
+
            <Button
+
              variant="background"
+
              title="Settings"
+
              slot="toggle"
+
              let:toggle
+
              on:click={toggle}>
+
              <IconSmall name="settings" />
+
              Settings
+
            </Button>

-
          <Settings slot="popover" />
-
        </Popover>
-
        <Popover popoverPositionBottom="2.5rem" popoverPositionLeft="0">
-
          <Button
-
            variant="background"
-
            title="Help"
-
            slot="toggle"
-
            let:toggle
-
            on:click={toggle}>
-
            <IconSmall name="help" />
-
            Help
-
          </Button>
-
          <Help slot="popover" />
-
        </Popover>
+
            <Settings slot="popover" />
+
          </Popover>
+
          <Popover popoverPositionBottom="2.5rem" popoverPositionLeft="0">
+
            <Button
+
              variant="background"
+
              title="Help"
+
              slot="toggle"
+
              let:toggle
+
              on:click={toggle}>
+
              <IconSmall name="help" />
+
              Help
+
            </Button>
+
            <Help slot="popover" />
+
          </Popover>
+
        </div>
      </div>
-
    </div>
+
    {/if}
  </div>
</div>
modified src/views/projects/Source.svelte
@@ -99,6 +99,7 @@
    display: flex;
    flex-direction: column;
    width: 100%;
+
    padding-bottom: 2.5rem;
    /* To allow pre elements to shrink when overflowing */
    min-width: 0;
  }
@@ -119,9 +120,16 @@
    top: 0rem;
    max-height: calc(100vh - 5.5rem);
  }
+
  @media (max-width: 719.98px) {
+
    .container {
+
      display: flex;
+
      width: inherit;
+
      padding: 0;
+
    }
+
  }
</style>

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

  <div style:margin="1rem 0 1rem 1rem" slot="subheader">
@@ -136,7 +144,7 @@
      filesLinkActive={true}
      historyLinkActive={false} />
  </div>
-
  <div class="global-hide-on-desktop">
+
  <div class="global-hide-on-medium-desktop-up">
    {#if tree.entries.length > 0}
      <div style:margin="1rem">
        <Button
@@ -170,7 +178,7 @@

  <div class="container center-content">
    {#if tree.entries.length > 0}
-
      <div class="column-left global-hide-on-mobile">
+
      <div class="column-left global-hide-on-small-desktop-down">
        <div class="source-tree sticky">
          <TreeComponent
            projectId={project.id}
modified src/views/projects/Source/Blob.svelte
@@ -137,13 +137,21 @@
  .no-scrollbar::-webkit-scrollbar {
    display: none;
  }
+
  .markdown-wrapper {
+
    padding: 2rem;
+
  }
+
  @media (max-width: 719.98px) {
+
    .markdown-wrapper {
+
      padding: 1rem;
+
    }
+
  }
</style>

<File sticky={false}>
  <FilePath slot="left-header" filenameWithPath={blob.path} />
  <svelte:fragment slot="right-header">
    <CommitButton styleRoundBorders {projectId} {baseUrl} commit={lastCommit} />
-
    <div class="global-hide-on-mobile teaser-buttons">
+
    <div class="global-hide-on-mobile-down teaser-buttons">
      {#if enablePreview}
        <Radio ariaLabel="Toggle render method">
          <Button
@@ -188,7 +196,7 @@
    {/if}
  {:else if preview && blob.content}
    {#if isMarkdown}
-
      <div style:padding="2rem">
+
      <div class="markdown-wrapper">
        <Markdown content={blob.content} {rawPath} {path} />
      </div>
    {:else if isSvg}
modified src/views/projects/Source/ProjectNameHeader.svelte
@@ -75,15 +75,18 @@
        Private
      </Badge>
    {/if}
-
    <div
-
      class="global-hide-on-mobile"
-
      style="margin-left: auto; display: flex; gap: 0.5rem;">
+
    <div style="margin-left: auto; display: flex; gap: 0.5rem;">
      <Share {baseUrl} />
-
      <CloneButton {baseUrl} id={project.id} name={project.name} />
-
      <SeedButton
-
        {seeding}
-
        seedCount={project.seeding}
-
        projectId={project.id} />
+
      <div
+
        style:display="flex"
+
        style:gap="0.5rem"
+
        class="global-hide-on-mobile-down">
+
        <CloneButton {baseUrl} id={project.id} name={project.name} />
+
        <SeedButton
+
          {seeding}
+
          seedCount={project.seeding}
+
          projectId={project.id} />
+
      </div>
    </div>
  </div>
  <div class="id">
modified src/views/projects/components/CommitButton.svelte
@@ -43,7 +43,9 @@
      <div class="identifier global-commit">
        {commitShortId}
      </div>
-
      <span>{commit.summary}</span>
+
      <span class="global-hide-on-small-desktop-down">
+
        {commit.summary}
+
      </span>
    </div>
  </Button>
</Link>
modified tests/e2e/project.spec.ts
@@ -397,7 +397,7 @@ test("only one modal can be open at a time", async ({ page }) => {
  await expect(page.getByText("bob")).not.toBeVisible();
  await expect(page.getByText("feature/branch")).not.toBeVisible();

-
  await page.getByText("Settings").click();
+
  await page.getByRole("button", { name: "Settings" }).click();
  await expect(page.getByText("Code font")).toBeVisible();
  await expect(page.getByText("Use the Radicle CLI")).not.toBeVisible();
  await expect(page.getByText("bob")).not.toBeVisible();
modified tests/e2e/project/issue.spec.ts
@@ -180,7 +180,7 @@ test("handling embeds", async ({ page, authenticatedPeer }) => {
    `${scheme}://${hostname}:${port}/raw/rad:z2J7s48EbCBckcEmj2dm5eaFVoBsy/blobs/bae036309c2182c7304c97956969369823b5c6ad?mime=image/png`,
  );
  await expect(
-
    page.locator(".badge").filter({ hasText: "radicle-228x228.png" }),
+
    page.getByRole("button", { name: "radicle-228x228.png" }),
  ).toBeVisible();

  await page.getByRole("button", { name: "Edit" }).click();
@@ -201,9 +201,9 @@ test("handling embeds", async ({ page, authenticatedPeer }) => {
  await page.getByRole("button", { name: "Save" }).click();
  await expect(page.getByRole("button", { name: "Submit" })).toBeHidden();
  await expect(
-
    page.locator(".badge").filter({ hasText: "apple-touch-icon.png" }),
+
    page.getByRole("button", { name: "apple-touch-icon.png" }),
  ).toBeVisible();
  await expect(
-
    page.locator(".badge").filter({ hasText: "radicle-228x228.png" }),
+
    page.getByRole("button", { name: "radicle-228x228.png" }),
  ).toBeVisible();
});
modified tests/e2e/project/issues.spec.ts
@@ -88,12 +88,12 @@ test("create a new issue", async ({ page, authenticatedPeer }) => {
  await expect(page.getByText("This is a title")).toBeVisible();
  await expect(page.getByText("This is a description")).toBeVisible();
  await expect(
-
    page.getByText(
-
      `did:key:${authenticatedPeer.nodeId.substring(
+
    page.getByRole("button", {
+
      name: `did:key:${authenticatedPeer.nodeId.substring(
        0,
        6,
      )}…${authenticatedPeer.nodeId.slice(-6)}`,
-
    ),
+
    }),
  ).toBeVisible();
  await expect(
    page.getByRole("button", { name: "documentation" }),
modified tests/e2e/project/threads.spec.ts
@@ -134,7 +134,7 @@ test("handling embeds", async ({ page, authenticatedPeer }) => {
    `${scheme}://${hostname}:${port}/raw/rad:z2J7s48EbCBckcEmj2dm5eaFVoBsy/blobs/bae036309c2182c7304c97956969369823b5c6ad?mime=image/png`,
  );
  await expect(
-
    page.locator(".badge").filter({ hasText: "radicle-228x228.png" }),
+
    page.getByRole("button", { name: "radicle-228x228.png" }),
  ).toBeVisible();

  await page.getByRole("button", { name: "edit comment" }).click();
@@ -155,9 +155,9 @@ test("handling embeds", async ({ page, authenticatedPeer }) => {
  await page.getByRole("button", { name: "Save" }).click();
  await expect(page.getByRole("button", { name: "Save" })).toBeHidden();
  await expect(
-
    page.locator(".badge").filter({ hasText: "apple-touch-icon.png" }),
+
    page.getByRole("button", { name: "apple-touch-icon.png" }),
  ).toBeVisible();
  await expect(
-
    page.locator(".badge").filter({ hasText: "radicle-228x228.png" }),
+
    page.getByRole("button", { name: "radicle-228x228.png" }),
  ).toBeVisible();
});
modified tests/e2e/theme.spec.ts
@@ -19,7 +19,7 @@ test("theme persistance", async ({ page }) => {
  await expect(
    page.getByRole("banner").getByRole("link", { name: "source-browsing" }),
  ).toBeVisible();
-
  await page.getByText("Settings").click();
+
  await page.getByRole("button", { name: "Settings" }).click();

  await page.getByText("System").click();
  await page.getByRole("button", { name: "Light Mode" }).click();
@@ -38,7 +38,7 @@ test("change theme", async ({ page }) => {
  await expect(
    page.getByRole("banner").getByRole("link", { name: "source-browsing" }),
  ).toBeVisible();
-
  await page.getByText("Settings").click();
+
  await page.getByRole("button", { name: "Settings" }).click();

  await page.getByRole("button", { name: "Light Mode" }).click();
  await expect(page.locator("html")).toHaveAttribute("data-theme", "light");
@@ -65,7 +65,7 @@ test("change code font", async ({ page }) => {
    page.getByRole("banner").getByRole("link", { name: "source-browsing" }),
  ).toBeVisible();

-
  await page.getByText("Settings").click();
+
  await page.getByRole("button", { name: "Settings" }).click();

  await page.getByText("System").click();
  await expect(page.getByText("System")).toHaveClass(/selected/);