Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Clean up components
Rūdolfs Ošiņš committed 3 years ago
commit e04c792cafe1a47006b238fa848c9ef97c187c2f
parent 9fae5ebb33d85ccda92ca965d17c833e4fadd507
24 files changed +532 -709
deleted src/App/ColorPalette.svelte
@@ -1,156 +0,0 @@
-
<script lang="ts">
-
  import Modal from "@app/components/Modal.svelte";
-

-
  function extractCssVariables(variableName: string) {
-
    return Array.from(document.styleSheets)
-
      .filter(
-
        sheet =>
-
          sheet.href === null || sheet.href.startsWith(window.location.origin),
-
      )
-
      .reduce<string[]>(
-
        (acc, sheet) =>
-
          (acc = [
-
            ...acc,
-
            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-
            // @ts-ignore
-
            ...Array.from(sheet.cssRules).reduce(
-
              (def, rule) =>
-
                // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-
                // @ts-ignore
-
                (def =
-
                  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-
                  // @ts-ignore
-
                  rule.selectorText === ":root"
-
                    ? [
-
                        ...def,
-
                        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-
                        // @ts-ignore
-
                        ...Array.from(rule.style).filter(name =>
-
                          // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-
                          // @ts-ignore
-
                          name.startsWith(variableName),
-
                        ),
-
                      ]
-
                    : def),
-
              [],
-
            ),
-
          ]),
-
        [],
-
      );
-
  }
-

-
  // rg "\--color-\w*(-\d)*" -o --no-line-number --no-filename -g "\!public/colors.css" -g "\!src/ColorPalette.svelte" | sort | uniq | jq -sRM 'split("\n")[:-1]'
-
  const usedColors = [
-
    "--color-background",
-
    "--color-caution",
-
    "--color-caution-2",
-
    "--color-caution-3",
-
    "--color-caution-6",
-
    "--color-foreground",
-
    "--color-foreground-1",
-
    "--color-foreground-2",
-
    "--color-foreground-3",
-
    "--color-foreground-4",
-
    "--color-foreground-5",
-
    "--color-foreground-6",
-
    "--color-negative",
-
    "--color-negative-1",
-
    "--color-negative-2",
-
    "--color-negative-3",
-
    "--color-negative-4",
-
    "--color-negative-5",
-
    "--color-negative-6",
-
    "--color-positive",
-
    "--color-positive-1",
-
    "--color-positive-2",
-
    "--color-positive-3",
-
    "--color-positive-6",
-
    "--color-primary",
-
    "--color-primary-3",
-
    "--color-primary-5",
-
    "--color-secondary",
-
    "--color-secondary-1",
-
    "--color-secondary-2",
-
    "--color-secondary-3",
-
    "--color-secondary-5",
-
    "--color-secondary-6",
-
    "--color-tertiary",
-
    "--color-tertiary-1",
-
    "--color-tertiary-2",
-
    "--color-tertiary-3",
-
    "--color-tertiary-6",
-
  ];
-

-
  const colors = extractCssVariables("--color").filter(c => {
-
    return !c.startsWith("--color-prettylights-syntax");
-
  });
-

-
  const colorGroups = [
-
    ...new Set(
-
      colors.map(color => {
-
        const match = color.match(/--color-(\w*)-?/);
-
        if (match) {
-
          return match[1];
-
        } else {
-
          return "";
-
        }
-
      }),
-
    ),
-
  ];
-

-
  let checkers = false;
-
</script>
-

-
<style>
-
  .checkers {
-
    background: repeating-conic-gradient(#88888833 0% 25%, transparent 0% 50%)
-
      50% / 20px 20px;
-
    border-radius: 1rem;
-
  }
-

-
  .container {
-
    display: flex;
-
    margin: 0;
-
    padding: 0;
-
  }
-

-
  .color {
-
    width: 3rem;
-
    height: 3rem;
-
    border-radius: 0.5rem;
-
    outline-style: solid !important;
-
    outline-color: #88888899 !important;
-
    outline-offset: 0.3rem;
-
    margin: 1rem;
-
  }
-

-
  .unused {
-
    outline-style: dotted !important;
-
    outline-color: #55555555 !important;
-
  }
-
</style>
-

-
<Modal closeAction={false}>
-
  <!-- svelte-ignore a11y-click-events-have-key-events -->
-
  <div slot="body">
-
    <div class="container" on:click={() => (checkers = !checkers)}>
-
      <div class:checkers>
-
        {#each colorGroups as colorGroup}
-
          <div style:display="flex">
-
            {#each colors.filter(color => {
-
              return color.match(`--color-${colorGroup}`);
-
            }) as color}
-
              <div style:display="inline-flex">
-
                <div
-
                  class:unused={!usedColors.includes(color)}
-
                  title={color}
-
                  class="color"
-
                  style:background-color={`var(${color})`} />
-
              </div>
-
            {/each}
-
          </div>
-
        {/each}
-
      </div>
-
    </div>
-
  </div>
-
</Modal>
added src/App/ColorPaletteModal.svelte
@@ -0,0 +1,156 @@
+
<script lang="ts">
+
  import Modal from "@app/components/Modal.svelte";
+

