Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
Issue view follow-ups
Open rudolfs opened 1 year ago
  • Add markdown preview to create issue view
  • Add Issue state/label/assignee section in single issue view

check

👉 Workflow runs 👉 Branch on GitHub

7 files changed +289 -60 54ce1864 921a26dc
modified src/components/Border.svelte
@@ -8,6 +8,7 @@
  export let styleMinHeight: string | undefined = undefined;
  export let styleWidth: string | undefined = undefined;
  export let styleCursor: "default" | "pointer" = "default";
+
  export let styleGap: string = "0.5rem";

  $: style =
    `--local-button-color-1: var(--color-fill-${variant});` +
@@ -65,7 +66,6 @@
    grid-area: p3-3;
    display: flex;
    align-items: center;
-
    gap: 0.5rem;
  }
  .p3-4 {
    grid-area: p3-4;
@@ -172,7 +172,7 @@

  <div class="pixel p3-1"></div>
  <div class="pixel p3-2"></div>
-
  <div class="pixel p3-3" style:padding={stylePadding}>
+
  <div class="pixel p3-3" style:padding={stylePadding} style:gap={styleGap}>
    <slot />
  </div>
  <div class="pixel p3-4"></div>
modified src/components/Icon.svelte
@@ -10,18 +10,22 @@
    | "checkmark"
    | "chevron-right"
    | "copy"
+
    | "cross"
    | "dashboard"
    | "delegate"
    | "diff"
+
    | "eye"
    | "file"
    | "inbox"
    | "issue"
    | "lock"
+
    | "markdown"
    | "moon"
    | "more-vertical"
    | "offline"
    | "online"
    | "patch"
+
    | "pen"
    | "plus"
    | "repo"
    | "revision"
@@ -105,6 +109,23 @@
    <path d="M2.5 6H3.5L3.5 13H2.5V6Z" />
    <path d="M10.5 12H11.5V13H10.5V12Z" />
    <path d="M10.5 6L11.5 6V9H10.5L10.5 6Z" />
+
  {:else if name === "cross"}
+
    <path d="M5.00003 11V12H4.00003L4.00003 11H5.00003Z" />
+
    <path d="M6.00003 10L6.00003 11H5.00003L5.00003 10H6.00003Z" />
+
    <path d="M7.00003 9V10H6.00003L6.00003 9L7.00003 9Z" />
+
    <path d="M8.00003 7V8H7.00003V7H8.00003Z" />
+
    <path d="M10 6V7H9.00003V6H10Z" />
+
    <path d="M11 5V6L10 6V5H11Z" />
+
    <path d="M12 4V5L11 5V4L12 4Z" />
+
    <path d="M10 10V11H11V10H10Z" />
+
    <path d="M11 11V12H12V11H11Z" />
+
    <path d="M9.00003 9V10H10V9H9.00003Z" />
+
    <path d="M8.00003 8L8.00003 9H9.00003V8L8.00003 8Z" />
+
    <path d="M8.00003 7V8L9.00003 8V7L8.00003 7Z" />
+
    <path d="M7.00003 7L7.00003 9H8.00003L8.00003 7H7.00003Z" />
+
    <path d="M6.00003 6L6.00003 7H7.00003L7.00003 6H6.00003Z" />
+
    <path d="M5.00003 5V6L6.00003 6V5L5.00003 5Z" />
+
    <path d="M4.00003 4L4.00003 5L5.00003 5L5.00003 4L4.00003 4Z" />
  {:else if name === "dashboard"}
    <path d="M2 11H14V12H2V11Z" />
    <path d="M2 9H3V11H2L2 9Z" />
@@ -144,6 +165,29 @@
    <path d="M7 4H8V9H7V4Z" />
    <path d="M5 6H10V7H5V6Z" />
    <path d="M5 10H10V11H5V10Z" />
+
  {:else if name === "eye"}
+
    <path d="M10 5L8.00002 5V4L10 4V5Z" />
+
    <path d="M6.00002 11L8.00002 11L8.00002 12H6.00002L6.00002 11Z" />
+
    <path d="M7.00002 7L9.00002 7V6L7.00002 6V7Z" />
+
    <path d="M9.00001 9L7.00001 9V10L9.00001 10V9Z" />
+
    <path d="M4.00002 6V7H3.00002L3.00002 6H4.00002Z" />
+
    <path d="M12 10V9H13V10H12Z" />
+
    <path d="M12 6L10 6V5L12 5V6Z" />
+
    <path d="M4.00002 10L6.00002 10V11H4.00002L4.00002 10Z" />
+
    <path d="M6.00002 6L4.00002 6L4.00002 5L6.00002 5V6Z" />
+
    <path d="M10 10H12V11L10 11L10 10Z" />
+
    <path d="M12 7V6H13V7H12Z" />
+
    <path d="M4.00002 9V10H3.00002L3.00002 9H4.00002Z" />
+
    <path d="M8.00002 5L6.00002 5V4L8.00002 4V5Z" />
+
    <path d="M8.00002 11L10 11V12H8.00002L8.00002 11Z" />
+
    <path d="M6.00002 8V7L7.00002 7V8L6.00002 8Z" />
+
    <path d="M10 8V9H9.00001L9.00002 8H10Z" />
+
    <path d="M9.00002 8L9.00002 7L10 7V8H9.00002Z" />
+
    <path d="M7.00002 8L7.00001 9L6.00002 9V8L7.00002 8Z" />
+
    <path d="M14 7V8H13V7L14 7Z" />
+
    <path d="M2.00002 9L2.00002 8H3.00002V9L2.00002 9Z" />
+
    <path d="M3.00002 7L3.00002 8H2.00002L2.00002 7H3.00002Z" />
+
    <path d="M13 9V8H14V9L13 9Z" />
  {:else if name === "file"}
    <path d="M10 4H11V5H10V4Z" />
    <path d="M11 5L12 5V6H11V5Z" />
@@ -198,6 +242,23 @@
    <path d="M13 7H14V13H13V7Z" />
    <path d="M3 6H13V7H3L3 6Z" />
    <path d="M7 8H9V11H7V8Z" />
+
  {:else if name === "markdown"}
+
    <path d="M2 4.00003H3V12H2V4.00003Z" />
+
    <path d="M3 7.00003H4V12H3V7.00003Z" />
+
    <path d="M9 4.00003H8V8.00003H9V4.00003Z" />
+
    <path d="M8 7.00003H7V9.00003H8V7.00003Z" />
+
    <path d="M11 11H12V12H11V11Z" />
+
    <path d="M9 10H13V11H9V10Z" />
+
    <path d="M8 9.00003H14V10H8V9.00003Z" />
+
    <path d="M10 4.00003H12V10H10V4.00003Z" />
+
    <path d="M4 5.00003H5V7.00003H4V5.00003Z" />
+
    <path d="M5 7.00003H6V8.00003H5V7.00003Z" />
+
    <path d="M5 6.00003H6V7.00003H5V6.00003Z" />
+
    <path d="M6 5.00003H7V7.00003H6V5.00003Z" />
+
    <path d="M7 4.00003H8V6.00003H7V4.00003Z" />
+
    <path d="M8 11H9V12H8V11Z" />
+
    <path d="M7 10H8V12H7V10Z" />
+
    <path d="M3 4.00003H4V6.00003H3V4.00003Z" />
  {:else if name === "moon"}
    <path d="M4 3H6V4H4V3Z" />
    <path d="M3 4L4 4L4 6H3V4Z" />
@@ -300,6 +361,40 @@
    <path d="M7 4H8V9H7V4Z" />
    <path d="M5 6H10V7H5V6Z" />
    <path d="M5 10H10V11H5V10Z" />
+
  {:else if name === "pen"}
+
    <path d="M13 4.99998H14V5.99999H13V4.99998Z" />
+
    <path d="M2 13H3V14H2V13Z" />
+
    <path d="M10 1.99998H11V2.99998H10V1.99998Z" />
+
    <path d="M11 2.99998L12 2.99998V3.99998H11V2.99998Z" />
+
    <path d="M12 3.99998L13 3.99998V4.99998H12V3.99998Z" />
+
    <path d="M6 5.99998H7V6.99998H6V5.99998Z" />
+
    <path d="M7 4.99998H8V5.99998H7V4.99998Z" />
+
    <path d="M8 3.99998H9V4.99998H8V3.99998Z" />
+
    <path d="M9 4.99998L10 4.99998V5.99998H9V4.99998Z" />
+
    <path d="M10 5.99998H11V6.99998H10V5.99998Z" />
+
    <path d="M9 2.99998L10 2.99998V3.99998H9V2.99998Z" />
+
    <path d="M12 5.99998L13 5.99999V6.99998H12V5.99998Z" />
+
    <path d="M5 6.99999L6 6.99998L6 7.99999H5V6.99999Z" />
+
    <path d="M9 8.99998H10V9.99998H9V8.99998Z" />
+
    <path d="M10 7.99998H11V8.99998H10V7.99998Z" />
+
    <path d="M11 6.99998H12V7.99998H11V6.99998Z" />
+
    <path d="M4 7.99998L5 7.99999V8.99998H4V7.99998Z" />
+
    <path d="M8 9.99998H9L9 11H8V9.99998Z" />
+
    <path d="M3 8.99998H4V9.99998H3V8.99998Z" />
+
    <path d="M7 11H8V12H7V11Z" />
+
    <path d="M2 9.99998H3V11H2V9.99998Z" />
+
    <path d="M6 12H7V13H6V12Z" />
+
    <path d="M4 12H5V13H4V12Z" />
+
    <path d="M4 11H5V12H4V11Z" />
+
    <path d="M3 11H4V12H3V11Z" />
+
    <path d="M3 9.99998H4V11H3V9.99998Z" />
+
    <path d="M5 12H6V13H5V12Z" />
+
    <path d="M4 13H5V14H4V13Z" />
+
    <path d="M5 13H6L6 14H5V13Z" />
+
    <path d="M2 12H3V13H2V12Z" />
+
    <path d="M2 11H3V12H2V11Z" />
+
    <path d="M3 12H4V13H3V12Z" />
+
    <path d="M3 13H4V14H3V13Z" />
  {:else if name === "plus"}
    <path d="M7.00002 2H9.00002V14H7.00002V2Z" />
    <path d="M14 7V9L2.00002 9L2.00002 7L14 7Z" />
modified src/components/Markdown.svelte
@@ -321,14 +321,14 @@
  }

  .markdown :global(ol) {
-
    line-height: 1.625;
+
    line-height: 1.625rem;
    list-style-type: decimal;
    margin-bottom: 1rem;
    padding-left: 1.5rem;
  }

  .markdown :global(ul) {
-
    line-height: 1.625;
+
    line-height: 1.625rem;
    list-style-type: inherit;
    padding-left: 1.25rem;
    margin-bottom: 1rem;
modified src/components/OutlineButton.svelte
@@ -1,6 +1,7 @@
<script lang="ts">
  export let variant: "primary" | "secondary" | "ghost";
  export let onclick: (() => void) | undefined = undefined;
+
  export let disabled: boolean = false;

  $: style =
    `--button-color-1: var(--color-fill-${variant});` +
@@ -107,93 +108,113 @@
    grid-area: p5-5;
  }

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

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

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

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

