Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
Layout and header tweaks
Open rudolfs opened 1 year ago
16 files changed +962 -168 43ad446d 4f63f9e8
added public/aliens.png
modified public/index.css
@@ -7,6 +7,10 @@ html {
  width: 100%;
  /* Prevent rubber-band effect when scrolling via touchpad on macOS. */
  overscroll-behavior: none;
+
  cursor: default;
+
  -webkit-touch-callout: none;
+
  -webkit-user-select: none;
+
  user-select: none;
}

body {
added src/components/Avatar.svelte
@@ -0,0 +1,45 @@
+
<script lang="ts">
+
  import { createIcon } from "@app/lib/blockies";
+

+
  export let nodeId: string;
+

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

+
<style>
+
  .avatar {
+
    display: block;
+
    width: inherit;
+
    object-fit: cover;
+
    background-size: cover;
+
    background-repeat: no-repeat;
+
    width: 1rem;
+
    height: 1rem;
+
    clip-path: polygon(
+
      0 2px,
+
      2px 2px,
+
      2px 0,
+
      calc(100% - 2px) 0,
+
      calc(100% - 2px) 2px,
+
      100% 2px,
+
      100% calc(100% - 2px),
+
      calc(100% - 2px) calc(100% - 2px),
+
      calc(100% - 2px) calc(100% - 2px),
+
      calc(100% - 2px) 100%,
+
      2px 100%,
+
      2px calc(100% - 2px),
+
      0 calc(100% - 2px)
+
    );
+
    background-color: red;
+
  }
+
</style>
+

+
<img title={nodeId} src={createContainer(nodeId)} class="avatar" alt="avatar" />
modified src/components/CopyableId.svelte
@@ -24,6 +24,11 @@
<style>
  .copyable-id {
    cursor: pointer;
+
    color: var(--color-foreground-dim);
+
  }
+

+
  .copyable-id:hover {
+
    color: var(--color-foreground-contrast);
  }
</style>

@@ -32,8 +37,7 @@
  role="button"
  tabindex="0"
  onclick={copy}
-
  class="copyable-id global-flex txt-small txt-monospace"
-
  style:color="var(--color-foreground-dim)">
+
  class="copyable-id global-flex txt-small txt-monospace">
  {id}
  <Icon name={icon} />
</div>
modified src/components/Header.svelte
@@ -1,20 +1,17 @@
<script lang="ts">
-
  import Background from "./Header/Background.svelte";
  import Border from "./Border.svelte";
  import Icon from "./Icon.svelte";
+
  import NakedButton from "./NakedButton.svelte";
+
  import OutlineButton from "./OutlineButton.svelte";
  import Popover from "./Popover.svelte";
  import ThemeSwitch from "./ThemeSwitch.svelte";
</script>

<style>
-
  .flex-item {
-
    display: flex;
-
    align-items: center;
-
  }
-
  header {
+
  .header {
    padding: 0 0.5rem;
    gap: 0.25rem;
-
    height: 3rem;
+
    height: 5rem;
  }
  .wrapper {
    width: 100%;
@@ -25,56 +22,94 @@
    gap: 0.5rem;
    padding: 0 0.5rem;
  }
+
  .bottom-pixel-corners {
+
    position: absolute;
+
    top: 0;
+
    left: 0.5rem;
+
    right: 0.5rem;
+
    height: 5rem;
+
    z-index: -1;

-
  .navigation :global(svg:hover) {
-
    display: flex;
-
    color: var(--color-fill-secondary);
+
    background-color: var(--color-background-float);
+
    clip-path: polygon(
+
      0 0,
+
      100% 0,
+
      100% calc(100% - 6px),
+
      calc(100% - 2px) calc(100% - 6px),
+
      calc(100% - 2px) calc(100% - 4px),
+
      calc(100% - 4px) calc(100% - 4px),
+
      calc(100% - 4px) calc(100% - 2px),
+
      calc(100% - 6px) calc(100% - 2px),
+
      calc(100% - 6px) 100%,
+
      6px 100%,
+
      6px calc(100% - 2px),
+
      4px calc(100% - 2px),
+
      4px calc(100% - 4px),
+
      2px calc(100% - 4px),
+
      2px calc(100% - 6px),
+
      0 calc(100% - 6px)
+
    );
+
  }
+
  .breadcrumbs {
+
    gap: 0.5rem;
+
    margin-left: 1rem;
+
    min-height: 1.5rem;
  }
</style>

-
<header class="flex-item">
-
  <div class="wrapper flex-item">
-
    <div class="wrapper-left flex-item">
-
      <div class="flex-item navigation" style:gap="0.5rem">
-
        <Icon
-
          name="arrow-left"
-
          onclick={() => {
-
            window.history.back();
-
          }} />
-
        <Icon
-
          name="arrow-right"
-
          onclick={() => {
-
            window.history.forward();
-
          }} />
+
<div class="header global-flex">
+
  <div
+
    class="global-flex"
+
    style:flex-direction="column"
+
    style:width="100%"
+
    style:align-items="flex-start">
+
    <div class="wrapper global-flex">
+
      <div class="wrapper-left global-flex" style:gap="0">
+
        <div class="global-flex" style:gap="0">
+
          <NakedButton
+
            variant="ghost"
+
            onclick={() => {
+
              window.history.back();
+
            }}>
+
            <Icon name="arrow-left" />
+
          </NakedButton>
+
          <NakedButton
+
            variant="ghost"
+
            onclick={() => {
+
              window.history.forward();
+
            }}>
+
            <Icon name="arrow-right" />
+
          </NakedButton>
+
        </div>
+
        <slot name="icon-left" />
      </div>
-
      <slot name="icon-left" />
-
    </div>

-
    <slot name="center" />
+
      <slot name="center" />

-
    <div class="flex-item" style:gap="0.5rem">
-
      <Border variant="ghost" stylePadding="0 0.5rem" styleHeight="32px">
-
        <Icon name="offline" />
-
        <span class="txt-small txt-semibold">Offline</span>
-
      </Border>
-
      <Popover popoverPositionRight="0" popoverPositionTop="3rem">
-
        <Border
-
          slot="toggle"
-
          let:toggle
-
          onclick={toggle}
-
          variant="ghost"
-
          stylePadding="0 0.25rem"
-
          styleHeight="32px">
-
          <Icon name="more-vertical" />
-
        </Border>
-
        <Border variant="ghost" slot="popover" stylePadding="0.5rem 1rem">
-
          <div style="display: flex; gap: 2rem; align-items: center;">
-
            Theme <ThemeSwitch />
-
          </div>
-
        </Border>
-
      </Popover>
+
      <div class="global-flex" style:gap="0.5rem">
+
        <OutlineButton variant="ghost">
+
          <Icon name="offline" />
+
          Offline
+
        </OutlineButton>
+
        <Popover popoverPositionRight="0" popoverPositionTop="3rem">
+
          <NakedButton
+
            variant="ghost"
+
            slot="toggle"
+
            let:toggle
+
            onclick={toggle}>
+
            <Icon name="more-vertical" />
+
          </NakedButton>
+
          <Border variant="ghost" slot="popover" stylePadding="0.5rem 1rem">
+
            <div style="display: flex; gap: 2rem; align-items: center;">
+
              Theme <ThemeSwitch />
+
            </div>
+
          </Border>
+
        </Popover>
+
      </div>
+
    </div>
+
    <div class="global-flex txt-tiny txt-semibold breadcrumbs">
+
      <slot name="breadcrumbs" />
    </div>
  </div>
-

-
  <Background />
-
</header>
+
  <div class="bottom-pixel-corners"></div>
+
</div>
deleted src/components/Header/Background.svelte
@@ -1,52 +0,0 @@
-
<script lang="ts">
-
</script>
-

-
<style>
-
  .container {
-
    height: 3rem;
-
    z-index: -1;
-
    position: absolute;
-
    top: 0;
-
    left: 0.5rem;
-
    right: 0.5rem;
-
    display: grid;
-
    grid-template-columns: repeat(3, 2px) auto repeat(3, 2px);
-
    grid-template-rows: auto repeat(3, 2px);
-
  }
-
  .bg {
-
    background-color: var(--color-background-float);
-
  }
-
</style>
-

-
<div class="container">
-
  <div class="bg"></div>
-
  <div class="bg"></div>
-
  <div class="bg"></div>
-
  <div class="bg"></div>
-
  <div class="bg"></div>
-
  <div class="bg"></div>
-
  <div class="bg"></div>
-
  <div></div>
-
  <div class="bg"></div>
-
  <div class="bg"></div>
-
  <div class="bg"></div>
-
  <div class="bg"></div>
-
  <div class="bg"></div>
-
  <div></div>
-

-
  <div></div>
-
  <div></div>
-
  <div class="bg"></div>
-
  <div class="bg"></div>
-
  <div class="bg"></div>
-
  <div></div>
-
  <div></div>
-

-
  <div></div>
-
  <div></div>
-
  <div></div>
-
  <div class="bg"></div>
-
  <div></div>
-
  <div></div>
-
  <div></div>
-
</div>
modified src/components/Link.svelte
@@ -18,7 +18,7 @@

<style>
  a {
-
    color: var(--color-fill-secondary);
+
    color: var(--color-foreground-contrast);
    text-decoration: none;
  }
  a:hover {
added src/components/NakedButton.svelte
@@ -0,0 +1,228 @@
+
<script lang="ts">
+
  export let variant: "primary" | "secondary" | "ghost";
+
  export let onclick: (() => void) | undefined = undefined;
+

+
  $: style =
+
    `--button-color-1: var(--color-fill-${variant});` +
+
    `--button-color-2: var(--color-fill-${variant}-hover);` +
+
    `--button-color-3: var(--color-fill-${variant}-shade);` +
+
    // The ghost colors are called --color-fill-counter and --color-fill-counter-emphasized.
+
    `--button-color-4: var(--color-fill${variant === "ghost" ? "" : `-${variant}`}-counter)`;
+
</script>
+

+
<style>
+
  .pixel {
+
    background-color: transparent;
+
  }
+

+
  .p1-1 {
+
    grid-area: p1-1;
+
  }
+
  .p1-2 {
+
    grid-area: p1-2;
+
  }
+
  .p1-3 {
+
    grid-area: p1-3;
+
  }
+
  .p1-4 {
+
    grid-area: p1-4;
+
  }
+
  .p1-5 {
+
    grid-area: p1-5;
+
  }
+

+
  .p2-1 {
+
    grid-area: p2-1;
+
  }
+
  .p2-2 {
+
    grid-area: p2-2;
+
  }
+
  .p2-3 {
+
    grid-area: p2-3;
+
  }
+
  .p2-4 {
+
    grid-area: p2-4;
+
  }
+
  .p2-5 {
+
    grid-area: p2-5;
+
  }
+

+
  .p3-1 {
+
    grid-area: p3-1;
+
  }
+
  .p3-2 {
+
    grid-area: p3-2;
+
  }
+
  .p3-3 {
+
    grid-area: p3-3;
+
    padding: 2px 8px;
+
    display: flex;
+
    align-items: center;
+
    gap: 0.5rem;
+
  }
+
  .p3-4 {
+
    grid-area: p3-4;
+
  }
+
  .p3-5 {
+
    grid-area: p3-5;
+
  }
+

+
  .p4-1 {
+
    grid-area: p4-1;
+
  }
+
  .p4-2 {
+
    grid-area: p4-2;
+
  }
+
  .p4-3 {
+
    grid-area: p4-3;
+
  }
+
  .p4-4 {
+
    grid-area: p4-4;
+
  }
+
  .p4-5 {
+
    grid-area: p4-5;
+
  }
+

+
  .p5-1 {
+
    grid-area: p5-1;
+
  }
+
  .p5-2 {
+
    grid-area: p5-2;
+
  }
+
  .p5-3 {
+
    grid-area: p5-3;
+
  }
+
  .p5-4 {
+
    grid-area: p5-4;
+
  }
+
  .p5-5 {
+
    grid-area: p5-5;
+
  }
+

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

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

+
  .container:hover .p3-1 {
+
    background-color: var(--button-color-1);
+
  }
+
  .container:hover .p3-5 {
+
    background-color: var(--button-color-1);
+
  }
+

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

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

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

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

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

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

+
  .container {
+
    height: 32px;
+
    cursor: pointer;
+
    white-space: nowrap;
+

+
    -webkit-touch-callout: none;
+
    -webkit-user-select: none;
+
    user-select: none;
+

+
    column-gap: 0;
+
    row-gap: 0;
+
    display: grid;
+
    grid-template-columns: 2px 2px auto 2px 2px;
+
    grid-template-rows: 2px 2px auto 2px 2px;
+
    grid-template-areas:
+
      "p1-1 p1-2 p1-3 p1-4 p1-5"
+
      "p2-1 p2-2 p2-3 p2-4 p2-5"
+
      "p3-1 p3-2 p3-3 p3-4 p3-5"
+
      "p4-1 p4-2 p4-3 p4-4 p4-5"
+
      "p5-1 p5-2 p5-3 p5-4 p5-5";
+
  }
+
</style>
+

+
<!-- svelte-ignore a11y-click-events-have-key-events -->
+
<div class="container" {onclick} role="button" tabindex="0" {style}>
+
  <div class="pixel p1-1"></div>
+
  <div class="pixel p1-2"></div>
+
  <div class="pixel p1-3"></div>
+
  <div class="pixel p1-4"></div>
+
  <div class="pixel p1-5"></div>
+

+
  <div class="pixel p2-1"></div>
+
  <div class="pixel p2-2"></div>
+
  <div class="pixel p2-3"></div>
+
  <div class="pixel p2-4"></div>
+
  <div class="pixel p2-5"></div>
+

+
  <div class="pixel p3-1"></div>
+
  <div class="pixel p3-2"></div>
+
  <div class="pixel p3-3 txt-semibold txt-small"><slot /></div>
+
  <div class="pixel p3-4"></div>
+
  <div class="pixel p3-5"></div>
+

+
  <div class="pixel p4-1"></div>
+
  <div class="pixel p4-2"></div>
+
  <div class="pixel p4-3"></div>
+
  <div class="pixel p4-4"></div>
+
  <div class="pixel p4-5"></div>
+

+
  <div class="pixel p5-1"></div>
+
  <div class="pixel p5-2"></div>
+
  <div class="pixel p5-3"></div>
+
  <div class="pixel p5-4"></div>
+
  <div class="pixel p5-5"></div>
+
</div>
added src/components/NodeId.svelte
@@ -0,0 +1,40 @@
+
<script lang="ts">
+
  import { truncateId } from "@app/lib/utils";
+

+
  import Avatar from "./Avatar.svelte";
+

+
  export let nodeId: string;
+
  export let alias: string | undefined = undefined;
+
  export let styleFontSize: string | undefined = "var(--font-size-small)";
+
  export let styleFontFamily: string | undefined =
+
    "var(--font-family-monospace)";
+
</script>
+

+
<style>
+
  .avatar-alias {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.375rem;
+
    height: 1rem;
+
    font-weight: var(--font-weight-semibold);
+
  }
+
  .no-alias {
+
    color: var(--color-foreground-dim);
+
  }
+
</style>
+

+
<div
+
  class="avatar-alias"
+
  style:font-size={styleFontSize}
+
  style:font-family={styleFontFamily}>
+
  <Avatar {nodeId} />
+
  {#if alias}
+
    <span class="txt-overflow">
+
      {alias}
+
    </span>
+
  {:else}
+
    <span class="no-alias">
+
      {truncateId(nodeId)}
+
    </span>
+
  {/if}
+
</div>
added src/components/OutlineButton.svelte
@@ -0,0 +1,251 @@
+
<script lang="ts">
+
  export let variant: "primary" | "secondary" | "ghost";
+
  export let onclick: (() => void) | undefined = undefined;
+

+
  $: style =
+
    `--button-color-1: var(--color-fill-${variant});` +
+
    `--button-color-2: var(--color-fill-${variant}-hover);` +
+
    `--button-color-3: var(--color-fill-${variant}-shade);` +
+
    // The ghost colors are called --color-fill-counter and --color-fill-counter-emphasized.
+
    `--button-color-4: var(--color-fill${variant === "ghost" ? "" : `-${variant}`}-counter)`;
+
</script>
+

+
<style>
+
  .pixel {
+
    background-color: transparent;
+
  }
+

+
  .p1-1 {
+
    grid-area: p1-1;
+
  }
+
  .p1-2 {
+
    grid-area: p1-2;
+
  }
+
  .p1-3 {
+
    grid-area: p1-3;
+
    background-color: var(--button-color-1);
+
  }
+
  .p1-4 {
+
    grid-area: p1-4;
+
  }
+
  .p1-5 {
+
    grid-area: p1-5;
+
  }
+

+
  .p2-1 {
+
    grid-area: p2-1;
+
  }
+
  .p2-2 {
+
    grid-area: p2-2;
+
    background-color: var(--button-color-1);
+
  }
+
  .p2-3 {
+
    grid-area: p2-3;
+
  }
+
  .p2-4 {
+
    grid-area: p2-4;
+
    background-color: var(--button-color-1);
+
  }
+
  .p2-5 {
+
    grid-area: p2-5;
+
  }
+

+
  .p3-1 {
+
    grid-area: p3-1;
+
    background-color: var(--button-color-1);
+
  }
+
  .p3-2 {
+
    grid-area: p3-2;
+
  }
+
  .p3-3 {
+
    grid-area: p3-3;
+
    padding: 2px 8px;
+
    display: flex;
+
    align-items: center;
+
    gap: 0.5rem;
+
  }
+
  .p3-4 {
+
    grid-area: p3-4;
+
  }
+
  .p3-5 {
+
    grid-area: p3-5;
+
    background-color: var(--button-color-1);
+
  }
+

+
  .p4-1 {
+
    grid-area: p4-1;
+
  }
+
  .p4-2 {
+
    grid-area: p4-2;
+
    background-color: var(--button-color-1);
+
  }
+
  .p4-3 {
+
    grid-area: p4-3;
+
  }
+
  .p4-4 {
+
    grid-area: p4-4;
+
    background-color: var(--button-color-1);
+
  }
+
  .p4-5 {
+
    grid-area: p4-5;
+
  }
+

+
  .p5-1 {
+
    grid-area: p5-1;
+
  }
+
  .p5-2 {
+
    grid-area: p5-2;
+
  }
+
  .p5-3 {
+
    grid-area: p5-3;
+
    background-color: var(--button-color-1);
+
  }
+
  .p5-4 {
+
    grid-area: p5-4;
+
  }
+
  .p5-5 {
+
    grid-area: p5-5;
+
  }
+

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

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

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

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

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

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

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

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

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

+
  .container {
+
    height: 32px;
+
    cursor: pointer;
+
    white-space: nowrap;
+

+
    -webkit-touch-callout: none;
+
    -webkit-user-select: none;
+
    user-select: none;
+

+
    column-gap: 0;
+
    row-gap: 0;
+
    display: grid;
+
    grid-template-columns: 2px 2px auto 2px 2px;
+
    grid-template-rows: 2px 2px auto 2px 2px;
+
    grid-template-areas:
+
      "p1-1 p1-2 p1-3 p1-4 p1-5"
+
      "p2-1 p2-2 p2-3 p2-4 p2-5"
+
      "p3-1 p3-2 p3-3 p3-4 p3-5"
+
      "p4-1 p4-2 p4-3 p4-4 p4-5"
+
      "p5-1 p5-2 p5-3 p5-4 p5-5";
+
  }
+
</style>
+

+
<!-- svelte-ignore a11y-click-events-have-key-events -->
+
<div class="container" {onclick} role="button" tabindex="0" {style}>
+
  <div class="pixel p1-1"></div>
+
  <div class="pixel p1-2"></div>
+
  <div class="pixel p1-3"></div>
+
  <div class="pixel p1-4"></div>
+
  <div class="pixel p1-5"></div>
+

+
  <div class="pixel p2-1"></div>
+
  <div class="pixel p2-2"></div>
+
  <div class="pixel p2-3"></div>
+
  <div class="pixel p2-4"></div>
+
  <div class="pixel p2-5"></div>
+

+
  <div class="pixel p3-1"></div>
+
  <div class="pixel p3-2"></div>
+
  <div class="pixel p3-3 txt-semibold txt-small"><slot /></div>
+
  <div class="pixel p3-4"></div>
+
  <div class="pixel p3-5"></div>
+

+
  <div class="pixel p4-1"></div>
+
  <div class="pixel p4-2"></div>
+
  <div class="pixel p4-3"></div>
+
  <div class="pixel p4-4"></div>
+
  <div class="pixel p4-5"></div>
+

+
  <div class="pixel p5-1"></div>
+
  <div class="pixel p5-2"></div>
+
  <div class="pixel p5-3"></div>
+
  <div class="pixel p5-4"></div>
+
  <div class="pixel p5-5"></div>
+
</div>
added src/lib/blockies.ts
@@ -0,0 +1,125 @@
+
// Copyright (c) 2019, Ethereum Name Service
+

+
// The random number is a js implementation of the Xorshift PRNG
+
const randseed = new Array(4); // Xorshift: [x, y, z, w] 32 bit values
+

+
function seedrand(seed: string) {
+
  for (let i = 0; i < randseed.length; i++) {
+
    randseed[i] = 0;
+
  }
+
  for (let i = 0; i < seed.length; i++) {
+
    randseed[i % 4] =
+
      (randseed[i % 4] << 5) - randseed[i % 4] + seed.charCodeAt(i);
+
  }
+
}
+

+
function rand(): number {
+
  // Based on Java's String.hashCode(), expanded to 4 32bit values.
+
  const t = randseed[0] ^ (randseed[0] << 11);
+

+
  randseed[0] = randseed[1];
+
  randseed[1] = randseed[2];
+
  randseed[2] = randseed[3];
+
  randseed[3] = randseed[3] ^ (randseed[3] >> 19) ^ t ^ (t >> 8);
+

+
  return (randseed[3] >>> 0) / ((1 << 31) >>> 0);
+
}
+

+
function createColor(): string {
+
  // Saturation is the whole color spectrum.
+
  const h = Math.floor(rand() * 360);
+
  // Saturation goes from 40 to 100, it avoids greyish colors.
+
  const s = rand() * 60 + 40 + "%";
+
  // Lightness can be anything from 0 to 100, but probabilities are a bell curve around 50%.
+
  const l = (rand() + rand() + rand() + rand()) * 25 + "%";
+

+
  return `hsl(${h}, ${s}, ${l})`;
+
}
+

+
function createImageData(size: number): number[] {
+
  const width = size;
+
  const height = size;
+

+
  const dataWidth = Math.ceil(width / 2);
+
  const mirrorWidth = width - dataWidth;
+

+
  const data = [];
+
  for (let y = 0; y < height; y++) {
+
    let row = [];
+
    for (let x = 0; x < dataWidth; x++) {
+
      // this makes foreground and background color to have a 43% (1/2.3) probability
+
      // spot color has 13% chance
+
      row[x] = Math.floor(rand() * 2.3);
+
    }
+
    const r = row.slice(0, mirrorWidth);
+
    r.reverse();
+
    row = row.concat(r);
+

+
    for (let i = 0; i < row.length; i++) {
+
      data.push(row[i]);
+
    }
+
  }
+

+
  return data;
+
}
+

+
function createCanvas(
+
  imageData: number[],
+
  color: string,
+
  scale: number,
+
  bgcolor: string,
+
  spotcolor: string,
+
): HTMLCanvasElement {
+
  const c = document.createElement("canvas");
+
  const width = Math.sqrt(imageData.length);
+
  c.width = c.height = width * scale;
+

+
  const cc = c.getContext("2d");
+

+
  if (!cc) throw new Error("Can't get 2D context");
+

+
  cc.fillStyle = bgcolor;
+
  cc.fillRect(0, 0, c.width, c.height);
+
  cc.fillStyle = color;
+

+
  for (let i = 0; i < imageData.length; i++) {
+
    const row = Math.floor(i / width);
+
    const col = i % width;
+
    // if data is 2, choose spot color, if 1 choose foreground
+
    cc.fillStyle = imageData[i] === 1 ? color : spotcolor;
+

+
    // if data is 0, leave the background
+
    if (imageData[i]) {
+
      cc.fillRect(col * scale, row * scale, scale, scale);
+
    }
+
  }
+

+
  return c;
+
}
+

+
export interface Options {
+
  seed: string;
+
  size: number;
+
  scale: number;
+
  color?: string;
+
  bgcolor?: string;
+
  spotcolor?: string;
+
}
+

+
export function createIcon(opts: Options): HTMLCanvasElement {
+
  opts = opts || {};
+
  const size = opts.size || 8;
+
  const scale = opts.scale || 4;
+
  const seed =
+
    opts.seed || Math.floor(Math.random() * Math.pow(10, 16)).toString(16);
+

+
  seedrand(seed);
+

+
  const color = opts.color || createColor();
+
  const bgcolor = opts.bgcolor || createColor();
+
  const spotcolor = opts.spotcolor || createColor();
+
  const imageData = createImageData(size);
+
  const canvas = createCanvas(imageData, color, scale, bgcolor, spotcolor);
+

+
  return canvas;
+
}
modified src/views/Home.svelte
@@ -7,42 +7,96 @@
  import CopyableId from "@app/components/CopyableId.svelte";
  import Header from "@app/components/Header.svelte";
  import RepoCard from "@app/components/RepoCard.svelte";
+
  import NodeId from "@app/components/NodeId.svelte";

  export let repos: RepoInfo[];
  export let config: Config;
</script>

<style>
-
  .layout {
-
    padding: 1rem;
+
  .header {
+
    position: sticky;
+
    top: 0;
+
    z-index: 1;
  }
  .repo-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(21rem, 1fr));
    gap: 1rem;
  }
+

+
  .hero-image {
+
    background-image: url("aliens.png");
+
    background-position: center;
+
    background-size: cover;
+
    height: 9.5rem;
+
    clip-path: polygon(
+
      0 6px,
+
      2px 6px,
+
      2px 4px,
+
      4px 4px,
+
      4px 2px,
+
      6px 2px,
+
      6px 0,
+
      calc(100% - 6px) 0,
+
      calc(100% - 6px) 2px,
+
      calc(100% - 4px) 2px,
+
      calc(100% - 4px) 4px,
+
      calc(100% - 2px) 4px,
+
      calc(100% - 2px) 6px,
+
      100% 6px,
+
      100% calc(100% - 6px),
+
      calc(100% - 2px) calc(100% - 6px),
+
      calc(100% - 2px) calc(100% - 4px),
+
      calc(100% - 4px) calc(100% - 4px),
+
      calc(100% - 4px) calc(100% - 2px),
+
      calc(100% - 6px) calc(100% - 2px),
+
      calc(100% - 6px) 100%,
+
      6px 100%,
+
      6px calc(100% - 2px),
+
      4px calc(100% - 2px),
+
      4px calc(100% - 4px),
+
      2px calc(100% - 4px),
+
      2px calc(100% - 6px),
+
      0 calc(100% - 6px)
+
    );
+
  }
</style>

-
<Header>
-
  <svelte:fragment slot="center">
-
    <CopyableId id={`did:key:${config.publicKey}`} />
-
  </svelte:fragment>
-
</Header>
-
<div class="layout">
-
  <div class="repo-grid">
-
    {#each repos as repo}
-
      {#if repo.payloads["xyz.radicle.project"]}
-
        <RepoCard
-
          {repo}
-
          selfDid={`did:key:${config.publicKey}`}
-
          onclick={() => {
-
            void router.push({
-
              resource: "repo.issues",
-
              rid: repo.rid,
-
              status: "open",
-
            });
-
          }} />
-
      {/if}
-
    {/each}
+
<div style:height="fit-content">
+
  <div class="header">
+
    <Header>
+
      <svelte:fragment slot="center">
+
        <CopyableId id={`did:key:${config.publicKey}`} />
+
      </svelte:fragment>
+
      <svelte:fragment slot="breadcrumbs">
+
        <NodeId
+
          nodeId={config.publicKey}
+
          alias={config.alias}
+
          styleFontFamily="var(--font-family-sans-serif)"
+
          styleFontSize="var(--font-size-tiny)" />
+
      </svelte:fragment>
+
    </Header>
+
  </div>
+
  <div style:padding="1rem">
+
    <div class="hero-image"></div>
+

+
    <div class="txt-semibold" style:margin="1.5rem 0">Repositories</div>
+
    <div class="repo-grid">
+
      {#each repos as repo}
+
        {#if repo.payloads["xyz.radicle.project"]}
+
          <RepoCard
+
            {repo}
+
            selfDid={`did:key:${config.publicKey}`}
+
            onclick={() => {
+
              void router.push({
+
                resource: "repo.issues",
+
                rid: repo.rid,
+
                status: "open",
+
              });
+
            }} />
+
        {/if}
+
      {/each}
+
    </div>
  </div>
</div>
modified src/views/repo/Issues.svelte
@@ -1,14 +1,36 @@
<script lang="ts">
+
  import type { Config } from "@bindings/Config";
  import type { Issue } from "@bindings/Issue";
  import type { RepoInfo } from "@bindings/RepoInfo";

  import Layout from "./Layout.svelte";

+
  import Icon from "@app/components/Icon.svelte";
+
  import Link from "@app/components/Link.svelte";
+
  import NodeId from "@app/components/NodeId.svelte";
+

  export let repo: RepoInfo;
  export let issues: Issue[];
+
  export let config: Config;
+

+
  $: project = repo.payloads["xyz.radicle.project"]!;
</script>

<Layout {repo}>
+
  <svelte:fragment slot="breadcrumbs">
+
    <Link route={{ resource: "home" }}>
+
      <NodeId
+
        nodeId={config.publicKey}
+
        alias={config.alias}
+
        styleFontFamily="var(--font-family-sans-serif)"
+
        styleFontSize="var(--font-size-tiny)" />
+
    </Link>
+
    <Icon name="chevron-right" />
+
    {project.data.name}
+
    <Icon name="chevron-right" />
+
    Issues
+
  </svelte:fragment>
+

  <pre>
    <!-- prettier-ignore -->
    {#each issues as issue}
modified src/views/repo/Layout.svelte
@@ -5,42 +5,56 @@
  import Header from "@app/components/Header.svelte";
  import Icon from "@app/components/Icon.svelte";
  import Link from "@app/components/Link.svelte";
+
  import NakedButton from "@app/components/NakedButton.svelte";

  export let repo: RepoInfo;
-

-
  $: project = repo.payloads["xyz.radicle.project"]!;
</script>

-
<Header>
-
  <svelte:fragment slot="icon-left">
-
    <Icon name="sidebar" />
-
  </svelte:fragment>
-
  <svelte:fragment slot="center">
-
    <CopyableId id={repo.rid} />
-
  </svelte:fragment>
-
</Header>
-
<div>{project.data.name}</div>
+
<style>
+
  .header {
+
    position: sticky;
+
    top: 0;
+
  }
+
</style>
+

+
<div style:height="fit-content">
+
  <div class="header">
+
    <Header>
+
      <svelte:fragment slot="icon-left">
+
        <NakedButton variant="ghost">
+
          <Icon name="sidebar" />
+
        </NakedButton>
+
      </svelte:fragment>
+
      <svelte:fragment slot="center">
+
        <CopyableId id={repo.rid} />
+
      </svelte:fragment>
+
      <svelte:fragment slot="breadcrumbs">
+
        <slot name="breadcrumbs" />
+
      </svelte:fragment>
+
    </Header>
+
  </div>

-
Issues
-
<Link route={{ resource: "repo.issues", rid: repo.rid, status: "open" }}>
-
  Open
-
</Link>
-
<Link route={{ resource: "repo.issues", rid: repo.rid, status: "closed" }}>
-
  Closed
-
</Link>
+
  Issues
+
  <Link route={{ resource: "repo.issues", rid: repo.rid, status: "open" }}>
+
    Open
+
  </Link>
+
  <Link route={{ resource: "repo.issues", rid: repo.rid, status: "closed" }}>
+
    Closed
+
  </Link>

-
<br />
-
Patches
-
<Link route={{ resource: "repo.patches", rid: repo.rid, status: "draft" }}>
-
  Draft
-
</Link>
-
<Link route={{ resource: "repo.patches", rid: repo.rid, status: "open" }}>
-
  Open
-
</Link>
-
<Link route={{ resource: "repo.patches", rid: repo.rid, status: "archived" }}>
-
  Archived
-
</Link>
-
<Link route={{ resource: "repo.patches", rid: repo.rid, status: "merged" }}>
-
  Merged
-
</Link>
-
<slot />
+
  <br />
+
  Patches
+
  <Link route={{ resource: "repo.patches", rid: repo.rid, status: "draft" }}>
+
    Draft
+
  </Link>
+
  <Link route={{ resource: "repo.patches", rid: repo.rid, status: "open" }}>
+
    Open
+
  </Link>
+
  <Link route={{ resource: "repo.patches", rid: repo.rid, status: "archived" }}>
+
    Archived
+
  </Link>
+
  <Link route={{ resource: "repo.patches", rid: repo.rid, status: "merged" }}>
+
    Merged
+
  </Link>
+
  <slot />
+
</div>
modified src/views/repo/Patches.svelte
@@ -1,14 +1,35 @@
<script lang="ts">
+
  import type { Config } from "@bindings/Config";
  import type { Patch } from "@bindings/Patch";
  import type { RepoInfo } from "@bindings/RepoInfo";

  import Layout from "./Layout.svelte";

+
  import Icon from "@app/components/Icon.svelte";
+
  import Link from "@app/components/Link.svelte";
+
  import NodeId from "@app/components/NodeId.svelte";
+

  export let repo: RepoInfo;
  export let patches: Patch[];
+
  export let config: Config;
+

+
  $: project = repo.payloads["xyz.radicle.project"]!;
</script>

<Layout {repo}>
+
  <svelte:fragment slot="breadcrumbs">
+
    <Link route={{ resource: "home" }}>
+
      <NodeId
+
        nodeId={config.publicKey}
+
        alias={config.alias}
+
        styleFontFamily="var(--font-family-sans-serif)"
+
        styleFontSize="var(--font-size-tiny)" />
+
    </Link>
+
    <Icon name="chevron-right" />
+
    {project.data.name}
+
    <Icon name="chevron-right" />
+
    Patches
+
  </svelte:fragment>
  <pre>
    <!-- prettier-ignore -->
    {#each patches as patch}
modified src/views/repo/router.ts
@@ -1,6 +1,7 @@
-
import type { RepoInfo } from "@bindings/RepoInfo";
-
import type { Patch } from "@bindings/Patch";
+
import type { Config } from "@bindings/Config";
import type { Issue } from "@bindings/Issue";
+
import type { Patch } from "@bindings/Patch";
+
import type { RepoInfo } from "@bindings/RepoInfo";

import { invoke } from "@tauri-apps/api/core";
import { unreachable } from "@app/lib/utils";
@@ -13,7 +14,7 @@ export interface RepoIssuesRoute {

export interface LoadedRepoIssuesRoute {
  resource: "repo.issues";
-
  params: { repo: RepoInfo; issues: Issue[] };
+
  params: { repo: RepoInfo; config: Config; issues: Issue[] };
}

export interface RepoPatchesRoute {
@@ -24,7 +25,7 @@ export interface RepoPatchesRoute {

export interface LoadedRepoPatchesRoute {
  resource: "repo.patches";
-
  params: { repo: RepoInfo; patches: Patch[] };
+
  params: { repo: RepoInfo; config: Config; patches: Patch[] };
}

export type RepoRoute = RepoIssuesRoute | RepoPatchesRoute;
@@ -34,24 +35,26 @@ export async function loadPatches(route: RepoRoute): Promise<LoadedRepoRoute> {
  const repo: RepoInfo = await invoke("repo_by_id", {
    rid: route.rid,
  });
+
  const config: Config = await invoke("config");
  const patches: Patch[] = await invoke("list_patches", {
    rid: route.rid,
    status: route.status,
  });

-
  return { resource: "repo.patches", params: { repo, patches } };
+
  return { resource: "repo.patches", params: { repo, config, patches } };
}

export async function loadIssues(route: RepoRoute): Promise<LoadedRepoRoute> {
  const repo: RepoInfo = await invoke("repo_by_id", {
    rid: route.rid,
  });
+
  const config: Config = await invoke("config");
  const issues: Issue[] = await invoke("list_issues", {
    rid: route.rid,
    status: route.status,
  });

-
  return { resource: "repo.issues", params: { repo, issues } };
+
  return { resource: "repo.issues", params: { repo, config, issues } };
}

export function repoRouteToPath(route: RepoRoute): string {