+
  function extractCssVariables(variableName: string) {
+
    return Array.from(document.styleSheets)
+
      .filter(
+
        sheet =>
+
          sheet.href === null || sheet.href.startsWith(window.location.origin),
+
      )
+
      .reduce<string[]>(
+
        (acc, sheet) =>
+
          (acc = [
+
            ...acc,
+
            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+
            // @ts-ignore
+
            ...Array.from(sheet.cssRules).reduce(
+
              (def, rule) =>
+
                // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+
                // @ts-ignore
+
                (def =
+
                  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+
                  // @ts-ignore
+
                  rule.selectorText === ":root"
+
                    ? [
+
                        ...def,
+
                        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+
                        // @ts-ignore
+
                        ...Array.from(rule.style).filter(name =>
+
                          // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+
                          // @ts-ignore
+
                          name.startsWith(variableName),
+
                        ),
+
                      ]
+
                    : def),
+
              [],
+
            ),
+
          ]),
+
        [],
+
      );
+
  }
+

+
  // rg "\--color-\w*(-\d)*" -o --no-line-number --no-filename -g "\!public/colors.css" -g "\!src/ColorPalette.svelte" | sort | uniq | jq -sRM 'split("\n")[:-1]'
+
  const usedColors = [
+
    "--color-background",
+
    "--color-caution",
+
    "--color-caution-2",
+
    "--color-caution-3",
+
    "--color-caution-6",
+
    "--color-foreground",
+
    "--color-foreground-1",
+
    "--color-foreground-2",
+
    "--color-foreground-3",
+
    "--color-foreground-4",
+
    "--color-foreground-5",
+
    "--color-foreground-6",
+
    "--color-negative",
+
    "--color-negative-1",
+
    "--color-negative-2",
+
    "--color-negative-3",
+
    "--color-negative-4",
+
    "--color-negative-5",
+
    "--color-negative-6",
+
    "--color-positive",
+
    "--color-positive-1",
+
    "--color-positive-2",
+
    "--color-positive-3",
+
    "--color-positive-6",
+
    "--color-primary",
+
    "--color-primary-3",
+
    "--color-primary-5",
+
    "--color-secondary",
+
    "--color-secondary-1",
+
    "--color-secondary-2",
+
    "--color-secondary-3",
+
    "--color-secondary-5",
+
    "--color-secondary-6",
+
    "--color-tertiary",
+
    "--color-tertiary-1",
+
    "--color-tertiary-2",
+
    "--color-tertiary-3",
+
    "--color-tertiary-6",
+
  ];
+

+
  const colors = extractCssVariables("--color").filter(c => {
+
    return !c.startsWith("--color-prettylights-syntax");
+
  });
+

+
  const colorGroups = [
+
    ...new Set(
+
      colors.map(color => {
+
        const match = color.match(/--color-(\w*)-?/);
+
        if (match) {
+
          return match[1];
+
        } else {
+
          return "";
+
        }
+
      }),
+
    ),
+
  ];
+

+
  let checkers = false;
+
</script>
+

+
<style>
+
  .checkers {
+
    background: repeating-conic-gradient(#88888833 0% 25%, transparent 0% 50%)
+
      50% / 20px 20px;
+
    border-radius: 1rem;
+
  }
+

+
  .container {
+
    display: flex;
+
    margin: 0;
+
    padding: 0;
+
  }
+

+
  .color {
+
    width: 3rem;
+
    height: 3rem;
+
    border-radius: 0.5rem;
+
    outline-style: solid !important;
+
    outline-color: #88888899 !important;
+
    outline-offset: 0.3rem;
+
    margin: 1rem;
+
  }
+

+
  .unused {
+
    outline-style: dotted !important;
+
    outline-color: #55555555 !important;
+
  }
+
</style>
+

+
<Modal closeAction={false}>
+
  <!-- svelte-ignore a11y-click-events-have-key-events -->
+
  <div slot="body">
+
    <div class="container" on:click={() => (checkers = !checkers)}>
+
      <div class:checkers>
+
        {#each colorGroups as colorGroup}
+
          <div style:display="flex">
+
            {#each colors.filter(color => {
+
              return color.match(`--color-${colorGroup}`);
+
            }) as color}
+
              <div style:display="inline-flex">
+
                <div
+
                  class:unused={!usedColors.includes(color)}
+
                  title={color}
+
                  class="color"
+
                  style:background-color={`var(${color})`} />
+
              </div>
+
            {/each}
+
          </div>
+
        {/each}
+
      </div>
+
    </div>
+
  </div>
+
</Modal>
modified src/App/Header/ThemeToggle.svelte
@@ -1,6 +1,6 @@
<script lang="ts">
  import Icon from "@app/components/Icon.svelte";
-
  import Toggle from "@app/components/Toggle.svelte";
+
  import ToggleSwitch from "@app/components/ToggleSwitch.svelte";
  import { theme, storeTheme } from "@app/lib/appearance";

  $: document.documentElement.setAttribute("data-theme", $theme);
@@ -19,7 +19,7 @@

<div class="theme">
  <Icon name="sun" on:click={() => theme.set("light")} />
-
  <Toggle