-
  .container:hover .p5-3 {
+
  .container:hover:not(.disabled) .p5-3 {
    background-color: var(--button-color-1);
  }

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

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

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

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

+
  .container.disabled {
+
    color: var(--color-foreground-disabled);
+
  }
+

+
  .disabled .p1-3,
+
  .disabled .p2-2,
+
  .disabled .p2-3,
+
  .disabled .p2-4,
+
  .disabled .p3-1,
+
  .disabled .p3-2,
+
  .disabled .p3-3,
+
  .disabled .p3-4,
+
  .disabled .p3-5,
+
  .disabled .p4-2,
+
  .disabled .p4-3,
+
  .disabled .p4-4,
+
  .disabled .p5-3 {
+
    background-color: var(--color-fill-ghost);
+
  }
+

  .container {
    height: 32px;
    cursor: pointer;
@@ -218,7 +239,14 @@
</style>

<!-- svelte-ignore a11y-click-events-have-key-events -->
-
<div class="container" {onclick} role="button" tabindex="0" {style}>
+
<div
+
  class="container"
+
  style:cursor={!disabled ? "pointer" : "default"}
+
  class:disabled
+
  onclick={!disabled ? onclick : undefined}
+
  role="button"
+
  tabindex="0"
+
  {style}>
  <div class="pixel p1-1"></div>
  <div class="pixel p1-2"></div>
  <div class="pixel p1-3"></div>
modified src/components/Textarea.svelte
@@ -6,10 +6,8 @@
  export let value: string | undefined = undefined;
  export let placeholder: string | undefined = undefined;
  export let focus: boolean = false;
-
  // If `false` we automatically grow the textarea height.
-
  // If `true` we show a resize handle on the lower right-hand side of the
-
  // textarea to allow resizing the textarea manually.
-
  export let resizable: boolean = false;
+
  export let size: "grow" | "resizable" | "fixed-height" = "grow";
+
  export let styleMinHeight: string | undefined = undefined;

  // Defaulting selectionStart and selectionEnd to 0, since no full support yet.
  export let selectionStart: number = 0;
@@ -21,7 +19,7 @@
  // We either auto-grow the textarea, or allow the user to resize it. These
  // options are mutually exclusive because a user resized textarea would
  // automatically shrink upon text input otherwise.
-
  $: if (textareaElement && !resizable) {
+
  $: if (textareaElement && size === "grow") {
    // React to changes to the textarea content.
    // eslint-disable-next-line @typescript-eslint/no-unused-expressions
    value;
@@ -66,11 +64,6 @@
    outline: none;
  }

-
  .resizable {
-
    resize: vertical;
-
    overflow: scroll;
-
  }
-

  textarea::-webkit-scrollbar-corner {
    background-color: transparent;
  }
@@ -87,14 +80,21 @@
  }
</style>

-
<Border variant={focussed ? "secondary" : "ghost"} styleWidth="100%">
+
<Border
+
  variant={focussed ? "secondary" : "ghost"}
+
  styleWidth="100%"
+
  {styleMinHeight}>
  <textarea
+
    style:min-height={styleMinHeight}
    tabindex="0"
    bind:this={textareaElement}
    bind:value
    aria-label="textarea-comment"
    class="txt-small"
-
    class:resizable
+
    style:resize={size === "resizable" ? "vertical" : undefined}
+
    style:overflow={size === "resizable" || size === "fixed-height"
+
      ? "scroll"
+
      : undefined}
    {placeholder}
    on:change
    on:click
modified src/views/repo/CreateIssue.svelte
@@ -19,6 +19,7 @@
  import OutlineButton from "@app/components/OutlineButton.svelte";
  import TextInput from "@app/components/TextInput.svelte";
  import Textarea from "@app/components/Textarea.svelte";
+
  import Markdown from "@app/components/Markdown.svelte";

  export let repo: RepoInfo;
  export let issues: Issue[];
@@ -26,6 +27,7 @@

  let title: string = "";
  let description: string = "";
+
  let preview: boolean = false;

  const labels: string[] = [];
  const assignees: Author[] = [];
@@ -52,8 +54,8 @@
    font-weight: var(--font-weight-medium);
    -webkit-user-select: text;
    user-select: text;
-
    margin-bottom: 1rem;
    margin-top: 0.35rem;
+
    margin-bottom: 1rem;
  }
  .issue-teaser {
    max-width: 11rem;
@@ -68,6 +70,13 @@
  }
  .content {
    padding: 0 1rem 1rem 1rem;
+
    height: calc(100% - 8rem);
+
  }
+
  .body {
+
    background-color: var(--color-background-float);
+
    padding: 1rem;
+
    min-height: calc(100% + 2px);
+
    clip-path: var(--2px-corner-fill);
  }
</style>

@@ -140,27 +149,60 @@
  </svelte:fragment>

  <div class="content">
-
    <div class="title">
-
      <TextInput placeholder="Title" autofocus bind:value={title} />
-
    </div>
-
    <Textarea placeholder="Description" bind:value={description} />
+
    {#if preview}
+
      <div class="title">
+
        <InlineTitle content={title} fontSize="medium" />
+
      </div>
+
    {:else}
+
      <div style:margin-bottom="0.35rem">
+
        <TextInput placeholder="Title" autofocus bind:value={title} />
+
      </div>
+
    {/if}
+
    {#if preview}
+
      <div class="txt-small body">
+
        {#if description.trim() === ""}
+
          <span class="txt-missing">No description.</span>
+
        {:else}
+
          <Markdown rid={repo.rid} content={description} breaks />
+
        {/if}
+
      </div>
+
    {:else}
+
      <Textarea
+
        placeholder="Description"
+
        bind:value={description}
+
        size="fixed-height"
+
        styleMinHeight="100%" />
+
    {/if}
    <div
      class="global-flex"
-
      style:justify-content="flex-end"
+
      style:justify-content="space-between"
+
      style:padding-bottom="1.5rem"
      style:margin-top="1.5rem">
      <OutlineButton
        variant="ghost"
        onclick={() => {
          window.history.back();
        }}>
-
        Cancel
+
        <Icon name="cross" />Discard
      </OutlineButton>
-
      <Button
-
        variant="ghost"
-
        disabled={title.length === 0}
-
        onclick={createIssue}>
-
        Save
-
      </Button>
+
      <div class="global-flex">
+
        <div class="global-flex txt-small txt-missing">
+
          <Icon name="markdown" />
+
          Markdown is supported.
+
        </div>
+
        <OutlineButton
+
          variant="ghost"
+
          disabled={title.length === 0}
+
          onclick={() => (preview = !preview)}>
+
          <Icon name={preview ? "pen" : "eye"} />{preview ? "Edit" : "Preview"}
+
        </OutlineButton>
+
        <Button
+
          variant="ghost"
+
          disabled={title.length === 0}
+
          onclick={createIssue}>
+
          <Icon name="checkmark" />Save
+
        </Button>
+
      </div>
    </div>
  </div>
</Layout>
modified src/views/repo/Issue.svelte
@@ -3,6 +3,8 @@
  import type { Issue } from "@bindings/Issue";
  import type { RepoInfo } from "@bindings/RepoInfo";

+
  import capitalize from "lodash/capitalize";
+

  import { formatTimestamp, formatOid, issueStatusColor } from "@app/lib/utils";

  import Border from "@app/components/Border.svelte";
@@ -45,10 +47,30 @@
  .content {
    padding: 0 1rem 1rem 1rem;
  }
-

  .body {
    background-color: var(--color-background-float);
    padding: 1rem;
+
    margin-top: 1rem;
+
    clip-path: var(--2px-corner-fill);
+
  }
+
  .divider {
+
    width: 2px;
+
    background-color: var(--color-fill-ghost);
+
    height: calc(100% + 8px);
+
    top: -2px;
+
    position: relative;
+
  }
+
  .section {
+
    padding: 0.5rem;
+
    font-size: var(--font-size-small);
+
    display: flex;
+
    flex-direction: column;
+
    align-items: flex-start;
+
    height: 100%;
+
  }
+
  .section-title {
+
    margin-bottom: 0.5rem;
+
    color: var(--color-foreground-dim);
  }
</style>

@@ -124,14 +146,56 @@
    <div class="title">
      <InlineTitle content={issue.title} fontSize="medium" />
    </div>
+

+
    <Border variant="ghost" styleGap="0">
+
      <div class="section" style:min-width="8rem">
+
        <div class="section-title">Status</div>
+
        <div
+
          class="global-counter txt-small"
+
          style:width="fit-content"
+
          style:color="var(--color-foreground-match-background)"
+
          style:background-color={issueStatusColor[issue.state.status]}>
+
          {capitalize(issue.state.status)}
+
        </div>
+
      </div>
+

+
      <div class="divider"></div>
+

+
      <div class="section" style:flex="1">
+
        <div class="section-title">Labels</div>
+
        <div class="global-flex" style:flex-wrap="wrap">
+
          {#each issue.labels as label}
+
            <div class="global-counter txt-small">{label}</div>
+
          {:else}
+
            <span class="txt-missing">No labels.</span>
+
          {/each}
+
        </div>
+
      </div>
+

+
      <div class="divider"></div>
+

+
      <div class="section" style:flex="1">
+
        <div class="section-title">Assignees</div>
+
        <div class="global-flex" style:flex-wrap="wrap">
+
          {#each issue.assignees as assignee}
+
            <NodeId
+
              nodeId={assignee.did.replace("did:key:", "")}
+
              alias={assignee.alias} />
+
          {:else}
+
            <span class="txt-missing">Not assigned to anyone.</span>
+
          {/each}
+
        </div>
+
      </div>
+
    </Border>
+

    <div class="txt-small body">
-
      {#if issue.discussion[0].edits.slice(-1)[0].body !== ""}
+
      {#if issue.discussion[0].edits.slice(-1)[0].body.trim() === ""}
+
        <span class="txt-missing">No description.</span>
+
      {:else}
        <Markdown
          rid={repo.rid}
          breaks
          content={issue.discussion[0].edits.slice(-1)[0].body} />
-
      {:else}
-
        <span class="txt-missing">No description.</span>
      {/if}
      <div class="global-flex txt-small" style:margin-top="1.5rem">
        <NodeId