Radish alpha
r
rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5
Radicle web interface
Radicle
Git
Collection of small UI tweaks around the new responsive layout
Merged rudolfs opened 1 year ago
29 files changed +334 -222 0e059bf3 ce8d6f6d
modified src/App/Footer.svelte
@@ -13,9 +13,9 @@
    justify-content: space-between;
    font-size: var(--font-size-small);
    color: var(--color-foreground-dim);
-
    height: 2.25rem;
+
    height: 2.3125rem;
    background-color: var(--color-background-dip);
-
    padding: 1rem;
+
    padding: 0 1rem;
  }

  .left {
modified src/App/Header.svelte
@@ -63,7 +63,7 @@
  <div class="right">
    {#if $experimental}
      {#if $httpdStore.state === "stopped"}
-
        <Popover popoverPositionTop="3rem" popoverPositionRight="0">
+
        <Popover popoverPositionTop="2.5rem" popoverPositionRight="0">
          <Button
            slot="toggle"
            let:toggle
modified src/App/Header/Authenticate.svelte
@@ -68,7 +68,7 @@
    </div>
  </Popover>
{:else}
-
  <Popover popoverPositionTop="3rem" popoverPositionRight="0">
+
  <Popover popoverPositionTop="2.5rem" popoverPositionRight="0">
    <Button slot="toggle" let:toggle on:click={toggle} variant="naked-toggle">
      <IconSmall name="key" />
      Authenticate
modified src/App/Header/Breadcrumbs.svelte
@@ -13,7 +13,8 @@
  .breadcrumbs {
    display: flex;
    align-items: center;
-
    gap: 0.25rem;
+
    column-gap: 0.25rem;
+
    line-height: 1rem;
    font-weight: var(--font-weight-semibold);
    font-size: var(--font-size-small);
    white-space: nowrap;
modified src/App/Header/Breadcrumbs/ProjectSegment.svelte
@@ -1,7 +1,7 @@
<script lang="ts">
  import type { ProjectLoadedRoute } from "@app/views/projects/router";

-
  import { unreachable } from "@app/lib/utils";
+
  import { formatObjectId, unreachable } from "@app/lib/utils";

  import FilePath from "@app/components/FilePath.svelte";
  import IconSmall from "@app/components/IconSmall.svelte";
@@ -97,16 +97,31 @@
{#if activeRoute.resource === "project.commit"}
  <Separator />
  <span class="id">
-
    {activeRoute.params.commit.commit.id}
+
    <div class="global-hide-on-small-desktop-down">
+
      {activeRoute.params.commit.commit.id}
+
    </div>
+
    <div class="global-hide-on-medium-desktop-up">
+
      {formatObjectId(activeRoute.params.commit.commit.id)}
+
    </div>
  </span>
{:else if activeRoute.resource === "project.issue"}
  <Separator />
  <span class="id">
-
    {activeRoute.params.issue.id}
+
    <div class="global-hide-on-small-desktop-down">
+
      {activeRoute.params.issue.id}
+
    </div>
+
    <div class="global-hide-on-medium-desktop-up">
+
      {formatObjectId(activeRoute.params.issue.id)}
+
    </div>
  </span>
{:else if activeRoute.resource === "project.patch"}
  <Separator />
  <span class="id">
-
    {activeRoute.params.patch.id}
+
    <div class="global-hide-on-small-desktop-down">
+
      {activeRoute.params.patch.id}
+
    </div>
+
    <div class="global-hide-on-medium-desktop-up">
+
      {formatObjectId(activeRoute.params.patch.id)}
+
    </div>
  </span>
{/if}
modified src/App/Settings.svelte
@@ -109,7 +109,7 @@
      </Radio>
    </div>
  </div>
-
  <div class="item">
+
  <div class="item global-hide-on-mobile-down">
    <div>Radicle HTTP Daemon Port</div>
    <div class="right txt-monospace" style:width="6rem">
      <TextInput
modified src/components/Badge.svelte
@@ -29,8 +29,6 @@
    white-space: nowrap;
    align-items: center;
    gap: 0.25rem;
-
    /* This is aprox. a DID assignee */
-
    max-width: 16rem;
  }
  .background {
    color: currentColor;
modified src/components/Comment.svelte
@@ -197,7 +197,7 @@
            state = "read";
          }} />
      {:else}
-
        <div style:overflow="hidden">
+
        <div style:overflow="hidden" style:width="100%">
          <Markdown breaks {rawPath} content={body} />
        </div>
      {/if}
modified src/components/InlineMarkdown.svelte
@@ -42,5 +42,4 @@
  class:txt-small={fontSize === "small"}
  class:txt-tiny={fontSize === "tiny"}>
  {@html render(content)}
-
  <slot />
</span>
modified src/components/Link.svelte
@@ -6,6 +6,7 @@

  export let route: Route;
  export let disabled: boolean = false;
+
  export let styleHoverState: boolean = false;
  export let title: string | undefined = undefined;
  export let style: string | undefined = undefined;

@@ -29,6 +30,19 @@
  }
</script>

-
<a on:click={navigateToRoute} href={routeToPath(route)} {title} {style}>
+
<style>
+
  .hover-style:hover {
+
    text-decoration: underline;
+
    text-decoration-thickness: 1px;
+
    text-underline-offset: 2px;
+
  }
+
</style>
+

+
<a
+
  class:hover-style={styleHoverState}
+
  on:click={navigateToRoute}
+
  href={routeToPath(route)}
+
  {title}
+
  {style}>
  <slot />
</a>
modified src/components/ReactionSelector.svelte
@@ -41,7 +41,7 @@

<div>
  <Popover
-
    popoverPositionBottom="2rem"
+
    popoverPositionBottom="2.5rem"
    popoverPositionLeft="0"
    popoverPadding="0">
    <IconButton
modified src/views/home/components/HomepageSection.svelte
@@ -32,7 +32,6 @@
    align-items: center;
    margin-top: 0.25rem;
    color: var(--color-foreground-dim);
-
    white-space: nowrap;
  }

  .actions {
modified src/views/projects/Changeset.svelte
@@ -45,9 +45,11 @@
  }
  .additions {
    color: var(--color-foreground-success);
+
    white-space: nowrap;
  }
  .deletions {
    color: var(--color-foreground-red);
+
    white-space: nowrap;
  }
  .diff-list {
    display: flex;
@@ -63,9 +65,6 @@
    .diff-list {
      padding: 1rem 0;
    }
-
    .header {
-
      align-items: flex-start;
-
    }
  }
</style>

@@ -73,7 +72,6 @@
  <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)}