+
  <ToggleSwitch
    checked={$theme === "dark"}
    on:change={() => {
      theme.set($theme === "dark" ? "light" : "dark");
deleted src/App/HelpModal.svelte
@@ -1,79 +0,0 @@
-
<script lang="ts">
-
  import Modal from "@app/components/Modal.svelte";
-
</script>
-

-
<style>
-
  .hotkeys {
-
    gap: 3rem;
-
    align-items: flex-start;
-
    justify-content: center;
-
    display: flex;
-
    color: var(--color-foreground-6);
-
  }
-

-
  .key {
-
    border: 1px solid var(--color-secondary-5);
-
    box-shadow: inset 0 -4px 0 var(--color-secondary-5);
-
    height: 36px;
-
    display: flex;
-
    align-items: center;
-
    justify-content: center;
-
    border-radius: var(--border-radius-small);
-
    background-color: var(--color-secondary-1);
-
    min-width: 2rem;
-
    padding: 0 1rem 4px 1rem;
-
  }
-

-
  .description {
-
    text-align: left;
-
  }
-

-
  .pair {
-
    display: flex;
-
    align-items: center;
-
    gap: 1rem;
-
  }
-

-
  .group {
-
    display: flex;
-
    gap: 2rem;
-
    flex-direction: column;
-
  }
-
</style>
-

-
<Modal emoji="⌨️" title="Keyboard shortcuts" closeAction={false}>
-
  <div slot="body" style:margin="1rem 0">
-
    <div class="hotkeys">
-
      <div class="group">
-
        <div class="pair">
-
          <div class="key txt-bold">?</div>
-
          <div class="description">Shortcuts</div>
-
        </div>
-

-
        <div class="pair">
-
          <div class="key txt-bold">/</div>
-
          <div class="description">Search</div>
-
        </div>
-

-
        {#if import.meta.env.DEV}
-
          <div class="pair">
-
            <div class="key txt-bold">d</div>
-
            <div class="description">Color palette</div>
-
          </div>
-
        {/if}
-
      </div>
-

-
      <div class="group">
-
        <div class="pair">
-
          <div class="key txt-bold">enter</div>
-
          <div class="description">Submit</div>
-
        </div>
-

-
        <div class="pair">
-
          <div class="key txt-bold">esc</div>
-
          <div class="description">Close</div>
-
        </div>
-
      </div>
-
    </div>
-
  </div>
-
</Modal>
modified src/App/Hotkeys.svelte
@@ -1,8 +1,8 @@
<script lang="ts">
  import * as modal from "@app/lib/modal";

-
  import ColorPalette from "./ColorPalette.svelte";
-
  import HelpModal from "./HelpModal.svelte";
+
  import ColorPaletteModal from "@app/App/ColorPaletteModal.svelte";
+
  import HotkeysModal from "@app/App/HotkeysModal.svelte";

  const onKeydown = (event: KeyboardEvent) => {
    if (event.key === "Escape") {
@@ -12,7 +12,7 @@

    switch (event.key) {
      case "?":
-
        modal.toggle({ component: HelpModal, props: {} });
+
        modal.toggle({ component: HotkeysModal, props: {} });
        break;
      case "/": {
        event.preventDefault();
@@ -26,7 +26,7 @@
        if (import.meta.env.PROD) {
          return;
        }
-
        modal.toggle({ component: ColorPalette, props: {} });
+
        modal.toggle({ component: ColorPaletteModal, props: {} });
        break;
    }
  };
added src/App/HotkeysModal.svelte
@@ -0,0 +1,79 @@
+
<script lang="ts">
+
  import Modal from "@app/components/Modal.svelte";
+
</script>
+

+
<style>
+
  .hotkeys {
+
    gap: 3rem;
+
    align-items: flex-start;
+
    justify-content: center;
+
    display: flex;
+
    color: var(--color-foreground-6);
+
  }
+

+
  .key {
+
    border: 1px solid var(--color-secondary-5);
+
    box-shadow: inset 0 -4px 0 var(--color-secondary-5);
+
    height: 36px;
+
    display: flex;
+
    align-items: center;
+
    justify-content: center;
+
    border-radius: var(--border-radius-small);
+
    background-color: var(--color-secondary-1);
+
    min-width: 2rem;
+
    padding: 0 1rem 4px 1rem;
+
  }
+

+
  .description {
+
    text-align: left;
+
  }
+

+
  .pair {
+
    display: flex;
+
    align-items: center;
+
    gap: 1rem;
+
  }
+

+
  .group {
+
    display: flex;
+
    gap: 2rem;
+
    flex-direction: column;
+
  }
+
</style>
+

+
<Modal emoji="⌨️" title="Keyboard shortcuts" closeAction={false}>
+
  <div slot="body" style:margin="1rem 0">
+
    <div class="hotkeys">
+
      <div class="group">
+
        <div class="pair">
+
          <div class="key txt-bold">?</div>
+
          <div class="description">Shortcuts</div>
+
        </div>
+

+
        <div class="pair">
+
          <div class="key txt-bold">/</div>
+
          <div class="description">Search</div>
+
        </div>
+

+
        {#if import.meta.env.DEV}
+
          <div class="pair">
+
            <div class="key txt-bold">d</div>
+
            <div class="description">Color palette</div>
+
          </div>
+
        {/if}
+
      </div>
+

+
      <div class="group">
+
        <div class="pair">
+
          <div class="key txt-bold">enter</div>
+
          <div class="description">Submit</div>
+
        </div>
+

+
        <div class="pair">
+
          <div class="key txt-bold">esc</div>
+
          <div class="description">Close</div>
+
        </div>
+
      </div>
+
    </div>
+
  </div>
+
</Modal>
deleted src/components/Avatar.svelte
@@ -1,61 +0,0 @@
-
<script lang="ts">
-
  import { createIcon } from "@app/lib/blockies";
-
  import { isPeerId, isRadicleId } from "@app/lib/utils";
-

-
  export let title: string;
-
  export let source: string;
-
  export let inline = false;
-
  export let grayscale = false;
-

-
  function handleMissingFile() {
-
    console.warn("Not able to locate", source);
-
    source = createContainer(title);
-
  }
-

-
  function createContainer(source: string) {
-
    const seed = source.toLowerCase();
-
    const avatar = createIcon({
-
      seed,
-
      size: 8,
-
      scale: 16,
-
    });
-
    return avatar.toDataURL();
-
  }
-

-
  if (isRadicleId(source) || isPeerId(source)) {
-
    source = createContainer(source);
-
  }
-
  grayscale = isPeerId(title) || isRadicleId(title);
-
</script>
-

-
<style>
-
  .avatar {
-
    display: block;
-
    border-radius: var(--border-radius-round);
-
    min-width: 1rem;
-
    min-height: 1rem;
-
    height: 100%;
-
    width: inherit;
-
    object-fit: cover;
-
    background-size: cover;
-
    background-repeat: no-repeat;
-
  }
-
  .grayscale {
-
    filter: grayscale();
-
  }
-
  .inline {
-
    display: inline-block !important;
-
    width: 1rem;
-
    height: 1rem;
-
    margin-right: 0.5rem;
-
  }
-
</style>
-

-
<img
-
  {title}
-
  src={source}
-
  class="avatar"
-
  alt="avatar"
-
  on:error={handleMissingFile}
-
  class:inline
-
  class:grayscale />
modified src/components/Comment.svelte
@@ -3,10 +3,10 @@
  import type { Comment, Thread } from "@app/lib/issue";

  import Authorship from "@app/components/Authorship.svelte";
-
  import Avatar from "@app/components/Avatar.svelte";
+
  import Avatar from "@app/components/Comment/Avatar.svelte";
  import Markdown from "@app/components/Markdown.svelte";
  import ReactionSelector from "./Comment/ReactionSelector.svelte";
-
  import Reactions from "./Comment/Reactions.svelte";
+
  import Reactions from "@app/components/Comment/Reactions.svelte";

  export let comment: Comment | Thread;
  export let caption = "left a comment";
added src/components/Comment/Avatar.svelte
@@ -0,0 +1,61 @@
+
<script lang="ts">
+
  import { createIcon } from "@app/lib/blockies";
+
  import { isPeerId, isRadicleId } from "@app/lib/utils";
+

+
  export let title: string;
+
  export let source: string;
+
  export let inline = false;
+
  export let grayscale = false;
+

+
  function handleMissingFile() {
+
    console.warn("Not able to locate", source);
+
    source = createContainer(title);
+
  }
+

+
  function createContainer(source: string) {
+
    const seed = source.toLowerCase();
+
    const avatar = createIcon({
+
      seed,
+
      size: 8,
+
      scale: 16,
+
    });
+
    return avatar.toDataURL();
+
  }
+

+
  if (isRadicleId(source) || isPeerId(source)) {
+
    source = createContainer(source);
+
  }
+
  grayscale = isPeerId(title) || isRadicleId(title);
+
</script>
+

+
<style>
+
  .avatar {
+
    display: block;
+
    border-radius: var(--border-radius-round);
+
    min-width: 1rem;
+
    min-height: 1rem;
+
    height: 100%;
+
    width: inherit;
+
    object-fit: cover;
+
    background-size: cover;
+
    background-repeat: no-repeat;
+
  }
+
  .grayscale {
+
    filter: grayscale();
+
  }
+
  .inline {
+
    display: inline-block !important;
+
    width: 1rem;
+
    height: 1rem;
+
    margin-right: 0.5rem;
+
  }
+
</style>
+

+
<img
+
  {title}
+
  src={source}
+
  class="avatar"
+
  alt="avatar"
+
  on:error={handleMissingFile}
+
  class:inline
+
  class:grayscale />
deleted src/components/Emoji.svelte
@@ -1,16 +0,0 @@
-
<script lang="ts">
-
  // Use this component if you need an emoji that is reactive. For static
-
  // emojis use the `twemoji` action from the utils module.
-

-
  import twemoji from "twemoji";
-
  import { base } from "@app/lib/router";
-

-
  export let emoji: string;
-
</script>
-

-
{@html twemoji.parse(emoji, {
-
  base,
-
  folder: "twemoji",
-
  ext: ".svg",
-
  className: "txt-emoji",
-
})}
deleted src/components/Error.svelte
@@ -1,35 +0,0 @@
-
<script lang="ts">
-
  import { twemoji } from "@app/lib/utils";
-

-
  export let emoji: string = "👻";
-
  export let title: string;
-
  export let message: string;
-
</script>
-

-
<style>
-
  .error {
-
    display: flex;
-
    align-items: center;
-
    flex-direction: column;
-
    gap: 1rem;
-
  }
-

-
  .emoji {
-
    font-size: var(--font-size-xx-large);
-
    display: flex;
-
  }
-

-
  .title {
-
    color: var(--color-negative);
-
  }
-

-
  .message {
-
    color: var(--color-foreground-6);
-
  }
-
</style>
-

-
<div class="error">
-
  <div class="emoji" use:twemoji>{emoji}</div>
-
  <div class="title txt-medium txt-bold">{title}</div>
-
  <div class="message">{message}</div>
-
</div>
deleted src/components/ErrorModal.svelte
@@ -1,83 +0,0 @@
-
<script lang="ts">
-
  import type {
-
    CloseAction,
-
    PrimaryAction,
-
  } from "@app/components/Modal.svelte";
-

-
  import debounce from "lodash/debounce";
-
  import Modal from "@app/components/Modal.svelte";
-
  import Icon from "@app/components/Icon.svelte";
-

-
  import { toClipboard } from "@app/lib/utils";
-

-
  export let title: string;
-
  export let caption: string = "There was an error with your transaction.";
-
  export let error: string | undefined = undefined;
-

-
  export let closeAction: CloseAction = undefined;
-
  export let primaryAction: PrimaryAction = undefined;
-

-
  const emoji = "🚨";
-
  let clipboardIcon: "clipboard" | "checkmark" = "clipboard";
-

-
  const resetIcon = debounce(() => {
-
    clipboardIcon = "clipboard";
-
  }, 800);
-

-
  function copy() {
-
    if (error) {
-
      toClipboard(error);
-
    }
-
    clipboardIcon = "checkmark";
-
    resetIcon();
-
  }
-
</script>
-

-
<style>
-
  .container {
-
    overflow: hidden;
-
    border-radius: var(--border-radius);
-
    position: relative;
-
  }
-

-
  .copy {
-
    position: absolute;
-
    right: 10px;
-
    top: 10px;
-
    padding: 2px;
-
    background-color: var(--color-foreground-2);
-
    border-radius: var(--border-radius-round);
-
    cursor: pointer;
-
  }
-

-
  .message {
-
    font-size: var(--font-size-tiny);
-
    word-wrap: break-word;
-
    max-height: 8rem;
-
    background-color: var(--color-foreground-2);
-
    overflow-y: auto;
-
    padding: 1rem;
-
    text-align: left;
-
  }
-
</style>
-

-
{#if error}
-
  <Modal {title} {emoji} {closeAction} {primaryAction}>
-
    <div slot="subtitle">{caption}</div>
-
    <div slot="body">
-
      <div class="container">
-
        <!-- svelte-ignore a11y-click-events-have-key-events -->
-
        <div class="copy" on:click={copy}>
-
          <Icon name={clipboardIcon} />
-
        </div>
-
        <div class="message txt-monospace txt-small">
-
          {error}
-
        </div>
-
      </div>
-
    </div>
-
  </Modal>
-
{:else}
-
  <Modal {title} {emoji} {closeAction} {primaryAction}>
-
    <div slot="subtitle">{caption}</div>
-
  </Modal>
-
{/if}
modified src/components/Modal.svelte
@@ -26,10 +26,12 @@
</script>

<script lang="ts">
+
  import twemoji from "twemoji";
+

  import * as modal from "@app/lib/modal";
+
  import { base } from "@app/lib/router";

  import Button from "@app/components/Button.svelte";
-
  import Emoji from "@app/components/Emoji.svelte";

  export let emoji: string | undefined = undefined;
  export let title: string | undefined = undefined;
@@ -89,7 +91,12 @@
<div class="modal">
  {#if emoji}
    <div style:font-size="var(--font-size-xx-large)">
-
      <Emoji {emoji} />
+
      {@html twemoji.parse(emoji, {
+
        base,
+
        folder: "twemoji",
+
        ext: ".svg",
+
        className: "txt-emoji",
+
      })}
    </div>
  {/if}

deleted src/components/RadicleId.svelte
@@ -1,51 +0,0 @@
-
<script lang="ts">
-
  import { parseRadicleId, toClipboard, twemoji } from "@app/lib/utils";
-
  import Button from "@app/components/Button.svelte";
-

-
  export let id: string;
-

-
  let copied = false;
-

-
  function copy() {
-
    toClipboard(id).then(() => {
-
      copied = true;
-
      setTimeout(() => {
-
        copied = false;
-
      }, 1000);
-
    });
-
  }
-
</script>
-

-
<style>
-
  .id {
-
    display: inline-flex;
-
    font-size: var(--font-size-regular);
-
    line-height: 2rem;
-
    color: var(--color-foreground-6);
-
    vertical-align: middle;
-
  }
-
  .icon {
-
    width: 1rem;
-
    margin-right: 0.5rem;
-
  }
-
  .id > * {
-
    vertical-align: middle;
-
  }
-
</style>
-

-
<div class="layout-desktop">
-
  <div class="id">
-
    <span class="icon" use:twemoji>🌱</span>
-
    <span class="txt-faded">rad:</span>
-
    <span>{parseRadicleId(id)}</span>
-
  </div>
-
</div>
-
<div>
-
  <Button variant="foreground" size="small" disabled={copied} on:click={copy}>
-
    {#if copied}
-
      Copy ✓
-
    {:else}
-
      Copy
-
    {/if}
-
  </Button>
-
</div>
deleted src/components/SeedAddress.svelte
@@ -1,74 +0,0 @@
-
<script lang="ts">
-
  import type { Seed } from "@app/lib/seed";
-

-
  import Clipboard from "@app/components/Clipboard.svelte";
-
  import Link from "@app/components/Link.svelte";
-
  import {
-
    formatSeedAddress,
-
    formatSeedId,
-
    formatSeedHost,
-
    twemoji,
-
  } from "@app/lib/utils";
-

-
  export let seed: Seed;
-
  export let port: number;
-
  export let full = false;
-

-
  const seedHost = seed.addr.port
-
    ? `${seed.addr.host}:${seed.addr.port}`
-
    : `${formatSeedHost(seed.addr.host)}`;
-
</script>
-

-
<style>
-
  .wrapper {
-
    display: flex;
-
    align-items: center;
-
    gap: 0.2rem;
-
  }
-
  .seed-address {
-
    display: inline-flex;
-
    font-size: var(--font-size-regular);
-
    line-height: 2rem;
-
    color: var(--color-foreground-6);
-
    vertical-align: middle;
-
  }
-
  .seed-icon {
-
    width: 1rem;
-
    margin-right: 0.5rem;
-
  }
-
  .seed-address > * {
-
    vertical-align: middle;
-
  }
-
</style>
-

-
<div class="wrapper">
-
  <div class="seed-address">
-
    <span class="seed-icon" use:twemoji>{seed.emoji}</span>
-
    {#if full}
-
      <span>
-
        <Link
-
          route={{
-
            resource: "seeds",
-
            params: { host: formatSeedHost(seedHost) },
-
          }}>
-
          <span class="txt-link">{formatSeedId(seed.id)}@{seed.host}</span>
-
        </Link>
-
      </span>
-
      <span class="txt-faded">:{port}</span>
-
    {:else}
-
      <span>
-
        <Link
-
          route={{
-
            resource: "seeds",
-
            params: { host: seedHost },
-
          }}>
-
          <span class="txt-link">{formatSeedHost(seedHost)}</span>
-
        </Link>
-
      </span>
-
    {/if}
-
  </div>
-
  <Clipboard
-
    small
-
    text={full ? formatSeedAddress(seed.id, seed.host, port) : seed.host} />
-
</div>
-
<div class="layout-desktop" />
added src/components/TabBar.svelte
@@ -0,0 +1,71 @@
+
<script lang="ts" context="module">
+
  export interface Tab<T> {
+
    title?: string;
+
    count?: number;
+
    value: T;
+
  }
+
</script>
+

+
<script lang="ts" strictEvents>
+
  type T = $$Generic;
+

+
  import { createEventDispatcher } from "svelte";
+
  import { capitalize } from "@app/lib/utils";
+

+
  export let options: Tab<T>[];
+
  export let active: T;
+

+
  const dispatch = createEventDispatcher<{ select: T }>();
+

+
  function onSelect(option: Tab<T>) {
+
    if (option.count !== 0) {
+
      dispatch("select", option.value);
+
    }
+
  }
+
</script>
+

+
<style>
+
  .wrapper {
+
    display: flex;
+
    gap: 1rem;
+
    user-select: none;
+
  }
+
  button {
+
    border-radius: var(--border-radius-small);
+
    color: var(--color-foreground-6);
+
    cursor: pointer;
+
    font-family: var(--font-family-monospace);
+
    font-size: var(--font-size-tiny);
+
    height: var(--button-tiny-height);
+
    padding: 0.25rem 0.5rem;
+
    border: none;
+
    min-width: 0;
+
    background-color: var(--color-background);
+
  }
+
  button:hover,
+
  button.active {
+
    cursor: pointer;
+
    color: var(--color-foreground);
+
    background-color: var(--color-foreground-1);
+
  }
+
  button[disabled],
+
  button[disabled]:hover {
+
    cursor: not-allowed;
+
    color: var(--color-foreground-6);
+
  }
+
</style>
+

+
<div class="wrapper">
+
  {#each options as option}
+
    <button
+
      class="state-toggle"
+
      on:click={() => onSelect(option)}
+
      disabled={option.count === 0}
+
      class:active={active === option.value}>
+
      {#if option.count !== undefined}
+
        {option.count}
+
      {/if}
+
      {option.title ?? capitalize(`${option.value}`)}
+
    </button>
+
  {/each}
+
</div>
deleted src/components/Toggle.svelte
@@ -1,55 +0,0 @@
-
<script lang="ts">
-
  // Is not as good as crypto.randomUUID() but we need some kind of fallback
-
  const id = self.crypto.randomUUID
-
    ? self.crypto.randomUUID()
-
    : new Date().getTime().toString();
-

-
  export let checked: boolean;
-
</script>
-

-
<style>
-
  .toggle input[type="checkbox"] {
-
    display: none;
-
  }
-

-
  .toggle label {
-
    background-color: var(--color-foreground-1);
-
    border: 1px solid var(--color-foreground-6);
-
    border-radius: var(--border-radius-round);
-
    cursor: pointer;
-
    display: block;
-
    position: relative;
-
    transition: transform ease-in-out 0.2s;
-
    width: 2.5rem;
-
    height: 1.5rem;
-
  }
-

-
  .toggle label::after {
-
    background-color: var(--color-foreground-6);
-
    border-radius: var(--border-radius-round);
-
    content: " ";
-
    cursor: pointer;
-
    display: inline-block;
-
    position: absolute;
-
    left: 3px;
-
    top: 3px;
-
    transition: transform ease-in-out 0.2s;
-
    width: 1rem;
-
    height: 1rem;
-
  }
-

-
  .toggle input[type="checkbox"]:checked ~ label {
-
    background-color: var(--color-foreground-1);
-
    border-color: var(--color-foreground-6);
-
  }
-

-
  .toggle input[type="checkbox"]:checked ~ label::after {
-
    background-color: var(--color-foreground-6);
-
    transform: translateX(15px);
-
  }
-
</style>
-

-
<div class="toggle">
-
  <input type="checkbox" bind:checked on:change {id} />
-
  <label for={id} />
-
</div>
deleted src/components/ToggleButton.svelte
@@ -1,71 +0,0 @@
-
<script lang="ts" context="module">
-
  export interface ToggleButtonOption<T> {
-
    title?: string;
-
    count?: number;
-
    value: T;
-
  }
-
</script>
-

-
<script lang="ts" strictEvents>
-
  type T = $$Generic;
-

-
  import { createEventDispatcher } from "svelte";
-
  import { capitalize } from "@app/lib/utils";
-

-
  export let options: ToggleButtonOption<T>[];
-
  export let active: T;
-

-
  const dispatch = createEventDispatcher<{ select: T }>();
-

-
  function onSelect(option: ToggleButtonOption<T>) {
-
    if (option.count !== 0) {
-
      dispatch("select", option.value);
-
    }
-
  }
-
</script>
-

-
<style>
-
  .wrapper {
-
    display: flex;
-
    gap: 1rem;
-
    user-select: none;
-
  }
-
  button {
-
    border-radius: var(--border-radius-small);
-
    color: var(--color-foreground-6);
-
    cursor: pointer;
-
    font-family: var(--font-family-monospace);
-
    font-size: var(--font-size-tiny);
-
    height: var(--button-tiny-height);
-
    padding: 0.25rem 0.5rem;
-
    border: none;
-
    min-width: 0;
-
    background-color: var(--color-background);
-
  }
-
  button:hover,
-
  button.active {
-
    cursor: pointer;
-
    color: var(--color-foreground);
-
    background-color: var(--color-foreground-1);
-
  }
-
  button[disabled],
-
  button[disabled]:hover {
-
    cursor: not-allowed;
-
    color: var(--color-foreground-6);
-
  }
-
</style>
-

-
<div class="wrapper">
-
  {#each options as option}
-
    <button
-
      class="state-toggle"
-
      on:click={() => onSelect(option)}
-
      disabled={option.count === 0}
-
      class:active={active === option.value}>
-
      {#if option.count !== undefined}
-
        {option.count}
-
      {/if}
-
      {option.title ?? capitalize(`${option.value}`)}
-
    </button>
-
  {/each}
-
</div>
added src/components/ToggleSwitch.svelte
@@ -0,0 +1,55 @@
+
<script lang="ts">
+
  // Is not as good as crypto.randomUUID() but we need some kind of fallback
+
  const id = self.crypto.randomUUID
+
    ? self.crypto.randomUUID()
+
    : new Date().getTime().toString();
+

+
  export let checked: boolean;
+
</script>
+

+
<style>
+
  .toggle input[type="checkbox"] {
+
    display: none;
+
  }
+

+
  .toggle label {
+
    background-color: var(--color-foreground-1);
+
    border: 1px solid var(--color-foreground-6);
+
    border-radius: var(--border-radius-round);
+
    cursor: pointer;
+
    display: block;
+
    position: relative;
+
    transition: transform ease-in-out 0.2s;
+
    width: 2.5rem;
+
    height: 1.5rem;
+
  }
+

+
  .toggle label::after {
+
    background-color: var(--color-foreground-6);
+
    border-radius: var(--border-radius-round);
+
    content: " ";
+
    cursor: pointer;
+
    display: inline-block;
+
    position: absolute;
+
    left: 3px;
+
    top: 3px;
+
    transition: transform ease-in-out 0.2s;
+
    width: 1rem;
+
    height: 1rem;
+
  }
+

+
  .toggle input[type="checkbox"]:checked ~ label {
+
    background-color: var(--color-foreground-1);
+
    border-color: var(--color-foreground-6);
+
  }
+

+
  .toggle input[type="checkbox"]:checked ~ label::after {
+
    background-color: var(--color-foreground-6);
+
    transform: translateX(15px);
+
  }
+
</style>
+

+
<div class="toggle">
+
  <input type="checkbox" bind:checked on:change {id} />
+
  <label for={id} />
+
</div>
modified src/views/projects/Issues.svelte
@@ -4,7 +4,7 @@

<script lang="ts">
  import type { Issue } from "@app/lib/issue";
-
  import type { ToggleButtonOption } from "@app/components/ToggleButton.svelte";
+
  import type { Tab } from "@app/components/TabBar.svelte";

  import { capitalize } from "@app/lib/utils";
  import { groupIssues } from "@app/lib/issue";
@@ -12,12 +12,12 @@

  import IssueTeaser from "@app/views/projects/Issue/IssueTeaser.svelte";
  import Placeholder from "@app/components/Placeholder.svelte";
-
  import ToggleButton from "@app/components/ToggleButton.svelte";
+
  import TabBar from "@app/components/TabBar.svelte";

  export let issues: Issue[];
  export let state: State;

-
  let options: ToggleButtonOption<State>[];
+
  let options: Tab<State>[];
  const { open, closed } = groupIssues(issues);

  $: filteredIssues = state === "open" ? open : closed;
@@ -59,7 +59,7 @@

<div class="issues">
  <div style="margin-bottom: 1rem;">
-
    <ToggleButton
+
    <TabBar
      {options}
      on:select={e =>
        router.updateProjectRoute({
modified src/views/projects/Patch/PatchTabBar.svelte
@@ -1,9 +1,9 @@
<script lang="ts" strictEvents>
-
  import type { ToggleButtonOption } from "@app/components/ToggleButton.svelte";
+
  import type { Tab } from "@app/components/TabBar.svelte";

  import Dropdown from "@app/components/Dropdown.svelte";
  import Floating, { closeFocused } from "@app/components/Floating.svelte";
-
  import ToggleButton from "@app/components/ToggleButton.svelte";
+
  import TabBar from "@app/components/TabBar.svelte";

  import type { Revision } from "@app/lib/patch";
  import { PatchTab } from "@app/lib/patch";
@@ -37,7 +37,7 @@
    dispatch("revisionChanged", detail);
  };

-
  let options: ToggleButtonOption<PatchTab>[];
+
  let options: Tab<PatchTab>[];
  $: options = [
    {
      title: "Patch",
@@ -77,7 +77,7 @@
</style>

<div class="bar txt-small">
-
  <ToggleButton
+
  <TabBar
    {options}
    on:select={e => {
      dispatch("switchTab", e.detail);
modified src/views/projects/Patches.svelte
@@ -4,11 +4,11 @@

<script lang="ts">
  import type { Patch } from "@app/lib/patch";
-
  import type { ToggleButtonOption } from "@app/components/ToggleButton.svelte";
+
  import type { Tab } from "@app/components/TabBar.svelte";

  import PatchTeaser from "./Patch/PatchTeaser.svelte";
  import Placeholder from "@app/components/Placeholder.svelte";
-
  import ToggleButton from "@app/components/ToggleButton.svelte";
+
  import TabBar from "@app/components/TabBar.svelte";

  import { capitalize } from "@app/lib/utils";
  import { groupPatches } from "@app/lib/patch";
@@ -17,7 +17,7 @@
  export let state: State;
  export let patches: Patch[];

-
  let options: ToggleButtonOption<State>[];
+
  let options: Tab<State>[];
  const sortedPatches = groupPatches(patches);

  $: filteredPatches = sortedPatches[state];
@@ -59,7 +59,7 @@

<div class="patches">
  <div style="margin-bottom: 1rem;">
-
    <ToggleButton
+
    <TabBar
      {options}
      on:select={e =>
        router.updateProjectRoute({
modified src/views/seeds/View.svelte
@@ -1,15 +1,16 @@
<script lang="ts">
-
  import type { Stats } from "@app/lib/seed";
+
  import type { Host } from "@app/lib/api";
  import type { ProjectInfo } from "@app/lib/project";
-
  import { formatSeedId, formatSeedHost, twemoji } from "@app/lib/utils";
-
  import { Seed, defaultSeedPort } from "@app/lib/seed";
+
  import type { Stats } from "@app/lib/seed";
+

+
  import Clipboard from "@app/components/Clipboard.svelte";
  import Loading from "@app/components/Loading.svelte";
-
  import SeedAddress from "@app/components/SeedAddress.svelte";
  import NotFound from "@app/components/NotFound.svelte";
-
  import Clipboard from "@app/components/Clipboard.svelte";
  import Projects from "@app/views/seeds/View/Projects.svelte";
+
  import SeedAddress from "@app/views/seeds/View/SeedAddress.svelte";
  import { Project } from "@app/lib/project";
-
  import type { Host } from "@app/lib/api";
+
  import { Seed, defaultSeedPort } from "@app/lib/seed";
+
  import { formatSeedId, formatSeedHost, twemoji } from "@app/lib/utils";

  export let hostAndPort: string;

added src/views/seeds/View/SeedAddress.svelte
@@ -0,0 +1,74 @@
+
<script lang="ts">
+
  import type { Seed } from "@app/lib/seed";
+

+
  import Clipboard from "@app/components/Clipboard.svelte";
+
  import Link from "@app/components/Link.svelte";
+
  import {
+
    formatSeedAddress,
+
    formatSeedId,
+
    formatSeedHost,
+
    twemoji,
+
  } from "@app/lib/utils";
+

+
  export let seed: Seed;
+
  export let port: number;
+
  export let full = false;
+

+
  const seedHost = seed.addr.port
+
    ? `${seed.addr.host}:${seed.addr.port}`
+
    : `${formatSeedHost(seed.addr.host)}`;
+
</script>
+

+
<style>
+
  .wrapper {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.2rem;
+
  }
+
  .seed-address {
+
    display: inline-flex;
+
    font-size: var(--font-size-regular);
+
    line-height: 2rem;
+
    color: var(--color-foreground-6);
+
    vertical-align: middle;
+
  }
+
  .seed-icon {
+
    width: 1rem;
+
    margin-right: 0.5rem;
+
  }
+
  .seed-address > * {
+
    vertical-align: middle;
+
  }
+
</style>
+

+
<div class="wrapper">
+
  <div class="seed-address">
+
    <span class="seed-icon" use:twemoji>{seed.emoji}</span>
+
    {#if full}
+
      <span>
+
        <Link
+
          route={{
+
            resource: "seeds",
+
            params: { host: formatSeedHost(seedHost) },
+
          }}>
+
          <span class="txt-link">{formatSeedId(seed.id)}@{seed.host}</span>
+
        </Link>
+
      </span>
+
      <span class="txt-faded">:{port}</span>
+
    {:else}
+
      <span>
+
        <Link
+
          route={{
+
            resource: "seeds",
+
            params: { host: seedHost },
+
          }}>
+
          <span class="txt-link">{formatSeedHost(seedHost)}</span>
+
        </Link>
+
      </span>
+
    {/if}
+
  </div>
+
  <Clipboard
+
    small
+
    text={full ? formatSeedAddress(seed.id, seed.host, port) : seed.host} />
+
</div>
+
<div class="layout-desktop" />