Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
.. AddRepoButton.svelte AnnounceSwitch.svelte AppSidebar.svelte AssigneeInput.svelte BadgeCounterSwitch.svelte Button.svelte Changes.svelte Changeset.svelte CheckoutPatchButton.svelte CheckoutRepoButton.svelte Clipboard.svelte CobCacheWarning.svelte CobCommitTeaser.svelte CodeFontSwitch.svelte Command.svelte Comment.svelte CommentToggleInput.svelte CommitsContainer.svelte CompactCommitAuthorship.svelte ConfirmClear.svelte CopyableId.svelte Diff.svelte DiffStatBadge.svelte Discussion.svelte DropdownList.svelte DropdownListItem.svelte EditableTitle.svelte ExtendedTextarea.svelte ExternalLink.svelte FileBlock.svelte FileDiff.svelte FileTreeFile.svelte FileTreeFolder.svelte FontSizeSwitch.svelte FullscreenModalPortal.svelte FullWindowError.svelte FuzzySearch.svelte HoverPopover.svelte Icon.svelte Id.svelte IdentityButton.svelte InboxList.svelte InfiniteScrollSentinel.svelte InlineTitle.svelte IssueStateButton.svelte IssueTeaser.svelte IssueTimeline.svelte JobCob.svelte Label.svelte LabelInput.svelte Markdown.svelte NewPatchButton.svelte NodeId.svelte NodeStatusButton.svelte NotificationsByRepo.svelte NotificationTeaser.svelte PatchMetadata.svelte PatchStateButton.svelte PatchTeaser.svelte PatchTimeline.svelte Path.svelte Popover.svelte PreviewSwitch.svelte RadicleWordmark.svelte Reactions.svelte ReactionSelector.svelte RepoAvatar.svelte RepoHeader.svelte Review.svelte Revision.svelte RevisionBadges.svelte RevisionReviews.svelte Revisions.svelte ScrollArea.svelte SidebarRepoList.svelte Spinner.svelte Textarea.svelte TextInput.svelte ThemeSwitch.svelte Thread.svelte Topbar.svelte Tree.svelte UpdateSwitch.svelte UserAvatar.svelte VerdictBadge.svelte VerdictButton.svelte VisibilityBadge.svelte
radicle-desktop src components Textarea.svelte
<script lang="ts">
  import type {
    ClipboardEventHandler,
    FormEventHandler,
  } from "svelte/elements";

  import { onMount, tick } from "svelte";

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

  interface Props {
    draggingOver?: boolean;
    borderVariant?: "float" | "ghost";
    onpaste?: ClipboardEventHandler<HTMLTextAreaElement>;
    focus?: boolean;
    oninput?: FormEventHandler<HTMLTextAreaElement>;
    onkeypress?: FormEventHandler<HTMLTextAreaElement>;
    placeholder?: string;
    selectionEnd?: number;
    selectionStart?: number;
    size?: "grow" | "resizable" | "fixed-height";
    styleMinHeight?: string;
    styleAlignItems?: string;
    stylePadding?: string;
    submit: () => Promise<void>;
    value?: string;
  }

  /* eslint-disable prefer-const */
  let {
    draggingOver,
    borderVariant = "float",
    focus = false,
    onpaste,
    oninput,
    onkeypress,
    placeholder = undefined,
    // Defaulting selectionStart and selectionEnd to 0, since no full support yet.
    selectionEnd = $bindable(0),
    selectionStart = $bindable(0),
    size = "grow",
    styleAlignItems = undefined,
    styleMinHeight = undefined,
    stylePadding = "0.75rem",
    submit,
    value = $bindable(undefined),
  }: Props = $props();
  /* eslint-enable prefer-const */

  const borderColors: Record<NonNullable<Props["borderVariant"]>, string> = {
    ghost: "var(--color-border-subtle)",
    float: "var(--color-border-subtle)",
  };

  let textareaElement: HTMLTextAreaElement | undefined = $state(undefined);
  let focussed = $state(false);

  onMount(() => {
    if (textareaElement) {
      // The selectionchange event listener doesn't modify the selection on Enter.
      textareaElement.addEventListener("keydown", (event: KeyboardEvent) => {
        if (event.key === "Enter") {
          selectionStart += 1;
          selectionEnd += 1;
        }
      });
      textareaElement.addEventListener("selectionchange", (event: Event) => {
        if (
          event.target &&
          "selectionStart" in event.target &&
          "selectionEnd" in event.target
        ) {
          selectionStart = event.target.selectionStart as number;
          selectionEnd = event.target.selectionEnd as number;
        }
      });
    }
  });

  // 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.
  $effect(() => {
    if (textareaElement && size === "grow") {
      // React to changes to the textarea content.
      // eslint-disable-next-line @typescript-eslint/no-unused-expressions
      value;

      // Reset height to 0px on every value change so that the textarea
      // immediately shrinks when all text is deleted.
      textareaElement.style.height = `0px`;
      textareaElement.style.height = `${textareaElement.scrollHeight}px`;
    }
  });

  $effect(() => {
    if (textareaElement && focus) {
      textareaElement.focus();
      focus = false;
    }
  });

  $effect(() => {
    void tick().then(() => {
      if (textareaElement && focus) {
        textareaElement.setSelectionRange(selectionStart, selectionEnd);
        textareaElement.focus();
      }
    });
  });

  function handleKeydown(event: KeyboardEvent) {
    event.stopPropagation();
    const auxiliarKey = utils.isMac() ? event.metaKey : event.ctrlKey;
    if (auxiliarKey && event.key === "Enter") {
      void submit();
    }
    if (event.key === "Escape") {
      textareaElement?.blur();
    }
  }