modified src/views/projects/Cob/AssigneeInput.svelte
@@ -97,7 +97,12 @@
    position: relative;
    margin-top: 0.5rem;
  }
-

+
  .input {
+
    width: 100%;
+
    display: flex;
+
    align-items: center;
+
    gap: 0.5rem;
+
  }
  @media (max-width: 1349.98px) {
    .wrapper {
      display: flex;
@@ -111,11 +116,17 @@
      display: flex;
      align-items: center;
    }
+
    .body {
+
      align-items: flex-start;
+
    }
    .no-assignees {
      height: 2rem;
      display: flex;
      align-items: center;
    }
+
    .input {
+
      width: 18rem;
+
    }
  }
</style>

@@ -127,12 +138,12 @@
        <Badge
          variant="neutral"
          size="small"
-
          style="cursor: pointer;"
+
          style="cursor: pointer; max-width: 14rem;"
          on:click={() =>
            (removeToggles[assignee.id] = !removeToggles[assignee.id])}>
          <div class="assignee">
            <Avatar inline nodeId={assignee.id} />
-
            <span>{formatNodeId(assignee.id)}</span>
+
            <span class="txt-overflow">{formatNodeId(assignee.id)}</span>
            {#if removeToggles[assignee.id]}
              <IconButton title="remove assignee">
                <IconSmall
@@ -144,32 +155,33 @@
        </Badge>
      {/each}
      {#if showInput}
-
        <div
-
          style="width:100%; display: flex; align-items: center; gap: 0.5rem;">
-
          <TextInput
-
            autofocus
-
            disabled={submitInProgress}
-
            bind:value={inputValue}
-
            placeholder="Add assignee"
-
            on:submit={addAssignee} />
-
          <IconButton
-
            title="discard assignee"
-
            on:click={() => {
-
              inputValue = "";
-
              validationMessage = undefined;
-
              showInput = false;
-
            }}>
-
            <IconSmall name="cross" />
-
          </IconButton>
-
          <IconButton title="save assignee" on:click={addAssignee}>
-
            <IconSmall name="checkmark" />
-
          </IconButton>
-
        </div>
-
        {#if validationMessage}
-
          <div class="validation-message">
-
            <IconSmall name="exclamation-circle" />{validationMessage}
+
        <div>
+
          <div class="input">
+
            <TextInput
+
              autofocus
+
              disabled={submitInProgress}
+
              bind:value={inputValue}
+
              placeholder="Add assignee"
+
              on:submit={addAssignee} />
+
            <IconButton
+
              title="discard assignee"
+
              on:click={() => {
+
                inputValue = "";
+
                validationMessage = undefined;
+
                showInput = false;
+
              }}>
+
              <IconSmall name="cross" />
+
            </IconButton>
+
            <IconButton title="save assignee" on:click={addAssignee}>
+
              <IconSmall name="checkmark" />
+
            </IconButton>
          </div>
-
        {/if}
+
          {#if validationMessage}
+
            <div class="validation-message">
+
              <IconSmall name="exclamation-circle" />{validationMessage}
+
            </div>
+
          {/if}
+
        </div>
      {:else}
        <div class="global-hide-on-mobile-down">
          <Badge
modified src/views/projects/Cob/Embeds.svelte
@@ -25,10 +25,16 @@
      display: flex;
      flex-direction: row;
      gap: 1rem;
-
      align-items: flex-start;
+
      align-items: flex;
    }
    .header {
      margin-bottom: 0;
+
      height: 2rem;
+
      display: flex;
+
      align-items: center;
+
    }
+
    .no-attachments {
+
      height: 2rem;
      display: flex;
      align-items: center;
    }
@@ -39,12 +45,12 @@
  <div class="header">Attachments</div>
  <div class="body">
    {#each embeds as embed}
-
      <Badge variant="neutral">
+
      <Badge variant="neutral" size="small" style="max-width: 14rem;">
        <span class="txt-overflow">{embed.name}</span>
        <Clipboard text={`![${embed.name}](${embed.content.substring(4)})`} />
      </Badge>
    {:else}
-
      <div class="txt-missing">No attachments</div>
+
      <div class="txt-missing no-attachments">No attachments</div>
    {/each}
  </div>
</div>
modified src/views/projects/Cob/LabelInput.svelte
@@ -77,6 +77,12 @@
    position: relative;
    margin-top: 0.5rem;
  }
+
  .input {
+
    width: 100%;
+
    display: flex;
+
    align-items: center;
+
    gap: 0.5rem;
+
  }
  @media (max-width: 1349.98px) {
    .wrapper {
      display: flex;
@@ -90,11 +96,17 @@
      display: flex;
      align-items: center;
    }
+
    .body {
+
      align-items: flex-start;
+
    }
    .no-labels {
      height: 2rem;
      display: flex;
      align-items: center;
    }
+
    .input {
+
      width: 18rem;
+
    }
  }
</style>

@@ -106,9 +118,9 @@
        <Badge
          variant="neutral"
          size="small"
-
          style="cursor: pointer;"
+
          style="cursor: pointer; max-width: 14rem;"
          on:click={() => (removeToggles[label] = !removeToggles[label])}>
-
          <div class="label">{label}</div>
+
          <div class="label txt-overflow">{label}</div>
          {#if removeToggles[label]}
            <IconButton title="remove label">
              <IconSmall name="cross" on:click={() => removeLabel(label)} />
@@ -117,32 +129,33 @@
        </Badge>
      {/each}
      {#if showInput}
-
        <div
-
          style="width:100%; display: flex; align-items: center; gap: 0.5rem;">
-
          <TextInput
-
            autofocus
-
            {valid}
-
            disabled={submitInProgress}
-
            placeholder="Add label"
-
            bind:value={inputValue}
-
            on:submit={addLabel} />
-
          <IconButton
-
            title="discard label"
-
            on:click={() => {
-
              inputValue = "";
-
              showInput = false;
-
            }}>
-
            <IconSmall name="cross" />
-
          </IconButton>
-
          <IconButton title="save label" on:click={addLabel}>
-
            <IconSmall name="checkmark" />
-
          </IconButton>
-
        </div>
-
        {#if !valid && validationMessage}
-
          <div class="validation-message">
-
            <IconSmall name="exclamation-circle" />{validationMessage}
+
        <div>
+
          <div class="input">
+
            <TextInput
+
              autofocus
+
              {valid}
+
              disabled={submitInProgress}
+
              placeholder="Add label"
+
              bind:value={inputValue}
+
              on:submit={addLabel} />
+
            <IconButton
+
              title="discard label"
+
              on:click={() => {
+
                inputValue = "";
+
                showInput = false;
+
              }}>
+
              <IconSmall name="cross" />
+
            </IconButton>
+
            <IconButton title="save label" on:click={addLabel}>
+
              <IconSmall name="checkmark" />
+
            </IconButton>
          </div>
-
        {/if}
+
          {#if !valid && validationMessage}
+
            <div class="validation-message">
+
              <IconSmall name="exclamation-circle" />{validationMessage}
+
            </div>
+
          {/if}
+
        </div>
      {:else}
        <div class="global-hide-on-mobile-down">
          <Badge
modified src/views/projects/Cob/Labels.svelte
@@ -11,15 +11,15 @@
  }
</style>

-
{#each labels.slice(0, 4) as label}
+
{#each labels.slice(0, 2) 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">
+
{#if labels.length > 2}
+
  <Badge title={labels.slice(2, undefined).join(" ")} variant="neutral">
    <span class="label">
-
      +{labels.length - 4} more label{labels.length > 5 ? "s" : ""}
+
      +{labels.length - 2} more
    </span>
  </Badge>
{/if}
modified src/views/projects/Cob/Reviews.svelte
@@ -39,6 +39,9 @@
    }
    .header {
      margin-bottom: 0;
+
      height: 2rem;
+
      display: flex;
+
      align-items: center;
    }
    .no-reviews {
      display: flex;
modified src/views/projects/CommentCounter.svelte
@@ -11,6 +11,7 @@
    display: flex;
    align-items: center;
    gap: 0.25rem;
+
    white-space: nowrap;
  }
</style>

modified src/views/projects/Commit/CommitTeaser.svelte
@@ -60,6 +60,9 @@
  }
  .summary:hover {
    text-decoration: underline;
+
    text-decoration-thickness: 1px;
+
    text-underline-offset: 2px;
+
    cursor: pointer;
  }
  .commit-message {
    margin: 0.5rem 0;
modified src/views/projects/Issue.svelte
@@ -430,7 +430,7 @@
    background-color: var(--color-background-float);
  }
  .bottom {
-
    padding: 0 1rem 1rem 1rem;
+
    padding: 0 1rem 2.5rem 1rem;
    background-color: var(--color-background-default);
    height: 100%;
    border-top: 1px solid var(--color-border-hint);
@@ -467,6 +467,7 @@
    gap: 0.5rem;
    font-weight: var(--font-weight-semibold);
    font-size: var(--font-size-large);
+
    word-break: break-word;
  }
  .reactions {
    display: flex;
@@ -481,7 +482,7 @@
  }
</style>

-
<Layout {baseUrl} {project} activeTab="issues">
+
<Layout {baseUrl} {project} activeTab="issues" stylePaddingBottom="0">
  <div class="issue">
    <div class="main">
      <CobHeader>
@@ -504,7 +505,7 @@
            {/if}
          </div>
          <div style="display: flex; gap: 0.5rem;">
-
            {#if session && role.isDelegateOrAuthor(session.publicKey, delegates, issue.author.id) && issueState === "read"}
+
            {#if $experimental && session && role.isDelegateOrAuthor(session.publicKey, delegates, issue.author.id) && issueState === "read"}
              <div class="global-hide-on-mobile-down">
                <Button
                  variant="outline"
@@ -517,7 +518,7 @@
            {/if}
            {#if issueState === "read"}
              <Share {baseUrl} />
-
              {#if session && role.isDelegateOrAuthor(session.publicKey, delegates, issue.author.id)}
+
              {#if $experimental && session && role.isDelegateOrAuthor(session.publicKey, delegates, issue.author.id)}
                <div class="global-hide-on-small-desktop-down">
                  <CobStateButton
                    items={items.filter(
@@ -588,9 +589,7 @@
              labels={issue.labels}
              submitInProgress={labelState === "submit"}
              on:save={({ detail: newLabels }) => void saveLabels(newLabels)} />
-
            <div class="global-hide-on-mobile-down">
-
              <Embeds embeds={uniqueEmbeds} />
-
            </div>
+
            <Embeds embeds={uniqueEmbeds} />
          </div>
        </div>
        <svelte:fragment slot="description">
@@ -628,12 +627,10 @@
                }
              }} />
          {:else if issue.discussion[0].body}
-
            <div style:max-width="fit-content">
-
              <Markdown
-
                breaks
-
                content={issue.discussion[0].body}
-
                rawPath={rawPath(project.head)} />
-
            </div>
+
            <Markdown
+
              breaks
+
              content={issue.discussion[0].body}
+
              rawPath={rawPath(project.head)} />
          {:else}
            <span class="txt-missing">No description</span>
          {/if}
modified src/views/projects/Issue/IssueTeaser.svelte
@@ -43,19 +43,17 @@
    flex: 1;
  }
  .subtitle {
-
    font-size: var(--font-size-small);
-
    flex-wrap: wrap;
    display: flex;
-
    align-items: center;
+
    flex-direction: column;
+
    flex-wrap: wrap;
+
    font-size: var(--font-size-small);
    gap: 0.5rem;
  }
  .summary {
    display: flex;
    align-items: flex-start;
    gap: 0.5rem;
-
  }
-
  .issue-title:hover {
-
    text-decoration: underline;
+
    word-break: break-word;
  }
  .right {
    display: flex;
@@ -86,28 +84,29 @@
  </div>
  <div class="content">
    <div class="summary">
-
      <Link
-
        route={{
-
          resource: "project.issue",
-
          project: projectId,
-
          node: baseUrl,
-
          issue: issue.id,
-
        }}>
-
        <span class="issue-title">
+
      <span class="issue-title">
+
        <Link
+
          styleHoverState
+
          route={{
+
            resource: "project.issue",
+
            project: projectId,
+
            node: baseUrl,
+
            issue: issue.id,
+
          }}>
          {#if !issue.title}
            <span class="txt-missing">No title</span>
          {:else}
-
            <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>
+
            <InlineMarkdown fontSize="regular" content={issue.title} />
          {/if}
+
        </Link>
+
      </span>
+
      {#if issue.labels.length > 0}
+
        <span
+
          class="global-hide-on-small-desktop-down"
+
          style="display: inline-flex; gap: 0.5rem;">
+
          <Labels labels={issue.labels} />
        </span>
-
      </Link>
+
      {/if}
      <div class="right">
        {#if commentCount > 0}
          <CommentCounter {commentCount} />
@@ -115,15 +114,25 @@
      </div>
    </div>
    <div class="subtitle">
-
      <NodeId
-
        stylePopoverPositionLeft="-13px"
-
        nodeId={issue.author.id}
-
        alias={issue.author.alias} />
-
      opened
-
      <span class="global-oid">{formatObjectId(issue.id)}</span>
-
      <span title={absoluteTimestamp(issue.discussion[0].timestamp)}>
-
        {formatTimestamp(issue.discussion[0].timestamp)}
-
      </span>
+
      {#if issue.labels.length > 0}
+
        <div
+
          class="global-hide-on-medium-desktop-up"
+
          style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
+
          <Labels labels={issue.labels} />
+
        </div>
+
      {/if}
+
      <div
+
        style="display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;">
+
        <NodeId
+
          stylePopoverPositionLeft="-13px"
+
          nodeId={issue.author.id}
+
          alias={issue.author.alias} />
+
        opened
+
        <span class="global-oid">{formatObjectId(issue.id)}</span>
+
        <span title={absoluteTimestamp(issue.discussion[0].timestamp)}>
+
          {formatTimestamp(issue.discussion[0].timestamp)}
+
        </span>
+
      </div>
    </div>
  </div>
</div>
modified src/views/projects/Issues.svelte
@@ -91,6 +91,17 @@
    background-color: var(--color-fill-counter);
    color: var(--color-foreground-dim);
  }
+
  .placeholder {
+
    height: calc(100% - 4rem);
+
    display: flex;
+
    align-items: center;
+
    justify-content: center;
+
  }
+
  @media (max-width: 719.98px) {
+
    .placeholder {
+
      height: calc(100vh - 10rem);
+
    }
+
  }
</style>

<Layout {baseUrl} {project} activeTab="issues">
@@ -184,8 +195,7 @@
  {/if}

  {#if project.issues[state] === 0}
-
    <div
-
      style="height: calc(100% - 4rem); display: flex; align-items: center; justify-content: center;">
+
    <div class="placeholder">
      <Placeholder iconName="no-issues" caption={`No ${state} issues`} />
    </div>
  {/if}
modified src/views/projects/Patch.svelte
@@ -66,6 +66,7 @@
  import CobHeader from "@app/views/projects/Cob/CobHeader.svelte";
  import CobStateButton from "@app/views/projects/Cob/CobStateButton.svelte";
  import CommentToggleInput from "@app/components/CommentToggleInput.svelte";
+
  import CompareButton from "@app/views/projects/Patch/CompareButton.svelte";
  import CopyableId from "@app/components/CopyableId.svelte";
  import DiffStatBadge from "@app/components/DiffStatBadge.svelte";
  import Embeds from "@app/views/projects/Cob/Embeds.svelte";
@@ -652,10 +653,11 @@
    gap: 0.5rem;
    font-weight: var(--font-weight-semibold);
    font-size: var(--font-size-large);
+
    word-break: break-word;
  }
  .bottom {
    background-color: var(--color-background-default);
-
    padding: 1rem 1rem 0 1rem;
+
    padding: 1rem 1rem 0.5rem 1rem;
    height: 100%;
  }
  .actions {
@@ -688,10 +690,6 @@
    gap: 0.5rem;
    width: 100%;
  }
-
  .diff-button-range {
-
    font-family: var(--font-family-monospace);
-
    font-weight: var(--font-weight-bold);
-
  }
  .connector {
    width: 1px;
    height: 1.5rem;
@@ -708,7 +706,7 @@
  }
</style>

-
<Layout {baseUrl} {project} activeTab="patches">
+
<Layout {baseUrl} {project} activeTab="patches" stylePaddingBottom="0">
  <div class="patch">
    <div class="main">
      <CobHeader>
@@ -731,7 +729,7 @@
              </div>
            {/if}
          </div>
-
          {#if session && role.isDelegateOrAuthor(session.publicKey, delegates, patch.author.id) && patchState === "read"}
+
          {#if $experimental && session && role.isDelegateOrAuthor(session.publicKey, delegates, patch.author.id) && patchState === "read"}
            <div class="global-hide-on-mobile-down">
              <Button
                variant="outline"
@@ -744,7 +742,7 @@
          {/if}
          {#if patchState === "read"}
            <Share {baseUrl} />
-
            {#if session && role.isDelegateOrAuthor(session.publicKey, delegates, patch.author.id)}
+
            {#if $experimental && session && role.isDelegateOrAuthor(session.publicKey, delegates, patch.author.id)}
              <div class="global-hide-on-small-desktop-down">
                <CobStateButton
                  items={items.filter(
@@ -848,12 +846,10 @@
                  }
                }} />
            {:else if description}
-
              <div style:max-width="fit-content">
-
                <Markdown
-
                  breaks
-
                  content={description}
-
                  rawPath={rawPath(patch.id)} />
-
              </div>
+
              <Markdown
+
                breaks
+
                content={description}
+
                rawPath={rawPath(patch.id)} />
            {:else}
              <span class="txt-missing">No description available</span>
            {/if}
@@ -898,35 +894,15 @@
            <Link {route}>
              <Button
                size="large"
-
                variant={name === view.name ? "tab-active" : "tab"}>
+
                variant={name === view.name ||
+
                (view.name === "diff" && name === "changes")
+
                  ? "tab-active"
+
                  : "tab"}>
                <IconSmall name={icon} />
                {capitalize(name)}
              </Button>
            </Link>
          {/each}
-
          {#if view.name === "diff"}
-
            <Link
-
              route={{
-
                resource: "project.patch",
-
                project: project.id,
-
                node: baseUrl,
-
                patch: patch.id,
-
                view: {
-
                  name: "diff",
-
                  fromCommit: view.fromCommit,
-
                  toCommit: view.toCommit,
-
                },
-
              }}>
-
              <Button size="large" variant="tab-active">
-
                Compare <span class="diff-button-range">
-
                  {view.fromCommit.substring(0, 6)}..{view.toCommit.substring(
-
                    0,
-
                    6,
-
                  )}
-
                </span>
-
              </Button>
-
            </Link>
-
          {/if}
        </Radio>

        {#if view.name === "changes"}
@@ -936,6 +912,17 @@
            <RevisionSelector {view} {baseUrl} {patch} {project} />
          </div>
        {/if}
+
        {#if view.name === "diff"}
+
          <div
+
            class="global-hide-on-mobile-down"
+
            style="margin-left: auto; margin-top: -0.5rem;">
+
            <div style:margin-left="auto">
+
              <CompareButton
+
                fromCommit={view.fromCommit}
+
                toCommit={view.toCommit} />
+
            </div>
+
          </div>
+
        {/if}
        <div class="tabs-spacer" />
      </div>
      <div class="bottom">
@@ -945,12 +932,19 @@
            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>
+
            <RevisionSelector {view} {baseUrl} {patch} {project} />
          </div>
        {/if}
        {#if view.name === "diff"}
+
          <div
+
            style:width="100%"
+
            style:padding="0 1rem"
+
            style:display="flex"
+
            class="global-hide-on-small-desktop-up">
+
            <CompareButton
+
              fromCommit={view.fromCommit}
+
              toCommit={view.toCommit} />
+
          </div>
          <Changeset
            {baseUrl}
            projectId={project.id}
added src/views/projects/Patch/CompareButton.svelte
@@ -0,0 +1,17 @@
+
<script lang="ts">
+
  import Button from "@app/components/Button.svelte";
+
  import IconSmall from "@app/components/IconSmall.svelte";
+

+
  export let fromCommit: string;
+
  export let toCommit: string;
+
</script>
+

+
<Button size="regular" disabled>
+
  <span style:color="var(--color-foregroung-disabled)">Compare</span>
+
  <span
+
    style:color="var(--color-foregroung-disabled)"
+
    style:font-family="var(--font-family-monospace)">
+
    {fromCommit.substring(0, 6)}..{toCommit.substring(0, 6)}
+
  </span>
+
  <IconSmall name={"chevron-down"} />
+
</Button>
modified src/views/projects/Patch/PatchTeaser.svelte
@@ -40,25 +40,23 @@
    background-color: var(--color-fill-float-hover);
  }
  .content {
+
    width: 100%;
    gap: 0.5rem;
    display: flex;
    flex-direction: column;
  }
  .subtitle {
    display: flex;
-
    flex-direction: row;
-
    align-items: center;
-
    gap: 0.5rem;
-
    font-size: var(--font-size-small);
+
    flex-direction: column;
    flex-wrap: wrap;
+
    font-size: var(--font-size-small);
+
    gap: 0.5rem;
  }
  .summary {
    display: flex;
-
    flex-direction: row;
-
    gap: 1rem;
-
  }
-
  .patch-title:hover {
-
    text-decoration: underline;
+
    align-items: flex-start;
+
    gap: 0.5rem;
+
    word-break: break-word;
  }
  .right {
    margin-left: auto;
@@ -87,15 +85,8 @@
    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">
@@ -110,50 +101,60 @@
  <div class="content">
    <div class="summary">
      <Link
+
        styleHoverState
        route={{
          resource: "project.patch",
          project: projectId,
          node: baseUrl,
          patch: patch.id,
        }}>
-
        <span class="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>
+
        <InlineMarkdown fontSize="regular" content={patch.title} />
      </Link>
+
      {#if patch.labels.length > 0}
+
        <span
+
          class="global-hide-on-small-desktop-down"
+
          style="display: inline-flex; gap: 0.5rem;">
+
          <Labels labels={patch.labels} />
+
        </span>
+
      {/if}
+
      <div class="right">
+
        <div class="diff-comment">
+
          {#if commentCount > 0}
+
            <CommentCounter {commentCount} />
+
          {/if}
+
          <DiffStatBadgeLoader {projectId} {baseUrl} {patch} {latestRevision} />
+
        </div>
+
      </div>
    </div>
    <div class="summary">
      <span class="subtitle">
-
        <NodeId
-
          stylePopoverPositionLeft="-13px"
-
          nodeId={patch.author.id}
-
          alias={patch.author.alias} />
-
        {patch.revisions.length > 1 ? "updated" : "opened"}
-
        <span class="global-oid">{formatObjectId(patch.id)}</span>
-
        {#if patch.revisions.length > 1}
-
          <span class="global-hide-on-mobile-down">
-
            to <span class="global-oid">
-
              {formatObjectId(patch.revisions[patch.revisions.length - 1].id)}
+
        {#if patch.labels.length > 0}
+
          <div
+
            class="global-hide-on-medium-desktop-up"
+
            style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
+
            <Labels labels={patch.labels} />
+
          </div>
+
        {/if}
+
        <div
+
          style="display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;">
+
          <NodeId
+
            stylePopoverPositionLeft="-13px"
+
            nodeId={patch.author.id}
+
            alias={patch.author.alias} />
+
          {patch.revisions.length > 1 ? "updated" : "opened"}
+
          <span class="global-oid">{formatObjectId(patch.id)}</span>
+
          {#if patch.revisions.length > 1}
+
            <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)}>
+
            {formatTimestamp(latestRevision.timestamp)}
          </span>
-
        {/if}
-
        <span title={absoluteTimestamp(latestRevision.timestamp)}>
-
          {formatTimestamp(latestRevision.timestamp)}
-
        </span>
+
        </div>
      </span>
    </div>
  </div>
-
  <div class="right">
-
    <div class="diff-comment">
-
      {#if commentCount > 0}
-
        <CommentCounter {commentCount} />
-
      {/if}
-
      <DiffStatBadgeLoader {projectId} {baseUrl} {patch} {latestRevision} />
-
    </div>
-
  </div>
</div>
modified src/views/projects/Patch/RevisionSelector.svelte
@@ -28,9 +28,16 @@
    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:color={patch.revisions.length > 1
+
        ? "var(--color-foreground-contrast)"
+
        : "var(--color-foregroung-disabled)"}>
+
      Revision
+
    </span>
+
    <span
+
      style:color={patch.revisions.length > 1
+
        ? "var(--color-fill-secondary)"
+
        : "var(--color-foregroung-disabled)"}
      style:font-family="var(--font-family-monospace)">
      {utils.formatObjectId(view.revision)}
    </span>
modified src/views/projects/Patches.svelte
@@ -106,6 +106,17 @@
    flex-direction: column;
    gap: 1rem;
  }
+
  .placeholder {
+
    height: calc(100% - 4rem);
+
    display: flex;
+
    align-items: center;
+
    justify-content: center;
+
  }
+
  @media (max-width: 719.98px) {
+
    .placeholder {
+
      height: calc(100vh - 10rem);
+
    }
+
  }
</style>

<Layout {baseUrl} {project} activeTab="patches">
@@ -203,8 +214,7 @@
  {/if}

  {#if project.patches[state] === 0}
-
    <div
-
      style="height: calc(100% - 4rem); display: flex; align-items: center; justify-content: center;">
+
    <div class="placeholder">
      <Placeholder iconName="no-patches" caption={`No ${state} patches`} />
    </div>
  {/if}
modified src/views/projects/Source.svelte
@@ -122,6 +122,11 @@
      width: inherit;
      padding: 0;
    }
+
    .placeholder {
+
      border-radius: 0;
+
      border-left: 0;
+
      border-right: 0;
+
    }
  }
</style>