Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
UI polish
Open rudolfs opened 1 year ago

See individual commits for details.

check check-e2e

👉 Workflow runs 👉 Branch on GitHub

29 files changed +398 -373 76162e45 82560dbb
modified public/index.css
@@ -111,6 +111,30 @@ body {
    0 calc(100% - 2px)
  );

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

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

  --2px-corner-fill: polygon(
    0 4px,
    2px 4px,
modified src/components/Button.svelte
@@ -10,6 +10,7 @@
    flatLeft?: boolean;
    flatRight?: boolean;
    title?: string;
+
    styleHeight?: "2rem" | "2.5rem";
  }

  const {
@@ -21,6 +22,7 @@
    flatLeft = false,
    flatRight = false,
    title,
+
    styleHeight = "2rem",
  }: Props = $props();

  const style = $derived(
@@ -43,7 +45,6 @@

    color: var(--text-color);

-
    height: 2rem;
    column-gap: 0;
    row-gap: 0;
    display: grid;
@@ -371,6 +372,7 @@
<div
  class="container active"
  style:cursor={!disabled ? "pointer" : "default"}
+
  style:height={styleHeight}
  class:disabled
  class:active
  class:flat-right={flatRight}
modified src/components/CommentToggleInput.svelte
@@ -70,7 +70,7 @@
    hoverable
    styleCursor="text"
    variant="float"
-
    styleHeight="40px"
+
    styleHeight="2.5rem"
    styleWidth="100%"
    onclick={e => {
      e.preventDefault();
modified src/components/DropdownListItem.svelte
@@ -7,7 +7,8 @@
    onclick: () => void;
    disabled?: boolean;
    title?: string;
-
    style?: string;
+
    styleGap?: string;
+
    styleMinHeight?: string;
  }

  const {
@@ -16,7 +17,8 @@
    selected,
    disabled = false,
    title,
-
    style,
+
    styleGap,
+
    styleMinHeight,
  }: Props = $props();
</script>

@@ -27,7 +29,7 @@
    align-items: center;
    flex-direction: row;
    min-height: 2rem;
-
    padding: 0 0.5rem;
+
    padding: 0 0.75rem;
    white-space: nowrap;
    font-size: var(--font-size-small);
    font-weight: var(--font-weight-regular);
@@ -65,7 +67,8 @@
  class="item"
  class:selected
  class:disabled
-
  {style}
+
  style:gap={styleGap}
+
  style:min-height={styleMinHeight}
  {title}
  onclick={() => {
    if (disabled) {
modified src/components/HomeSidebar.svelte
@@ -57,7 +57,7 @@

<div class="container">
  <div>
-
    <div style:margin-bottom="0.5rem">
+
    <div style:margin-bottom="1rem">
      {#if activeTab.type === "inbox"}
        <Border
          styleCursor="pointer"
modified src/components/IssueSecondColumn.svelte
@@ -70,14 +70,8 @@
  .container {
    display: flex;
    align-items: center;
-
    min-height: 40px;
-
  }
-
  .issue-list {
-
    margin-top: 0.5rem;
-
    display: flex;
-
    flex-direction: column;
-
    gap: 2px;
-
    padding-bottom: 1rem;
+
    min-height: 2.5rem;
+
    margin-bottom: 1rem;
  }
</style>

@@ -101,7 +95,8 @@

<div class="container">
  <div
-
    class="txt-regular txt-semibold global-flex"
+
    class="txt-medium global-flex"
+
    style:font-weight="var(--font-weight-medium)"
    style:gap="4px"
    style:white-space="nowrap">
    {title}
@@ -119,9 +114,9 @@

  <div class="global-flex" style:margin-left="auto">
    <NakedButton
+
      styleHeight="2.5rem"
      keyShortcuts="ctrl+f"
      variant="ghost"
-
      stylePadding="0 4px"
      active={showFilters}
      onclick={() => {
        if (showFilters) {
@@ -136,13 +131,18 @@

    <OutlineButton
      variant="ghost"
-
      disabled={$activeRouteStore.resource === "repo.createIssue"}
+
      styleHeight="2.5rem"
+
      active={$activeRouteStore.resource === "repo.createIssue"}
      onclick={() => {
-
        void router.push({
-
          resource: "repo.createIssue",
-
          rid: repo.rid,
-
          status,
-
        });
+
        if ($activeRouteStore.resource === "repo.createIssue") {
+
          window.history.back();
+
        } else {
+
          void router.push({
+
            resource: "repo.createIssue",
+
            rid: repo.rid,
+
            status,
+
          });
+
        }
      }}>
      <Icon name="plus" />New
    </OutlineButton>
@@ -151,9 +151,9 @@

{#if showFilters}
  <div class="global-flex" style:margin="1rem 0">
-
    <Popover popoverPositionRight="0" popoverPositionTop="2.5rem">
+
    <Popover popoverPositionLeft="0" popoverPositionTop="3rem">
      {#snippet toggle(onclick)}
-
        <OutlineButton variant="ghost" {onclick}>
+
        <OutlineButton variant="ghost" {onclick} styleHeight="2.5rem">
          {@render icons(status)}
          {capitalize(status)}
          {@render counters(status)}
@@ -166,7 +166,8 @@
          <DropdownList items={["all", "open", "closed"] as IssueStatus[]}>
            {#snippet item(state)}
              <DropdownListItem
-
                style="gap: 0.5rem"
+
                styleGap="0.5rem"
+
                styleMinHeight="2.5rem"
                selected={status === state}
                onclick={() => {
                  changeFilter(state);
@@ -211,10 +212,17 @@
  </div>
{/if}

-
<div class="issue-list">
+
<Border
+
  variant={searchResults.length === 1 && searchInput !== ""
+
    ? "secondary"
+
    : "float"}
+
  styleFlexDirection="column"
+
  styleOverflow="hidden"
+
  styleGap="2px"
+
  styleAlignItems="center"
+
  styleJustifyContent="center">
  {#each searchResults as result}
    <IssueTeaser
-
      focussed={searchResults.length === 1 && searchInput !== ""}
      compact
      issue={result.obj.issue}
      {status}
@@ -223,24 +231,19 @@
  {/each}

  {#if searchResults.length === 0}
-
    <Border
-
      styleMinWidth="25rem"
-
      variant="ghost"
-
      styleAlignItems="center"
-
      styleJustifyContent="center">
-
      <div
-
        class="global-flex"
-
        style:height="74px"
-
        style:justify-content="center">
-
        <div class="txt-missing txt-small global-flex" style:gap="0.25rem">
-
          <Icon name="none" />
-
          {#if issues.length > 0 && searchResults.length === 0}
-
            No matching issues.
-
          {:else}
-
            No {status === "all" ? "" : status} issues.
-
          {/if}
-
        </div>
+
    <div
+
      class="global-flex"
+
      style:height="74px"
+
      style:justify-content="center"
+
      style:min-width="405px">
+
      <div class="txt-missing txt-small global-flex" style:gap="0.25rem">
+
        <Icon name="none" />
+
        {#if issues.length > 0 && searchResults.length === 0}
+
          No matching issues.
+
        {:else}
+
          No {status === "all" ? "" : status} issues.
+
        {/if}
      </div>
-
    </Border>
+
    </div>
  {/if}
-
</div>
+
</Border>
deleted src/components/IssueStateBadge.svelte
@@ -1,22 +0,0 @@
-
<script lang="ts">
-
  import type { Issue } from "@bindings/cob/issue/Issue";
-

-
  import capitalize from "lodash/capitalize";
-
  import { issueStatusColor } from "@app/lib/utils";
-

-
  interface Props {
-
    state: Issue["state"];
-
  }
-

-
  const { state }: Props = $props();
-
</script>
-

-
<div
-
  class="global-counter txt-small"
-
  style:width="fit-content"
-
  style:color="var(--color-foreground-match-background)"
-
  style:background-color={issueStatusColor[state.status]}>
-
  {capitalize(state.status)}{state.status === "closed"
-
    ? ` as ${state.reason}`
-
    : ""}
-
</div>
modified src/components/IssueStateButton.svelte
@@ -1,85 +1,91 @@
<script lang="ts">
  import type { State } from "@bindings/cob/issue/State";

+
  import capitalize from "lodash/capitalize";
  import isEqual from "lodash/isEqual";

  import { closeFocused } from "@app/components/Popover.svelte";
+
  import { issueStatusBackgroundColor, issueStatusColor } from "@app/lib/utils";

  import Border from "@app/components/Border.svelte";
-
  import Button from "@app/components/Button.svelte";
  import DropdownList from "@app/components/DropdownList.svelte";
  import DropdownListItem from "@app/components/DropdownListItem.svelte";
  import Icon from "@app/components/Icon.svelte";
  import Popover from "@app/components/Popover.svelte";

-
  const {
-
    save,
-
    issueState,
-
  }: {
-
    save: (state: State) => Promise<void>;
-
    issueState: State;
-
  } = $props();
-

-
  const actions: { caption: string; state: State }[] = [
-
    { caption: "Reopen", state: { status: "open" } },
-
    {
-
      caption: "Close as solved",
-
      state: { status: "closed", reason: "solved" },
-
    },
-
    { caption: "Close as other", state: { status: "closed", reason: "other" } },
-
  ];
-

-
  let selectedAction = $state(
-
    issueState.status === "open" ? actions[1] : actions[0],
-
  );
+
  interface Props {
+
    selectedState: State;
+
    onSelect: (selectedStatus: State) => void;
+
  }

-
  // React to state changes that come from outside of this button.
-
  $effect(() => {
-
    selectedAction = issueState.status === "open" ? actions[1] : actions[0];
-
  });
+
  const { selectedState, onSelect }: Props = $props();
</script>

<style>
-
  .main {
+
  button {
+
    cursor: pointer;
+
    border: 0;
+
    background: none;
+
    margin: 0;
+
    padding: 0;
    display: flex;
-
    flex-direction: row;
+
    align-items: center;
    justify-content: center;
+
    font-size: var(--font-size-small);
+
  }
+
  .badge {
+
    gap: 6px;
+
    padding-right: 10px;
  }
</style>

-
<div class="main">
-
  <Button
-
    variant="secondary"
-
    flatRight
-
    onclick={() => void save($state.snapshot(selectedAction["state"]))}>
-
    {selectedAction["caption"]}
-
  </Button>
-

-
  <Popover
-
    popoverPadding="0"
-
    popoverPositionTop="2.5rem"
-
    popoverPositionRight="0">
-
    {#snippet toggle(onclick)}
-
      <Button flatLeft {onclick} variant="secondary">
+
<Popover popoverPadding="0" popoverPositionTop="2rem" popoverPositionLeft="0">
+
  {#snippet toggle(onclick)}
+
    <button {onclick}>
+
      <span
+
        class="global-counter badge"
+
        style:color={issueStatusColor[selectedState.status]}
+
        style:background-color={issueStatusBackgroundColor[
+
          selectedState.status
+
        ]}>
+
        <Icon
+
          name={selectedState.status === "open"
+
            ? "issue"
+
            : `issue-${selectedState.status}`} />
+
        {capitalize(selectedState.status)}
+
        {selectedState.status === "closed" ? `as ${selectedState.reason}` : ""}
        <Icon name="chevron-down" />
-
      </Button>
-
    {/snippet}
-
    {#snippet popover()}
-
      <Border variant="ghost">
-
        <DropdownList
-
          items={actions.filter(a => !isEqual(a.state, issueState))}>
-
          {#snippet item(action)}
-
            <DropdownListItem
-
              selected={isEqual(selectedAction, action)}
-
              onclick={() => {
-
                selectedAction = action;
-
                closeFocused();
-
              }}>
-
              {action.caption}
-
            </DropdownListItem>
-
          {/snippet}
-
        </DropdownList>
-
      </Border>
-
    {/snippet}
-
  </Popover>
-
</div>
+
      </span>
+
    </button>
+
  {/snippet}
+
  {#snippet popover()}
+
    <Border variant="ghost">
+
      <DropdownList
+
        items={[
+
          { status: "open" },
+
          { status: "closed", reason: "solved" },
+
          { status: "closed", reason: "other" },
+
        ] as State[]}>
+
        {#snippet item(state)}
+
          <DropdownListItem
+
            selected={isEqual(selectedState, state)}
+
            onclick={() => {
+
              onSelect(state);
+
              closeFocused();
+
            }}>
+
            <span
+
              class="global-flex"
+
              style:color={issueStatusColor[state.status]}>
+
              <Icon
+
                name={state.status === "open"
+
                  ? "issue"
+
                  : `issue-${state.status}`} />
+
              {capitalize(state.status)}
+
              {state.status === "closed" ? `as ${state.reason}` : ""}
+
            </span>
+
          </DropdownListItem>
+
        {/snippet}
+
      </DropdownList>
+
    </Border>
+
  {/snippet}
+
</Popover>
modified src/components/IssueTeaser.svelte
@@ -42,6 +42,7 @@
    align-items: center;
    gap: 0.25rem;
    min-height: 5rem;
+
    min-width: 405px;
    background-color: var(--color-background-float);
    padding: 1rem;
    cursor: pointer;
@@ -60,13 +61,13 @@
    margin-right: 1rem;
  }
  .issue-teaser:first-of-type {
-
    clip-path: var(--3px-top-corner-fill);
+
    clip-path: var(--1px-top-corner-fill);
  }
  .issue-teaser:last-of-type {
-
    clip-path: var(--3px-bottom-corner-fill);
+
    clip-path: var(--1px-bottom-corner-fill);
  }
  .issue-teaser:only-of-type {
-
    clip-path: var(--3px-corner-fill);
+
    clip-path: var(--1px-corner-fill);
  }
</style>

modified src/components/IssuesSecondColumn.svelte
@@ -58,7 +58,7 @@

<div class="container">
  <div>
-
    <div style:margin-bottom="0.5rem" style:padding-left="0.75rem">
+
    <div style:margin-bottom="1rem" style:padding-left="0.75rem">
      <RepoTeaser name={project.data.name} seeding={repo.seeding} />
    </div>

modified src/components/NakedButton.svelte
@@ -7,7 +7,7 @@
    disabled?: boolean;
    variant: "primary" | "secondary" | "ghost";
    onclick?: (e: MouseEvent) => void;
-
    styleHeight?: string;
+
    styleHeight?: "2rem" | "2.5rem";
    stylePadding?: string;
    active?: boolean;
    keyShortcuts?: string;
modified src/components/OutlineButton.svelte
@@ -6,9 +6,18 @@
    variant: "primary" | "secondary" | "ghost";
    onclick?: () => void;
    disabled?: boolean;
+
    styleHeight?: "2rem" | "2.5rem";
+
    active?: boolean;
  }

-
  const { children, variant, onclick, disabled = false }: Props = $props();
+
  const {
+
    children,
+
    variant,
+
    onclick,
+
    disabled = false,
+
    styleHeight = "2rem",
+
    active = false,
+
  }: Props = $props();

  const style = $derived(
    `--button-color-1: var(--color-fill-${variant});` +
@@ -160,46 +169,59 @@
    background-color: var(--button-color-1);
  }

-
  .container:active:not(.disabled) .p1-3 {
+
  .container:active:not(.disabled) .p1-3,
+
  .container.active:not(.disabled) .p1-3 {
    background-color: var(--button-color-1);
  }

-
  .container:active:not(.disabled) .p2-2 {
+
  .container:active:not(.disabled) .p2-2,
+
  .container.active:not(.disabled) .p2-2 {
    background-color: var(--button-color-1);
  }
-
  .container:active:not(.disabled) .p2-3 {
+
  .container:active:not(.disabled) .p2-3,
+
  .container.active:not(.disabled) .p2-3 {
    background-color: var(--button-color-3);
  }
-
  .container:active:not(.disabled) .p2-4 {
+
  .container:active:not(.disabled) .p2-4,
+
  .container.active:not(.disabled) .p2-4 {
    background-color: var(--button-color-1);
  }

-
  .container:active:not(.disabled) .p3-1 {
+
  .container:active:not(.disabled) .p3-1,
+
  .container.active:not(.disabled) .p3-1 {
    background-color: var(--button-color-1);
  }
-
  .container:active:not(.disabled) .p3-2 {
+
  .container:active:not(.disabled) .p3-2,
+
  .container.active:not(.disabled) .p3-2 {
    background-color: var(--button-color-3);
  }
-
  .container:active:not(.disabled) .p3-3 {
+
  .container:active:not(.disabled) .p3-3,
+
  .container.active:not(.disabled) .p3-3 {
    background-color: var(--button-color-1);
  }
-
  .container:active:not(.disabled) .p3-4 {
+
  .container:active:not(.disabled) .p3-4,
+
  .container.active:not(.disabled) .p3-4 {
    background-color: var(--button-color-2);
  }
-
  .container:active:not(.disabled) .p3-5 {
+
  .container:active:not(.disabled) .p3-5,
+
  .container.active:not(.disabled) .p3-5 {
    background-color: var(--button-color-1);
  }

-
  .container:active:not(.disabled) .p4-2 {
+
  .container:active:not(.disabled) .p4-2,
+
  .container.active:not(.disabled) .p4-2 {
    background-color: var(--button-color-1);
  }
-
  .container:active:not(.disabled) .p4-3 {
+
  .container:active:not(.disabled) .p4-3,
+
  .container.active:not(.disabled) .p4-3 {
    background-color: var(--button-color-2);
  }
-
  .container:active:not(.disabled) .p4-4 {
+
  .container:active:not(.disabled) .p4-4,
+
  .container.active:not(.disabled) .p4-4 {
    background-color: var(--button-color-1);
  }
-
  .container:active:not(.disabled) .p5-3 {
+
  .container:active:not(.disabled) .p5-3,
+
  .container.active:not(.disabled) .p5-3 {
    background-color: var(--button-color-1);
  }

@@ -224,7 +246,6 @@
  }

  .container {
-
    height: 2rem;
    cursor: pointer;
    white-space: nowrap;

@@ -249,8 +270,10 @@
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
  class="container"
+
  style:height={styleHeight}
  style:cursor={!disabled ? "pointer" : "default"}
  class:disabled
+
  class:active
  onclick={!disabled ? onclick : undefined}
  role="button"
  tabindex="0"
deleted src/components/PatchStateBadge.svelte
@@ -1,20 +0,0 @@
-
<script lang="ts">
-
  import type { Patch } from "@bindings/cob/patch/Patch";
-

-
  import capitalize from "lodash/capitalize";
-
  import { patchStatusColor } from "@app/lib/utils";
-

-
  interface Props {
-
    state: Patch["state"];
-
  }
-

-
  const { state }: Props = $props();
-
</script>
-

-
<div
-
  class="global-counter txt-small"
-
  style:width="fit-content"
-
  style:color="var(--color-foreground-match-background)"
-
  style:background-color={patchStatusColor[state.status]}>
-
  {capitalize(state.status)}
-
</div>
modified src/components/PatchStateButton.svelte
@@ -1,86 +1,88 @@
<script lang="ts">
  import type { State } from "@bindings/cob/patch/State";

-
  import isEqual from "lodash/isEqual";
+
  import capitalize from "lodash/capitalize";

  import { closeFocused } from "@app/components/Popover.svelte";
+
  import { patchStatusBackgroundColor, patchStatusColor } from "@app/lib/utils";

  import Border from "@app/components/Border.svelte";
-
  import Button from "@app/components/Button.svelte";
  import DropdownList from "@app/components/DropdownList.svelte";
  import DropdownListItem from "@app/components/DropdownListItem.svelte";
  import Icon from "@app/components/Icon.svelte";
  import Popover from "@app/components/Popover.svelte";

-
  const {
-
    save,
-
    patchState,
-
  }: {
-
    save: (state: State) => Promise<void>;
-
    patchState: State;
-
  } = $props();
-

-
  const actions: { caption: string; state: State }[] = [
-
    {
-
      caption: "Reopen",
-
      state: { status: "open" },
-
    },
-
    { caption: "Convert to draft", state: { status: "draft" } },
-
    { caption: "Archive", state: { status: "archived" } },
-
  ];
-

-
  let selectedAction = $state(
-
    patchState.status === "open" ? actions[1] : actions[0],
-
  );
+
  interface Props {
+
    selectedState: State;
+
    onSelect: (newState: State) => void;
+
  }

-
  // React to state changes that come from outside of this button.
-
  $effect(() => {
-
    selectedAction = patchState.status === "open" ? actions[1] : actions[0];
-
  });
+
  const { selectedState, onSelect }: Props = $props();
</script>

<style>
-
  .main {
+
  button {
+
    cursor: pointer;
+
    border: 0;
+
    background: none;
+
    margin: 0;
+
    padding: 0;
    display: flex;
-
    flex-direction: row;
+
    align-items: center;
    justify-content: center;
+
    font-size: var(--font-size-small);
+
  }
+
  .badge {
+
    gap: 6px;
+
    padding-right: 10px;
  }
</style>

-
<div class="main">
-
  <Button
-
    variant="secondary"
-
    flatRight
-
    onclick={() =>
-
      void save($state.snapshot(selectedAction["state"]) as State)}>
-
    {selectedAction["caption"]}
-
  </Button>
-

-
  <Popover
-
    popoverPadding="0"
-
    popoverPositionTop="2.5rem"
-
    popoverPositionRight="0">
-
    {#snippet toggle(onclick)}
-
      <Button flatLeft {onclick} variant="secondary">
+
<Popover popoverPadding="0" popoverPositionTop="2rem" popoverPositionLeft="0">
+
  {#snippet toggle(onclick)}
+
    <button {onclick}>
+
      <span
+
        class="global-counter badge"
+
        style:color={patchStatusColor[selectedState.status]}
+
        style:background-color={patchStatusBackgroundColor[
+
          selectedState.status
+
        ]}>
+
        <Icon
+
          name={selectedState.status === "open"
+
            ? "patch"
+
            : `patch-${selectedState.status}`} />
+
        {capitalize(selectedState.status)}
        <Icon name="chevron-down" />
-
      </Button>
-
    {/snippet}
-
    {#snippet popover()}
-
      <Border variant="ghost">
-
        <DropdownList
-
          items={actions.filter(a => !isEqual(a.state, patchState))}>
-
          {#snippet item(action)}
-
            <DropdownListItem
-
              selected={isEqual(selectedAction, action)}
-
              onclick={() => {
-
                selectedAction = action;
-
                closeFocused();
-
              }}>
-
              {action.caption}
-
            </DropdownListItem>
-
          {/snippet}
-
        </DropdownList>
-
      </Border>
-
    {/snippet}
-
  </Popover>
-
</div>
+
      </span>
+
    </button>
+
  {/snippet}
+
  {#snippet popover()}
+
    <Border variant="ghost">
+
      <DropdownList
+
        items={[
+
          { status: "open" },
+
          { status: "draft" },
+
          { status: "archived" },
+
        ] as State[]}>
+
        {#snippet item(state)}
+
          <DropdownListItem
+
            selected={selectedState.status === state.status}
+
            onclick={() => {
+
              onSelect(state);
+
              closeFocused();
+
            }}>
+
            <span
+
              class="global-flex"
+
              style:color={patchStatusColor[state.status]}>
+
              <Icon
+
                name={state.status === "open"
+
                  ? "patch"
+
                  : `patch-${state.status}`} />
+
              {capitalize(state.status)}
+
            </span>
+
          </DropdownListItem>
+
        {/snippet}
+
      </DropdownList>
+
    </Border>
+
  {/snippet}
+
</Popover>
modified src/components/PatchTeaser.svelte
@@ -47,6 +47,7 @@
    justify-content: space-between;
    gap: 0.25rem;
    min-height: 5rem;
+
    min-width: 440px;
    background-color: var(--color-background-float);
    padding: 1rem;
    cursor: pointer;
@@ -65,13 +66,13 @@
    margin-right: 1rem;
  }
  .patch-teaser:first-of-type {
-
    clip-path: var(--3px-top-corner-fill);
+
    clip-path: var(--1px-top-corner-fill);
  }
  .patch-teaser:last-of-type {
-
    clip-path: var(--3px-bottom-corner-fill);
+
    clip-path: var(--1px-bottom-corner-fill);
  }
  .patch-teaser:only-of-type {
-
    clip-path: var(--3px-corner-fill);
+
    clip-path: var(--1px-corner-fill);
  }
</style>

modified src/components/PatchesSecondColumn.svelte
@@ -63,7 +63,7 @@

<div class="container">
  <div>
-
    <div style:margin-bottom="0.5rem" style:padding-left="0.75rem">
+
    <div style:margin-bottom="1rem" style:padding-left="0.75rem">
      <RepoTeaser name={project.data.name} seeding={repo.seeding} />
    </div>

modified src/components/RepoTeaser.svelte
@@ -12,7 +12,7 @@
<style>
  .teaser {
    align-items: center;
-
    min-height: 40px;
+
    min-height: 2.5rem;
  }

  .seeding {
modified src/components/Review.svelte
@@ -253,9 +253,9 @@
    display: flex;
    align-items: center;
    white-space: nowrap;
-
    min-height: 40px;
-
    gap: 0.5rem;
-
    margin-bottom: 0.5rem;
+
    min-height: 2.5rem;
+
    gap: 0.75rem;
+
    margin-bottom: 1rem;
  }
  .metadata-divider {
    width: 2px;
@@ -297,12 +297,12 @@
</style>

<div class="content">
-
  <div style:margin-bottom="0.5rem">
+
  <div style:margin-bottom="1rem">
    <div class="title">
      <NakedButton
+
        styleHeight="2.5rem"
        variant="ghost"
-
        onclick={onNavigateBack}
-
        stylePadding="0 4px">
+
        onclick={onNavigateBack}>
        <Icon name="arrow-left" />
      </NakedButton>
      <span class="global-flex" style:gap="0">
modified src/components/Settings.svelte
@@ -10,13 +10,13 @@

  interface Props {
    compact?: boolean;
-
    styleHeight?: string;
+
    styleHeight?: ComponentProps<typeof NakedButton>["styleHeight"];
    popoverProps: Partial<ComponentProps<typeof Popover>>;
  }

  const {
    compact = true,
-
    styleHeight = "40px",
+
    styleHeight = "2.5rem",
    popoverProps,
  }: Props = $props();
</script>
modified src/components/Sidebar.svelte
@@ -21,8 +21,8 @@
    cursor: pointer;
    border: 0;
    background: none;
-
    height: 40px;
-
    width: 40px;
+
    height: 2.5rem;
+
    width: 2.5rem;
    clip-path: var(--2px-corner-fill);
    margin: 0;
    display: flex;
@@ -38,7 +38,7 @@
</style>

<div class="global-flex" style:flex-direction="column" style:gap="0.5rem">
-
  <div class="global-flex" style:height="40px">
+
  <div class="global-flex" style:height="2.5rem">
    <Icon name="repo" />
  </div>
  {#if activeTab === "issues"}
@@ -52,8 +52,8 @@
        });
      }}
      variant="ghost"
-
      styleWidth="40px"
-
      styleHeight="40px"
+
      styleWidth="2.5rem"
+
      styleHeight="2.5rem"
      styleJustifyContent="center">
      <Icon name="issue" />
    </Border>
@@ -82,8 +82,8 @@
        });
      }}
      variant="ghost"
-
      styleWidth="40px"
-
      styleHeight="40px"
+
      styleWidth="2.5rem"
+
      styleHeight="2.5rem"
      styleJustifyContent="center">
      <Icon name="patch" />
    </Border>
@@ -104,7 +104,7 @@

<div>
  <NakedButton
-
    styleHeight="40px"
+
    styleHeight="2.5rem"
    variant="ghost"
    onclick={() => {
      if (getLayout()) {
modified src/views/home/Inbox.svelte
@@ -158,7 +158,7 @@
    justify-content: space-between;
    padding-right: 1.5rem;
    align-items: center;
-
    min-height: 40px;
+
    min-height: 2.5rem;
  }
</style>

modified src/views/home/Repos.svelte
@@ -102,7 +102,7 @@
    justify-content: space-between;
    padding-right: 1.5rem;
    align-items: center;
-
    min-height: 40px;
+
    min-height: 2.5rem;
  }
</style>

@@ -120,7 +120,7 @@
      {notificationCount} />
  {/snippet}
  <div class="container">
-
    <div class="global-flex" style:margin-bottom="0.5rem">
+
    <div class="global-flex" style:margin-bottom="1rem">
      <div class="header">Repositories</div>
      {#if repos.length > 0}
        <div class="global-flex" style:margin-left="auto">
modified src/views/repo/CreateIssue.svelte
@@ -85,7 +85,11 @@
    font-weight: var(--font-weight-medium);
    -webkit-user-select: text;
    user-select: text;
-
    margin-top: 0.35rem;
+
    display: flex;
+
    align-items: center;
+
    justify-content: space-between;
+
    word-break: break-word;
+
    min-height: 2.5rem;
    margin-bottom: 1rem;
  }
  .content {
modified src/views/repo/Issue.svelte
@@ -30,7 +30,6 @@
  import Icon from "@app/components/Icon.svelte";
  import InlineTitle from "@app/components/InlineTitle.svelte";
  import IssueSecondColumn from "@app/components/IssueSecondColumn.svelte";
-
  import IssueStateBadge from "@app/components/IssueStateBadge.svelte";
  import IssueStateButton from "@app/components/IssueStateButton.svelte";
  import IssueTimeline from "@app/components/IssueTimeline.svelte";
  import LabelInput from "@app/components/LabelInput.svelte";
@@ -292,13 +291,13 @@
    align-items: center;
    justify-content: space-between;
    word-break: break-word;
-
    min-height: 40px;
+
    min-height: 2.5rem;
  }
  .status {
    padding: 0;
    margin-right: 0.75rem;
-
    height: 2rem;
-
    width: 2rem;
+
    height: 2.5rem;
+
    width: 2.5rem;
  }
  .issue-body {
    margin: 1rem 0;
@@ -368,7 +367,7 @@
  {/snippet}

  <div class="content">
-
    <div style:margin-bottom="0.5rem">
+
    <div style:margin-bottom="1rem">
      {#if editingTitle}
        <div class="title">
          <div
@@ -410,7 +409,6 @@
                updatedTitle = issue.title;
                editingTitle = !editingTitle;
              }} />
-
            <IssueStateButton issueState={issue.state} save={saveState} />
          </div>
        </div>
      {:else}
@@ -433,7 +431,6 @@
          {#if roles.isDelegateOrAuthor( config.publicKey, repo.delegates.map(delegate => delegate.did), issue.body.author.did, )}
            <div class="title-icons">
              <Icon name="pen" onclick={() => (editingTitle = !editingTitle)} />
-
              <IssueStateButton issueState={issue.state} save={saveState} />
            </div>
          {/if}
        </div>
@@ -443,7 +440,14 @@
    <Border variant="ghost" styleGap="0">
      <div class="metadata-section" style:min-width="8rem">
        <div class="metadata-section-title">Status</div>
-
        <IssueStateBadge state={issue.state} />
+
        <IssueStateButton
+
          selectedState={issue.state}
+
          onSelect={newState => {
+
            void saveState(newState);
+
            if (status !== "all" && newState.status !== status) {
+
              void loadIssues("all");
+
            }
+
          }} />
      </div>

      <div class="metadata-divider"></div>
modified src/views/repo/Issues.svelte
@@ -62,18 +62,13 @@
  .container {
    padding: 1rem 1rem 1rem 0;
  }
-
  .list {
-
    display: flex;
-
    flex-direction: column;
-
    gap: 2px;
-
  }
  .header {
    font-weight: var(--font-weight-medium);
    font-size: var(--font-size-medium);
    display: flex;
    align-items: center;
-
    min-height: 40px;
-
    margin-bottom: 0.5rem;
+
    min-height: 2.5rem;
+
    margin-bottom: 1rem;
  }
</style>

@@ -96,7 +91,7 @@
  <div class="container">
    <div class="header">
      <div>Issues</div>
-
      <div class="global-flex" style:margin-left="auto">
+
      <div class="global-flex" style:margin-left="auto" style:gap="0.75rem">
        {#if issues.length > 0}
          <TextInput
            onSubmit={async () => {
@@ -126,6 +121,7 @@
        {/if}
        <div class="txt-regular txt-semibold">
          <Button
+
            styleHeight="2.5rem"
            variant="secondary"
            onclick={() => {
              void router.push({
@@ -140,35 +136,34 @@
      </div>
    </div>

-
    <div class="list">
+
    <Border
+
      variant={searchResults.length === 1 && searchInput !== ""
+
        ? "secondary"
+
        : "float"}
+
      styleFlexDirection="column"
+
      styleOverflow="hidden"
+
      styleGap="2px"
+
      styleAlignItems="center"
+
      styleJustifyContent="center">
      {#each searchResults as result}
-
        <IssueTeaser
-
          focussed={searchResults.length === 1 && searchInput !== ""}
-
          issue={result.obj.issue}
-
          rid={repo.rid}
-
          {status} />
+
        <IssueTeaser issue={result.obj.issue} rid={repo.rid} {status} />
      {/each}

      {#if searchResults.length === 0}
-
        <Border
-
          variant="ghost"
-
          styleAlignItems="center"
-
          styleJustifyContent="center">
-
          <div
-
            class="global-flex"
-
            style:height="74px"
-
            style:justify-content="center">
-
            <div class="txt-missing txt-small global-flex" style:gap="0.25rem">
-
              <Icon name="none" />
-
              {#if issues.length > 0 && searchResults.length === 0}
-
                No matching issues.
-
              {:else}
-
                No {status === "all" ? "" : status} issues.
-
              {/if}
-
            </div>
+
        <div
+
          class="global-flex"
+
          style:height="74px"
+
          style:justify-content="center">
+
          <div class="txt-missing txt-small global-flex" style:gap="0.25rem">
+
            <Icon name="none" />
+
            {#if issues.length > 0 && searchResults.length === 0}
+
              No matching issues.
+
            {:else}
+
              No {status === "all" ? "" : status} issues.
+
            {/if}
          </div>
-
        </Border>
+
        </div>
      {/if}
-
    </div>
+
    </Border>
  </div>
</Layout>
modified src/views/repo/Patch.svelte
@@ -38,7 +38,6 @@
  import Link from "@app/components/Link.svelte";
  import NakedButton from "@app/components/NakedButton.svelte";
  import OutlineButton from "@app/components/OutlineButton.svelte";
-
  import PatchStateBadge from "@app/components/PatchStateBadge.svelte";
  import PatchStateButton from "@app/components/PatchStateButton.svelte";
  import PatchTeaser from "@app/components/PatchTeaser.svelte";
  import PatchTimeline from "@app/components/PatchTimeline.svelte";
@@ -174,20 +173,17 @@
    }
  }

-
  async function saveState(state: Patch["state"]) {
+
  async function saveState(newState: Patch["state"]) {
    try {
      await invoke("edit_patch", {
        rid: repo.rid,
        cobId: patch.id,
        action: {
          type: "lifecycle",
-
          state,
+
          state: newState,
        },
        opts: { announce: $nodeRunning && $announce },
      });
-
      if (initialStatus !== undefined) {
-
        status = state["status"];
-
      }
    } catch (error) {
      console.error("Changing state failed", error);
    } finally {
@@ -311,7 +307,7 @@
    align-items: center;
    justify-content: space-between;
    word-break: break-word;
-
    min-height: 40px;
+
    min-height: 2.5rem;
  }
  .title-icons {
    display: flex;
@@ -322,15 +318,8 @@
  .status {
    padding: 0;
    margin-right: 0.75rem;
-
    height: 2rem;
-
    width: 2rem;
-
  }
-
  .patch-list {
-
    margin-top: 0.5rem;
-
    display: flex;
-
    flex-direction: column;
-
    gap: 2px;
-
    padding-bottom: 1rem;
+
    height: 2.5rem;
+
    width: 2.5rem;
  }
  .content {
    padding: 1rem 1rem 1rem 0;
@@ -350,6 +339,7 @@
    flex-direction: column;
    align-items: flex-start;
    height: 100%;
+
    z-index: 20;
  }
  .metadata-section-title {
    margin-bottom: 0.5rem;
@@ -389,7 +379,10 @@
  {/snippet}

  {#snippet secondColumn()}
-
    <div class="txt-regular txt-semibold global-flex" style:min-height="40px">
+
    <div
+
      class="txt-regular txt-semibold global-flex"
+
      style:min-height="2.5rem"
+
      style:margin-bottom="1rem">
      <div class="global-flex" style:gap="4px">
        {project.data.name}
        <Icon name="chevron-right" />
@@ -405,9 +398,9 @@
      </div>
      <div style:margin-left="auto">
        <NakedButton
+
          styleHeight="2.5rem"
          keyShortcuts="ctrl+f"
          variant="ghost"
-
          stylePadding="0 4px"
          active={showFilters}
          onclick={() => {
            if (showFilters) {
@@ -423,9 +416,9 @@
    </div>
    {#if showFilters}
      <div class="global-flex" style:margin="1rem 0">
-
        <Popover popoverPositionLeft="0" popoverPositionTop="2.5rem">
+
        <Popover popoverPositionLeft="0" popoverPositionTop="3rem">
          {#snippet toggle(onclick)}
-
            <OutlineButton variant="ghost" {onclick}>
+
            <OutlineButton variant="ghost" {onclick} styleHeight="2.5rem">
              {@render icons(status)}
              {status ? capitalize(status) : "All"}
              {@render counters(status)}
@@ -445,7 +438,8 @@
                ] as const}>
                {#snippet item(state)}
                  <DropdownListItem
-
                    style="gap: 0.5rem"
+
                    styleGap="0.5rem"
+
                    styleMinHeight="2.5rem"
                    selected={status === state}
                    onclick={async () => {
                      await loadPatches(state);
@@ -501,10 +495,17 @@
        {/if}
      </div>
    {/if}
-
    <div class="patch-list">
+
    <Border
+
      variant={searchResults.length === 1 && searchInput !== ""
+
        ? "secondary"
+
        : "float"}
+
      styleFlexDirection="column"
+
      styleOverflow="hidden"
+
      styleGap="2px"
+
      styleAlignItems="center"
+
      styleJustifyContent="center">
      {#each searchResults as teaser}
        <PatchTeaser
-
          focussed={searchResults.length === 1 && searchInput !== ""}
          compact
          loadPatch={async (id: string) => {
            review = undefined;
@@ -517,27 +518,22 @@
      {/each}

      {#if searchResults.length === 0}
-
        <Border
-
          styleMinWidth="25rem"
-
          variant="ghost"
-
          styleAlignItems="center"
-
          styleJustifyContent="center">
-
          <div
-
            class="global-flex"
-
            style:height="74px"
-
            style:justify-content="center">
-
            <div class="txt-missing txt-small global-flex" style:gap="0.25rem">
-
              <Icon name="none" />
-
              {#if patchTeasers.length > 0 && searchResults.length === 0}
-
                No matching patches.
-
              {:else}
-
                No {status === undefined ? "" : status} patches.
-
              {/if}
-
            </div>
+
        <div
+
          class="global-flex"
+
          style:height="74px"
+
          style:min-width="440px"
+
          style:justify-content="center">
+
          <div class="txt-missing txt-small global-flex" style:gap="0.25rem">
+
            <Icon name="none" />
+
            {#if patchTeasers.length > 0 && searchResults.length === 0}
+
              No matching patches.
+
            {:else}
+
              No {status === undefined ? "" : status} patches.
+
            {/if}
          </div>
-
        </Border>
+
        </div>
      {/if}
-
    </div>
+
    </Border>
  {/snippet}

  {#if review}
@@ -553,7 +549,7 @@
      }} />
  {:else}
    <div class="content">
-
      <div style:margin-bottom="0.5rem">
+
      <div style:margin-bottom="1rem">
        {#if editingTitle}
          <div class="title">
            <div
@@ -595,7 +591,6 @@
                  updatedTitle = patch.title;
                  editingTitle = !editingTitle;
                }} />
-
              <PatchStateButton patchState={patch.state} save={saveState} />
            </div>
          </div>
        {:else}
@@ -619,7 +614,6 @@
                <Icon
                  name="pen"
                  onclick={() => (editingTitle = !editingTitle)} />
-
                <PatchStateButton patchState={patch.state} save={saveState} />
              </div>
            {/if}
          </div>
@@ -628,7 +622,15 @@
      <Border variant="ghost" styleGap="0">
        <div class="metadata-section" style:min-width="8rem">
          <div class="metadata-section-title">Status</div>
-
          <PatchStateBadge state={patch.state} />
+
          <PatchStateButton
+
            selectedState={patch.state}
+
            onSelect={newState => {
+
              void saveState(newState);
+
              if (status !== undefined && newState.status !== status) {
+
                status = undefined;
+
                void loadPatches(status);
+
              }
+
            }} />
        </div>

        <div class="metadata-divider"></div>
modified src/views/repo/Patches.svelte
@@ -104,8 +104,8 @@
    font-size: var(--font-size-medium);
    display: flex;
    align-items: center;
-
    min-height: 40px;
-
    margin-bottom: 0.5rem;
+
    min-height: 2.5rem;
+
    margin-bottom: 1rem;
  }
</style>

@@ -174,19 +174,20 @@
    </div>

    <div class="list">
-
      {#each searchResults as result}
-
        <PatchTeaser
-
          focussed={searchResults.length === 1 && searchInput !== ""}
-
          patch={result.obj.patch}
-
          rid={repo.rid}
-
          {status} />
-
      {/each}
-

-
      {#if searchResults.length === 0}
-
        <Border
-
          variant="ghost"
-
          styleAlignItems="center"
-
          styleJustifyContent="center">
+
      <Border
+
        variant={searchResults.length === 1 && searchInput !== ""
+
          ? "secondary"
+
          : "float"}
+
        styleFlexDirection="column"
+
        styleOverflow="hidden"
+
        styleGap="2px"
+
        styleAlignItems="center"
+
        styleJustifyContent="center">
+
        {#each searchResults as result}
+
          <PatchTeaser patch={result.obj.patch} rid={repo.rid} {status} />
+
        {/each}
+

+
        {#if searchResults.length === 0}
          <div
            class="global-flex"
            style:height="74px"
@@ -200,8 +201,8 @@
              {/if}
            </div>
          </div>
-
        </Border>
-
      {/if}
+
        {/if}
+
      </Border>
    </div>
  </div>
</Layout>
modified tests/e2e/repo/issue.spec.ts
@@ -10,9 +10,7 @@ test("navigate single issue", async ({ page }) => {
test("correct order of threads", async ({ page }) => {
  await page.goto("/repos");
  await page.getByRole("button", { name: "cobs" }).click();
-
  await page
-
    .getByRole("button", { name: "This title has **markdown**" })
-
    .click();
+
  await page.getByText("This title has **markdown**").click();
  const body = page.locator(".issue-body");
  await expect(body.getByText("This is a description")).toBeVisible();

@@ -43,10 +41,10 @@ test("creation of top level comments", async ({ page }) => {
    );
  await page.getByRole("button", { name: "icon-checkmark" }).click();
  await expect(
-
    page.getByText("Make sure that comment creation is working").last(),
-
  ).toBeVisible();
-
  await expect(
-
    page.getByRole("button", { name: "icon-issue Make sure that" }),
+
    page.getByRole("button", {
+
      name: "icon-issue Make sure that comment creation is working avatar palm opened 8faf9dc 6 months ago",
+
      exact: true,
+
    }),
  ).toBeVisible();
  await expect(
    page
modified tests/e2e/repos.spec.ts
@@ -3,9 +3,7 @@ import { expect, test } from "@tests/support/fixtures.js";
test("navigate to repo issues", async ({ page }) => {
  await page.goto("/repos");
  await page.getByRole("button", { name: "cobs" }).click();
-
  await page
-
    .getByRole("button", { name: "This title has **markdown**" })
-
    .click();
+
  await page.getByText("This title has **markdown**").click();
  await expect(
    page.getByText("This title has **markdown**").nth(1),
  ).toBeVisible();