</script>

<style>
  textarea {
    background-color: transparent;
    border: 0;
    color: var(--color-text-secondary);
    font-family: inherit;
    height: 100%;
    width: 100%;
    min-height: 6.375rem;
    resize: none;
    overflow: hidden;
    outline: none;
    line-height: 1rem;
  }

  textarea::-webkit-scrollbar-corner {
    background-color: transparent;
  }

  textarea::-webkit-resizer {
    background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAMAAAAolt3jAAAAAXNSR0IB2cksfwAAAAlwSFlzAAAWJQAAFiUBSVIk8AAAAD9QTFRFAAAAZWZmZmZmZmVmZWVmwsLBwsLCZ2ZmwsPCZmdlZWZnwcLBZmZkYGJjw8LDwsPBZmZnZWZkZ2ZkwMDBWFtcNbXb2AAAABV0Uk5TAP///////////////////////1H/YDRrSAAAAFBJREFUeJxVjUESgCAMA2mqAoqK6P/f6kzjIXIos5NumpI8g5LbpJnNQvDl52mWUYTquqnXwstshpHaTi+o+hHXccoKmHVW9yvIxv218ntivmOYAWpLfqaRAAAAAElFTkSuQmCC);
    background-size: 7px;
    background-repeat: no-repeat;
    background-position: bottom 1px right 1px;
  }

  textarea::placeholder {
    color: var(--color-text-secondary);
  }

  textarea::-webkit-scrollbar {
    width: 7px;
    height: 7px;
  }

  textarea::-webkit-scrollbar-track {
    background-color: transparent;
  }

  textarea::-webkit-scrollbar-thumb {
    background-color: var(--color-surface-subtle);
    border-radius: var(--border-radius-md);
  }

  .dragover {
    position: absolute;
    opacity: 0.5;
    display: flex;
    justify-content: center;
    align-items: center;
    width: 100%;
    height: 100%;
    background-color: var(--color-surface-canvas);
  }
</style>

<div
  style:border={`1px solid ${focussed ? "var(--color-border-brand)" : borderColors[borderVariant]}`}
  style:border-radius="var(--border-radius-sm)"
  style:display="flex"
  style:gap="0.5rem"
  style:align-items={styleAlignItems}
  style:background-color="var(--color-surface-base)"
  style:position="relative"
  style:width="100%"
  style:min-height={styleMinHeight}>
  <textarea
    style:min-height={styleMinHeight}
    style:padding={stylePadding}
    tabindex="0"
    bind:this={textareaElement}
    bind:value
    aria-label="textarea-comment"
    class="txt-body-m-regular"
    style:resize={size === "resizable" ? "vertical" : undefined}
    style:overflow={size === "resizable" || size === "fixed-height"
      ? "scroll"
      : undefined}
    {placeholder}
    {onpaste}
    {oninput}
    {onkeypress}
    onfocus={() => (focussed = true)}
    onblur={() => (focussed = false)}
    onkeydown={handleKeydown}>
  </textarea>
  {#if draggingOver}
    <div class="txt-body-m-regular dragover">
      Drop files to add them as embeds. Embeds are limited to 10MB.
    </div>
  {/if}
</div>