Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
Add pixelated Button component
Open rudolfs opened 1 year ago

This experiment explores building components with a pixelated look with CSS grids.

17 files changed +1240 -117 7e666475 bb68d6c5
modified eslint.config.js
@@ -134,6 +134,7 @@ export default [
  },
  {
    ignores: [
+
      "build/*",
      "isolation/*",
      "node_modules/**/*",
      "src-tauri/**/*",
modified index.html
@@ -1,24 +1,66 @@
<!doctype html>
<html lang="en">
+
  <head>
+
    <meta charset="UTF-8" />
+
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+
    <title>Radicle</title>
+
    <link
+
      rel="preload"
+
      href="/fonts/Inter-Regular.woff2"
+
      as="font"
+
      type="font/woff2"
+
      crossorigin="anonymous" />
+
    <link
+
      rel="preload"
+
      href="/fonts/Inter-Medium.woff2"
+
      as="font"
+
      type="font/woff2"
+
      crossorigin="anonymous" />
+
    <link
+
      rel="preload"
+
      href="/fonts/Inter-SemiBold.woff2"
+
      as="font"
+
      type="font/woff2"
+
      crossorigin="anonymous" />
+
    <link
+
      rel="preload"
+
      href="/fonts/Inter-Bold.woff2"
+
      as="font"
+
      type="font/woff2"
+
      crossorigin="anonymous" />
+
    <link
+
      rel="preload"
+
      href="/fonts/JetBrainsMono-Regular.woff2"
+
      as="font"
+
      type="font/woff2"
+
      crossorigin="anonymous" />
+
    <link
+
      rel="preload"
+
      href="/fonts/JetBrainsMono-Medium.woff2"
+
      as="font"
+
      type="font/woff2"
+
      crossorigin="anonymous" />
+
    <link
+
      rel="preload"
+
      href="/fonts/JetBrainsMono-SemiBold.woff2"
+
      as="font"
+
      type="font/woff2"
+
      crossorigin="anonymous" />
+
    <link
+
      rel="preload"
+
      href="/fonts/JetBrainsMono-Bold.woff2"
+
      as="font"
+
      type="font/woff2"
+
      crossorigin="anonymous" />

-
<head>
-
  <meta charset="UTF-8" />
-
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-
  <title>Radicle</title>
+
    <link rel="stylesheet" type="text/css" href="/index.css" />
+
    <link rel="stylesheet" type="text/css" href="/typography.css" />
+
    <link rel="stylesheet" type="text/css" href="/colors.css" />
+
  </head>

-
  <link rel="preload" href="/fonts/Inter-Regular.woff2" as="font" type="font/woff2" crossorigin="anonymous" />
-
  <link rel="preload" href="/fonts/Inter-Medium.woff2" as="font" type="font/woff2" crossorigin="anonymous" />
-
  <link rel="preload" href="/fonts/Inter-SemiBold.woff2" as="font" type="font/woff2" crossorigin="anonymous" />
-
  <link rel="preload" href="/fonts/Inter-Bold.woff2" as="font" type="font/woff2" crossorigin="anonymous" />
-

-
  <link rel="stylesheet" href="/index.css">
-
  <link rel="stylesheet" href="/colors.css">
-
  <link rel="stylesheet" href="/typography.css">
-
</head>
-

-
<body>
-
  <div id="app"></div>
-
  <script type="module" src="/src/main.ts"></script>
-
</body>
-

-
</html>

\ No newline at end of file
+
  <body>
+
    <div id="app"></div>
+
    <script type="module" src="/src/main.ts"></script>
+
  </body>
+
</html>
modified isolation/index.html
@@ -1,13 +1,11 @@
<!doctype html>
<html lang="en">
-

-
<head>
+
  <head>
    <meta charset="UTF-8" />
    <title>Isolation Secure Script</title>
-
</head>
+
  </head>

-
<body>
+
  <body>
    <script src="index.js"></script>
-
</body>
-

-
</html>

\ No newline at end of file
+
  </body>
+
</html>
modified public/colors.css
@@ -1,9 +1,121 @@
:root {
+
  --color-background-default: #f5f5ff;
+
  --color-background-float: #fafaff;
+
  --color-background-dip: #f5f5ff;
+
  --color-foreground-contrast: #232563;
+
  --color-foreground-dim: #6a6a81;
+
  --color-foreground-emphasized: #8585ff;
+
  --color-foreground-emphasized-hover: #7070ff;
+
  --color-foreground-match-background: #f5f5ff;
+
  --color-foreground-white: #ffffff;
+
  --color-foreground-black: #000000;
+
  --color-foreground-primary: #ff55ff;
+
  --color-foreground-primary-hover: #ff8fff;
+
  --color-foreground-success: #4fa877;
+
  --color-foreground-red: #aa5078;
+
  --color-foreground-yellow: #b29401;
+
  --color-foreground-disabled: #9b9bb1;
+
  --color-border-hint: #dbdbff;
+
  --color-border-default: #dbdbff;
+
  --color-border-focus: #7070ff;
+
  --color-border-contrast: #24252d;
+
  --color-border-error: #ce97af;
+
  --color-border-merged: #ffe5ff;
+
  --color-border-match-background: #f5f5ff;
+
  --color-border-primary: #ff1aff;
+
  --color-border-primary-hover: #ff55ff;
+
  --color-border-selected: #dbdbff;
+
  --color-border-warning: #ffe609;
+
  --color-border-success: #97ceb0;
+
  --color-fill-secondary: #7070ff;
+
  --color-fill-secondary-hover: #8585ff;
+
  --color-fill-ghost: #ebebff;
+
  --color-fill-ghost-hover: #f5f5ff;
+
  --color-fill-separator: #dbdbff;
+
  --color-fill-primary: #ff55ff;
+
  --color-fill-primary-hover: #ff70ff;
+
  --color-fill-primary-shade: #ff1aff;
+
  --color-fill-danger: #be7495;
+
  --color-fill-yellow: #ffe609;
+
  --color-fill-yellow-iconic: #ffff55;
+
  --color-fill-gray: #9b9bb1;
+
  --color-fill-secondary-shade: #5555ff;
+
  --color-fill-diff-red: #efdce4;
+
  --color-fill-diff-red-light: #f7eef2;
+
  --color-fill-success: #4fa877;
+
  --color-fill-diff-green: #badeca;
+
  --color-fill-diff-green-light: #dcefe5;
+
  --color-fill-float: #fafaff;
+
  --color-fill-float-hover: #dbdbff;
+
  --color-fill-merged: #ffeeff;
+
  --color-fill-selected: #ebebff;
+
  --color-fill-warning: #ffffe5;
+
  --color-fill-counter: #fafaff;
+
  --color-fill-counter-emphasized: #dbdbff;
+
  --color-fill-delegate: #ffe5ff;
+
  --color-fill-private: #fff5d6;
+
  --color-fill-secondary-counter: #9497ff;
+
  --color-fill-primary-counter: #ff8fff;
+
  --color-fill-ghost-shade: #dbdbff;
+
}
+

+
:root[data-theme="dark"] {
  --color-background-default: #0a0d10;
+
  --color-background-float: #14151a;
+
  --color-background-dip: #000000;
  --color-foreground-contrast: #f9f9fb;
+
  --color-foreground-dim: #9b9bb1;
+
  --color-foreground-emphasized: #7070ff;
+
  --color-foreground-emphasized-hover: #8585ff;
+
  --color-foreground-match-background: #0a0d10;
+
  --color-foreground-white: #ffffff;
+
  --color-foreground-black: #000000;
+
  --color-foreground-primary: #ff55ff;
+
  --color-foreground-primary-hover: #ff8fff;
+
  --color-foreground-success: #4fa877;
+
  --color-foreground-red: #aa5078;
+
  --color-foreground-yellow: #e5c001;
  --color-foreground-disabled: #6a6a81;
+
  --color-border-hint: #24252d;
  --color-border-default: #2e2f38;
-
  --color-background-float: #14151a;
-
  --color-fill-float-hover: #1b1c22;
+
  --color-border-focus: #7070ff;
+
  --color-border-contrast: #ebebff;
+
  --color-border-error: #6b2b42;
+
  --color-border-merged: #6b006b;
+
  --color-border-match-background: #0a0d10;
+
  --color-border-primary: #ff1aff;
+
  --color-border-primary-hover: #ff55ff;
+
  --color-border-selected: #232563;
+
  --color-border-warning: #4c4000;
+
  --color-border-success: #2a5a40;
+
  --color-fill-secondary: #7070ff;
+
  --color-fill-secondary-hover: #8585ff;
+
  --color-fill-secondary-shade: #5555ff;
  --color-fill-ghost: #24252d;
+
  --color-fill-ghost-hover: #2e2f38;
+
  --color-fill-separator: #24252d;
+
  --color-fill-primary: #ff1aff;
+
  --color-fill-primary-hover: #ff4dff;
+
  --color-fill-primary-shade: #e500e5;
+
  --color-fill-danger: #aa5078;
+
  --color-fill-yellow: #ffe609;
+
  --color-fill-yellow-iconic: #ffff55;
+
  --color-fill-gray: #9b9bb1;
+
  --color-fill-diff-red: #4d1929;
+
  --color-fill-diff-red-light: #2d060d;
+
  --color-fill-success: #4fa877;
+
  --color-fill-diff-green: #183425;
+
  --color-fill-diff-green-light: #142a1d;
+
  --color-fill-float: #14151a;
+
  --color-fill-float-hover: #1b1c22;
+
  --color-fill-merged: #1a001a;
+
  --color-fill-selected: #16173d;
+
  --color-fill-warning: #191500;
+
  --color-fill-counter: #393a46;
+
  --color-fill-counter-emphasized: #16173d;
+
  --color-fill-delegate: #3d003d;
+
  --color-fill-private: #4c4000;
+
  --color-fill-secondary-counter: #9497ff;
+
  --color-fill-primary-counter: #ff8fff;
+
  --color-fill-ghost-shade: #1b1c22;
}
deleted public/images/warning.png
modified public/typography.css
@@ -5,18 +5,21 @@
  font-display: swap;
  src: url("fonts/Inter-Regular.woff2");
}
+

@font-face {
  font-family: "Inter";
  font-weight: 500;
  font-display: swap;
  src: url("fonts/Inter-Medium.woff2");
}
+

@font-face {
  font-family: "Inter";
  font-weight: 600;
  font-display: swap;
  src: url("fonts/Inter-SemiBold.woff2");
}
+

@font-face {
  font-family: "Inter";
  font-weight: 700;
@@ -24,20 +27,79 @@
  src: url("fonts/Inter-Bold.woff2");
}

+
@font-face {
+
  font-family: "JetBrains Mono";
+
  font-style: normal;
+
  font-weight: 400;
+
  font-display: swap;
+
  src: url("fonts/JetBrainsMono-Regular.woff2");
+
}
+

+
@font-face {
+
  font-family: "JetBrains Mono";
+
  font-style: normal;
+
  font-weight: 500;
+
  font-display: swap;
+
  src: url("fonts/JetBrainsMono-Medium.woff2");
+
}
+

+
@font-face {
+
  font-family: "JetBrains Mono";
+
  font-weight: 600;
+
  font-display: swap;
+
  src: url("fonts/JetBrainsMono-SemiBold.woff2");
+
}
+

+
@font-face {
+
  font-family: "JetBrains Mono";
+
  font-weight: 700;
+
  font-display: swap;
+
  src: url("fonts/JetBrainsMono-Bold.woff2");
+
}
+

:root {
  --font-family-sans-serif: Inter, sans-serif;
+
  --font-family-monospace: monospace;
+
  --font-weight-regular: 400;
+
  --font-weight-medium: 500;
+
  --font-weight-semibold: 600;
+
  --font-weight-bold: 700;
+
  --font-size-tiny: 0.75rem; /* 12px */
  --font-size-small: 0.875rem; /* 14px */
  --font-size-regular: 1rem; /* 16px */
  --font-size-medium: 1.25rem; /* 20px */
-
  --font-weight-semibold: 600;
+
  --font-size-large: 1.5rem; /* 24px */
+
  --font-size-x-large: 2rem; /* 32px */
+
  --font-size-xx-large: 3rem; /* 48px */
+
}
+

+
[data-codefont="system"] {
+
  --font-family-monospace: monospace;
+
}
+

+
[data-codefont="jetbrains"] {
+
  --font-family-monospace: "JetBrains Mono";
}

html {
+
  -ms-text-size-adjust: 100%;
+
  -webkit-font-smoothing: antialiased;
+
  -webkit-text-size-adjust: 100%;
  font-family: var(--font-family-sans-serif);
+
  font-feature-settings: "zero";
+
  /* The root element font size has to be set in px,
+
   * otherwise Safari breaks. */
  font-size: 16px;
  font-weight: var(--font-weight-regular);
+
  line-height: 1.5;
}

+
p {
+
  margin: 1rem 0;
+
}
+
.txt-tiny {
+
  font-size: var(--font-size-tiny);
+
}
.txt-small {
  font-size: var(--font-size-small);
}
@@ -47,7 +109,36 @@ html {
.txt-medium {
  font-size: var(--font-size-medium);
}
+
.txt-large {
+
  font-size: var(--font-size-large);
+
}
+
.txt-huge {
+
  font-size: var(--font-size-x-large);
+
}
+
.txt-humongous {
+
  font-size: var(--font-size-xx-large);
+
}

+
.txt-monospace {
+
  font-family: var(--font-family-monospace);
+
}
+
.txt-bold {
+
  font-weight: var(--font-weight-bold) !important;
+
}
.txt-semibold {
  font-weight: var(--font-weight-semibold) !important;
}
+
.txt-missing {
+
  color: var(--color-foreground-dim);
+
}
+
.txt-emoji {
+
  height: 1em;
+
  width: 1em;
+
  margin: 0 0.05em 0 0.1em;
+
  vertical-align: -0.1em;
+
}
+
.txt-overflow {
+
  overflow: hidden;
+
  text-overflow: ellipsis;
+
  white-space: nowrap;
+
}
modified src/App.svelte
@@ -1,11 +1,16 @@
<script lang="ts">
  import { Router, Route } from "svelte-routing";
+
  import { theme } from "@app/components/ThemeSwitch.svelte";

-
  import Startup from "@app/views/Startup.svelte";
+
  import DesignSystem from "@app/views/DesignSystem.svelte";
  import Repos from "@app/views/Repos.svelte";
+
  import Startup from "@app/views/Startup.svelte";
+

+
  $: document.documentElement.setAttribute("data-theme", $theme);
</script>

<Router>
  <Route path="/" component={Startup} />
  <Route path="/repos" component={Repos} />
+
  <Route path="/design-system" component={DesignSystem} />
</Router>
added src/components/Border.svelte
@@ -0,0 +1,166 @@
+
<script lang="ts">
+
  export let variant: "primary" | "secondary" | "ghost";
+
  export let stylePadding: string | undefined = undefined;
+
  export let styleHeight: string | undefined = undefined;
+

+
  $: style = `--button-color-1: var(--color-fill-${variant});`;
+
</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;
+
    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 {
+
    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"
+
  on:click
+
  role="button"
+
  tabindex="0"
+
  {style}
+
  style:height={styleHeight}>
+
  <div class="pixel p1-1" />
+
  <div class="pixel p1-2" />
+
  <div class="pixel p1-3" />
+
  <div class="pixel p1-4" />
+
  <div class="pixel p1-5" />
+

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

+
  <div class="pixel p3-1" />
+
  <div class="pixel p3-2" />
+
  <div class="pixel p3-3 txt-semibold txt-small" style:padding={stylePadding}>
+
    <slot />
+
  </div>
+
  <div class="pixel p3-4" />
+
  <div class="pixel p3-5" />
+

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

+
  <div class="pixel p5-1" />
+
  <div class="pixel p5-2" />
+
  <div class="pixel p5-3" />
+
  <div class="pixel p5-4" />
+
  <div class="pixel p5-5" />
+
</div>
added src/components/Button.svelte
@@ -0,0 +1,255 @@
+
<script lang="ts">
+
  export let variant: "primary" | "secondary" | "ghost";
+

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

+
  .p4-1 {
+
    grid-area: p4-1;
+
  }
+
  .p4-2 {
+
    grid-area: p4-2;
+
    background-color: var(--button-color-1);
+
  }
+
  .p4-3 {
+
    grid-area: p4-3;
+
    background-color: var(--button-color-3);
+
  }
+
  .p4-4 {
+
    grid-area: p4-4;
+
    background-color: var(--button-color-3);
+
  }
+
  .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-3);
+
  }
+
  .p5-4 {
+
    grid-area: p5-4;
+
  }
+
  .p5-5 {
+
    grid-area: p5-5;
+
  }
+

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

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

+
  .container:hover .p3-1 {
+
    background-color: var(--button-color-2);
+
  }
+
  .container:hover .p3-2 {
+
    background-color: var(--button-color-4);
+
  }
+
  .container:hover .p3-3 {
+
    background-color: var(--button-color-2);
+
  }
+
  .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-2);
+
  }
+
  .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" on:click role="button" tabindex="0" {style}>
+
  <div class="pixel p1-1" />
+
  <div class="pixel p1-2" />
+
  <div class="pixel p1-3" />
+
  <div class="pixel p1-4" />
+
  <div class="pixel p1-5" />
+

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

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

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

+
  <div class="pixel p5-1" />
+
  <div class="pixel p5-2" />
+
  <div class="pixel p5-3" />
+
  <div class="pixel p5-4" />
+
  <div class="pixel p5-5" />
+
</div>
added src/components/Fill.svelte
@@ -0,0 +1,95 @@
+
<script lang="ts">
+
  export let variant: "primary" | "secondary" | "ghost" | "transparent";
+
  export let stylePadding: string | undefined = undefined;
+
  export let styleHeight: string | undefined = undefined;
+

+
  $: style =
+
    variant === "transparent"
+
      ? "--button-color-1: transparent"
+
      : `--button-color-1: var(--color-fill-${variant});`;
+
</script>
+

+
<style>
+
  .pixel {
+
    background-color: var(--button-color-1);
+
  }
+

+
  .p1-1 {
+
    grid-area: p1-1;
+
    background-color: transparent;
+
  }
+
  .p1-2 {
+
    grid-area: p1-2;
+
  }
+
  .p1-3 {
+
    grid-area: p1-3;
+
    background-color: transparent;
+
  }
+

+
  .p2-1 {
+
    grid-area: p2-1;
+
  }
+
  .p2-2 {
+
    grid-area: p2-2;
+
    display: flex;
+
    align-items: center;
+
    gap: 0.5rem;
+
  }
+
  .p2-3 {
+
    grid-area: p2-3;
+
  }
+

+
  .p3-1 {
+
    grid-area: p3-1;
+
    background-color: transparent;
+
  }
+
  .p3-2 {
+
    grid-area: p3-2;
+
  }
+
  .p3-3 {
+
    grid-area: p3-3;
+
    background-color: transparent;
+
  }
+

+
  .container {
+
    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 auto 2px;
+
    grid-template-rows: 2px auto 2px;
+
    grid-template-areas:
+
      "p1-1 p1-2 p1-3"
+
      "p2-1 p2-2 p2-3"
+
      "p3-1 p3-2 p3-3";
+
  }
+
</style>
+

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

+
  <div class="pixel p2-1" />
+
  <div class="pixel p2-2 txt-semibold txt-small" style:padding={stylePadding}>
+
    <slot />
+
  </div>
+
  <div class="pixel p2-3" />
+

+
  <div class="pixel p3-1" />
+
  <div class="pixel p3-2" />
+
  <div class="pixel p3-3" />
+
</div>
modified src/components/Header.svelte
@@ -1,7 +1,10 @@
<script lang="ts">
  import Background from "./Header/Background.svelte";
+
  import Border from "./Border.svelte";
+
  import Fill from "./Fill.svelte";
  import Icon from "./Icon.svelte";
-
  import Label from "./Label.svelte";
+
  import Popover from "./Popover.svelte";
+
  import ThemeSwitch from "./ThemeSwitch.svelte";
</script>

<style>
@@ -11,7 +14,7 @@
  }
  header {
    padding: 0 0.5rem;
-
    gap: 1.5rem;
+
    gap: 0.25rem;
    height: 3rem;
  }
  .wrapper {
@@ -32,20 +35,36 @@
        <Icon name="arrow-left" />
        <Icon name="arrow-right" />
      </div>
-
      <Label
-
        styleBorderColor="var(--color-fill-ghost)"
-
        styleFillColor="var(--color-fill-ghost)">
+
      <Fill variant="ghost" stylePadding="0 0.5rem" styleHeight="32px">
        Repositories
-
      </Label>
-
      <Label styleBorderColor="var(--color-fill-ghost)">
+
      </Fill>
+
      <Border variant="ghost" stylePadding="0 0.25rem" styleHeight="32px">
        <Icon name="plus" />
-
      </Label>
+
      </Border>
    </div>
-
    <div class="flex-item">
-
      <Label styleBorderColor="var(--color-fill-ghost)">
+

+
    <div class="flex-item" style:gap="0.5rem">
+
      <Border variant="ghost" stylePadding="0 0.5rem" styleHeight="32px">
        <Icon name="offline" /> Offline
-
      </Label>
+
      </Border>
+
      <Popover popoverPositionRight="0" popoverPositionTop="3rem">
+
        <Border
+
          slot="toggle"
+
          let:toggle
+
          on:click={toggle}
+
          variant="ghost"
+
          stylePadding="0 0.25rem"
+
          styleHeight="32px">
+
          <Icon name="settings" />
+
        </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>
  </div>
+

  <Background />
</header>
modified src/components/Icon.svelte
@@ -1,6 +1,25 @@
<script lang="ts">
  import { unreachable } from "@app/lib/utils";
-
  export let name: "arrow-left" | "arrow-right" | "plus" | "offline";
+

+
  export let size: "16" | "32" = "16";
+

+
  export let name:
+
    | "arrow-left"
+
    | "arrow-right"
+
    | "chevron-right"
+
    | "dashboard"
+
    | "delegate"
+
    | "diff"
+
    | "file"
+
    | "inbox"
+
    | "moon"
+
    | "offline"
+
    | "plus"
+
    | "repo"
+
    | "seedling"
+
    | "settings"
+
    | "sun"
+
    | "warning";
</script>

<style>
@@ -15,8 +34,8 @@
<svg
  role="img"
  on:click
-
  width="16"
-
  height="16"
+
  width={size}
+
  height={size}
  fill="currentColor"
  viewBox="0 0 16 16">
  {#if name === "arrow-left"}
@@ -39,9 +58,102 @@
    <path d="M10 3H9.00003V13H10V3Z" />
    <path d="M13 6H12V7H13V6Z" />
    <path d="M14 7H13V8H14V7Z" />
-
  {:else if name === "plus"}
-
    <path d="M7.00002 2H9.00002V14H7.00002V2Z" />
-
    <path d="M14 7V9L2.00002 9L2.00002 7L14 7Z" />
+
  {:else if name === "chevron-right"}
+
    <path d="M9 7L10 7L10 8L9 8L9 7Z" />
+
    <path d="M8 6H9V7H8L8 6Z" />
+
    <path d="M7 5L8 5V6L7 6L7 5Z" />
+
    <path d="M6 4L7 4L7 5L6 5L6 4Z" />
+
    <path d="M5 3L6 3L6 4H5L5 3Z" />
+
    <path d="M5 12H6L6 13H5L5 12Z" />
+
    <path d="M6 11H7L7 12H6L6 11Z" />
+
    <path d="M7 10L8 10L8 11L7 11L7 10Z" />
+
    <path d="M9 8L10 8V9L9 9V8Z" />
+
    <path d="M8 9H9V10H8L8 9Z" />
+
  {:else if name === "dashboard"}
+
    <path d="M2 11H14V12H2V11Z" />
+
    <path d="M2 9H3V11H2L2 9Z" />
+
    <path d="M14 9H13V11L14 11V9Z" />
+
    <path d="M10 4V5L6 5V4L10 4Z" />
+
    <path d="M3 7H4V9L3 9L3 7Z" />
+
    <path d="M13 7H12V9H13V7Z" />
+
    <path d="M4 6H5V7H4L4 6Z" />
+
    <path d="M12 6H11V7H12V6Z" />
+
    <path d="M5 5L6 5V6H5V5Z" />
+
    <path d="M11 5L10 5V6H11V5Z" />
+
    <path d="M7 8H8V10H7V8Z" />
+
    <path d="M8 6H9V8H8V6Z" />
+
    <path d="M6 10H10V11H6V10Z" />
+
  {:else if name === "delegate"}
+
    <path d="M2.33301 9L2.33301 7H3.33301V9H2.33301Z" />
+
    <path d="M3.33301 11L3.33301 9H4.33301L4.33301 11H3.33301Z" />
+
    <path d="M4.33301 12L4.33301 11H7.33301V12H4.33301Z" />
+
    <path d="M7.33301 11L7.33301 10H8.33301V11H7.33301Z" />
+
    <path d="M7.33301 6V5H8.33301V6H7.33301Z" />
+
    <path d="M4.33301 5V4L7.33301 4V5L4.33301 5Z" />
+
    <path d="M3.33301 7L3.33301 5L4.33301 5L4.33301 7H3.33301Z" />
+
    <path d="M4.33301 9L4.33301 7L5.33301 7V9L4.33301 9Z" />
+
    <path d="M7.33301 9V7H8.33301V9H7.33301Z" />
+
    <path d="M8.33301 7L8.33301 6L13.333 6V7L8.33301 7Z" />
+
    <path d="M8.33301 10L8.33301 9L10.333 9V10L8.33301 10Z" />
+
    <path d="M13.333 9V7L14.333 7V9H13.333Z" />
+
    <path d="M10.333 8H11.333V9L10.333 9V8Z" />
+
    <path d="M11.333 9L13.333 9V10H11.333L11.333 9Z" />
+
    <path d="M5.33301 6L7.33301 6L7.33301 7L5.33301 7L5.33301 6Z" />
+
    <path d="M5.33301 9L7.33301 9L7.33301 10H5.33301V9Z" />
+
  {:else if name === "diff"}
+
    <path d="M2 3H3V13H2V3Z" />
+
    <path d="M3 13H12V14H3L3 13Z" />
+
    <path d="M3 2H12V3L3 3L3 2Z" />
+
    <path d="M12 3L13 3V13H12V3Z" />
+
    <path d="M7 4H8V9H7V4Z" />
+
    <path d="M5 6H10V7H5V6Z" />
+
    <path d="M5 10H10V11H5V10Z" />
+
  {:else if name === "file"}
+
    <path d="M10 4H11V5H10V4Z" />
+
    <path d="M11 5L12 5V6H11V5Z" />
+
    <path d="M10 6L12 6V7H10V6Z" />
+
    <path d="M9 6H10V7H9V6Z" />
+
    <path d="M8 5H9L9 6L8 6V5Z" />
+
    <path d="M8 3H9V5H8V3Z" />
+
    <path d="M9 3L10 3V4H9V3Z" />
+
    <path d="M4 13H12V14H4V13Z" />
+
    <path d="M4 2H9V3L4 3V2Z" />
+
    <path d="M13 6V13H12V6L13 6Z" />
+
    <path d="M4 3V13H3L3 3L4 3Z" />
+
  {:else if name === "inbox"}
+
    <path d="M2 3H3V13H2V3Z" />
+
    <path d="M13 3H14V13H13V3Z" />
+
    <path d="M3 7H5V8H3V7Z" />
+
    <path d="M11 7H14V8H11V7Z" />
+
    <path d="M5 8H6V9H5L5 8Z" />
+
    <path d="M10 8H11V9H10V8Z" />
+
    <path d="M6 9H10L10 10H6L6 9Z" />
+
    <path d="M3 13H13V14H3L3 13Z" />
+
    <path d="M3 2H13V3H3L3 2Z" />
+
  {:else if name === "moon"}
+
    <path d="M4 3H6V4H4V3Z" />
+
    <path d="M3 4L4 4L4 6H3V4Z" />
+
    <path d="M2 6L3 6L3 10H2V6Z" />
+
    <path d="M3 10H4V12H3L3 10Z" />
+
    <path d="M4 12H6V13H4L4 12Z" />
+
    <path d="M6 13H10V14H6L6 13Z" />
+
    <path d="M6 13H10V14H6L6 13Z" />
+
    <path d="M12 10H13V12H12V10Z" />
+
    <path d="M8 9H10V10H8V9Z" />
+
    <path d="M6 6H7V7H6V6Z" />
+
    <path d="M6 7H7V8H6V7Z" />
+
    <path d="M7 8H8V9H7V8Z" />
+
    <path d="M10 10H12V11H10V10Z" />
+
    <path d="M5 5H6V6H5V5Z" />
+
    <path d="M10 12H12V13H10L10 12Z" />
+
    <path d="M5 4H6L6 5H5L5 4Z" />
+
    <path d="M13 9H14V11H13V9Z" />
+
    <path d="M5 2H7V3H5V2Z" />
+
    <path d="M5 2H7V3H5V2Z" />
+
    <path d="M5 2H7V3H5V2Z" />
+
    <path d="M11 4H12V7H11V4Z" />
+
    <path d="M10 5H13V6H10V5Z" />
+
    <path d="M9 3H10V4H9V3Z" />
  {:else if name === "offline"}
    <path d="M3 6L3 8H2L2 6H3Z" />
    <path d="M13 10V8H14V10H13Z" />
@@ -80,6 +192,120 @@
    <path d="M4 11L5 11V12L4 12L4 11Z" />
    <path d="M3 12H4L4 13H3L3 12Z" />
    <path d="M2 13L3 13L3 14H2V13Z" />
+
  {:else if name === "plus"}
+
    <path d="M7.00002 2H9.00002V14H7.00002V2Z" />
+
    <path d="M14 7V9L2.00002 9L2.00002 7L14 7Z" />
+
  {:else if name === "repo"}
+
    <path d="M13 5H14V7H13V5Z" />
+
    <path d="M13 10H14V11H13V10Z" />
+
    <path d="M13 7H14V5H13V7Z" />
+
    <path d="M13 9H14V8H13V9Z" />
+
    <path d="M3 7H2L2 5H3L3 7Z" />
+
    <path d="M3 8H2L2 9H3L3 8Z" />
+
    <path d="M3 5H2L2 7H3L3 5Z" />
+
    <path d="M3 10L2 10L2 11H3V10Z" />
+
    <path d="M11 4H13V5L11 5V4Z" />
+
    <path d="M11 8L13 8V7H11V8Z" />
+
    <path d="M11 10H13V9H11V10Z" />
+
    <path d="M11 12H13V11H11V12Z" />
+
    <path d="M9 3H11V4L9 4V3Z" />
+
    <path d="M9 9L11 9V8H9V9Z" />
+
    <path d="M9 11H11V10H9V11Z" />
+
    <path d="M9 13H11V12H9V13Z" />
+
    <path d="M8 2H9V3L8 3V2Z" />
+
    <path d="M8 10L9 10V9H8V10Z" />
+
    <path d="M8 12L9 12V11L8 11V12Z" />
+
    <path d="M8 14H9V13H8V14Z" />
+
    <path d="M7 2L8 2V3L7 3V2Z" />
+
    <path d="M7 10L8 10V9L7 9V10Z" />
+
    <path d="M7 12L8 12V11L7 11V12Z" />
+
    <path d="M7 14H8V13H7V14Z" />
+
    <path d="M5 5L3 5L3 4L5 4V5Z" />
+
    <path d="M5 7H3L3 8L5 8L5 7Z" />
+
    <path d="M5 9H3L3 10H5L5 9Z" />
+
    <path d="M5 11H3L3 12H5V11Z" />
+
    <path d="M7 4L5 4V3H7L7 4Z" />
+
    <path d="M7 8H5V9L7 9L7 8Z" />
+
    <path d="M7 10H5L5 11H7L7 10Z" />
+
    <path d="M7 12H5L5 13H7L7 12Z" />
+
  {:else if name === "seedling"}
+
    <path d="M10.333 6H9.33301V5L10.333 5V6Z" />
+
    <path d="M11.333 5L10.333 5V4L11.333 4V5Z" />
+
    <path d="M12.333 4H11.333L11.333 3L12.333 3L12.333 4Z" />
+
    <path d="M13.333 4L12.333 4L12.333 3L13.333 3L13.333 4Z" />
+
    <path d="M14.333 5L13.333 5V4L14.333 4V5Z" />
+
    <path d="M13.333 6H12.333V5H13.333V6Z" />
+
    <path d="M5.33301 7L5.33301 6L4.33301 6L4.33301 7L5.33301 7Z" />
+
    <path d="M4.33301 8V7L3.33301 7L3.33301 8L4.33301 8Z" />
+
    <path d="M3.33301 9L3.33301 8L2.33301 8L2.33301 9H3.33301Z" />
+
    <path d="M3.33301 10L3.33301 9H2.33301L2.33301 10H3.33301Z" />
+
    <path d="M4.33301 11L4.33301 10H3.33301L3.33301 11L4.33301 11Z" />
+
    <path d="M10.333 7H9.33301L9.33301 6H10.333L10.333 7Z" />
+
    <path d="M9.33301 8H8.33301V7L9.33301 7L9.33301 8Z" />
+
    <path d="M8.33301 8H7.33301V7H8.33301V8Z" />
+
    <path d="M11.333 8L10.333 8L10.333 7L11.333 7V8Z" />
+
    <path d="M12.333 8L10.333 8L10.333 7L12.333 7V8Z" />
+
    <path d="M13.333 7H12.333V6H13.333V7Z" />
+
    <path d="M5.33301 10L5.33301 9H4.33301L4.33301 10L5.33301 10Z" />
+
    <path d="M9.33301 8H8.33301V11H9.33301V8Z" />
+
    <path d="M8.33301 11L7.33301 11V14H8.33301V11Z" />
+
    <path d="M6.33301 7L6.33301 6H5.33301L5.33301 7H6.33301Z" />
+
    <path d="M7.33301 8V7L6.33301 7L6.33301 8H7.33301Z" />
+
    <path d="M7.33301 9L7.33301 7L6.33301 7L6.33301 9H7.33301Z" />
+
    <path d="M6.33301 10V9H5.33301L5.33301 10H6.33301Z" />
+
  {:else if name === "settings"}
+
    <path d="M7 5H14V6H7V5Z" />
+
    <path d="M9 11L2 11L2 10L9 10V11Z" />
+
    <path d="M2 5H5V6H2V5Z" />
+
    <path d="M14 11H11V10H14V11Z" />
+
    <path d="M9 8H11V9H9V8Z" />
+
    <path d="M5 3H7V4H5V3Z" />
+
    <path d="M9 12H11V13H9V12Z" />
+
    <path d="M5 7H7V8H5V7Z" />
+
    <path d="M8 9L9 9V12H8V9Z" />
+
    <path d="M4 4L5 4L5 7H4V4Z" />
+
    <path d="M11 9L12 9V12H11V9Z" />
+
    <path d="M7 4L8 4V7L7 7V4Z" />
+
  {:else if name === "sun"}
+
    <path d="M8 2H9V3H8V2Z" />
+
    <path d="M14 8V9H13V8H14Z" />
+
    <path d="M4 7V8H3V7H4Z" />
+
    <path d="M7 12H8V13H7V12Z" />
+
    <path d="M8 3H9L9 4H8L8 3Z" />
+
    <path d="M13 8V9H12V8H13Z" />
+
    <path d="M3 7V8H2L2 7H3Z" />
+
    <path d="M7 13H8V14H7V13Z" />
+
    <path d="M7 5H9V6H7V5Z" />
+
    <path d="M7 10H9V11H7V10Z" />
+
    <path d="M5 7H6V9H5V7Z" />
+
    <path d="M10 7H11V9H10V7Z" />
+
    <path d="M11 5H12V6H11V5Z" />
+
    <path d="M10 12H11V11H10V12Z" />
+
    <path d="M6 4H5V5H6V4Z" />
+
    <path d="M5 11H4V10H5V11Z" />
+
    <path d="M12 4H13V5H12V4Z" />
+
    <path d="M11 13H12V12H11V13Z" />
+
    <path d="M5 3H4V4H5L5 3Z" />
+
    <path d="M4 12H3V11H4L4 12Z" />
+
    <path d="M6 6H7V7L6 7V6Z" />
+
    <path d="M6 9L7 9L7 10H6V9Z" />
+
    <path d="M9 9L10 9L10 10H9L9 9Z" />
+
    <path d="M9 6H10V7H9V6Z" />
+
  {:else if name === "warning"}
+
    <path d="M7 2H9V3H7V2Z" />
+
    <path d="M6 3H7V5H6V3Z" />
+
    <path d="M5 5H6V7H5V5Z" />
+
    <path d="M4 7H5V9H4V7Z" />
+
    <path d="M3 9H4V11H3V9Z" />
+
    <path d="M2 11H3V13H2V11Z" />
+
    <path d="M10 3H9V5H10V3Z" />
+
    <path d="M11 5H10V7H11V5Z" />
+
    <path d="M12 7H11V9H12V7Z" />
+
    <path d="M13 9H12V11H13V9Z" />
+
    <path d="M14 11H13V13H14V11Z" />
+
    <path d="M3 13H13V14H3V13Z" />
+
    <path d="M7 6H9V10H7V6Z" />
+
    <path d="M7 11H9V12H7V11Z" />
  {:else}
    {unreachable(name)}
  {/if}
deleted src/components/Label.svelte
@@ -1,67 +0,0 @@
-
<script lang="ts">
-
  export let styleBorderColor: string;
-
  export let styleFillColor: string = "transparent";
-
  export let emptyPixels: 2 | 1 = 2;
-

-
  let gridTemplate = `${"2px ".repeat(emptyPixels)}auto ${"2px ".repeat(emptyPixels)}`;
-
</script>
-

-
<style>
-
  .content {
-
    padding: 2px 8px;
-
    display: flex;
-
    gap: 6px;
-
    align-items: center;
-
  }
-
  .container {
-
    white-space: nowrap;
-
    user-select: none;
-
    column-gap: 0;
-
    height: 2rem;
-
    row-gap: 0;
-
    display: grid;
-
  }
-
</style>
-

-
<div
-
  class="container"
-
  style:grid-template-columns={gridTemplate}
-
  style:grid-template-rows={gridTemplate}>
-
  {@html "<div></div>".repeat(emptyPixels)}
-
  <div style:background-color={styleBorderColor} />
-
  {@html "<div></div>".repeat(emptyPixels)}
-

-
  {#if emptyPixels === 2}
-
    <div />
-
    <div style:background-color={styleBorderColor} />
-
    <div style:background-color={styleFillColor} />
-
    <div style:background-color={styleBorderColor} />
-
    <div />
-
  {/if}
-

-
  <div style:background-color={styleBorderColor} />
-
  {#if emptyPixels === 2}
-
    <div style:background-color={styleFillColor} />
-
  {/if}
-
  <div
-
    class="content txt-semibold txt-small"
-
    style:background-color={styleFillColor}>
-
    <slot />
-
  </div>
-
  {#if emptyPixels === 2}
-
    <div style:background-color={styleFillColor} />
-
  {/if}
-
  <div style:background-color={styleBorderColor} />
-

-
  {#if emptyPixels === 2}
-
    <div />
-
    <div style:background-color={styleBorderColor} />
-
    <div style:background-color={styleFillColor} />
-
    <div style:background-color={styleBorderColor} />
-
    <div />
-
  {/if}
-

-
  {@html "<div></div>".repeat(emptyPixels)}
-
  <div style:background-color={styleBorderColor} />
-
  {@html "<div></div>".repeat(emptyPixels)}
-
</div>
added src/components/Popover.svelte
@@ -0,0 +1,70 @@
+
<script lang="ts" context="module">
+
  import { writable } from "svelte/store";
+
  const focused = writable<HTMLDivElement | undefined>(undefined);
+

+
  export function closeFocused() {
+
    focused.set(undefined);
+
  }
+
</script>
+

+
<script lang="ts">
+
  export let popoverContainerMinWidth: string | undefined = undefined;
+
  export let popoverBorderRadius: string | undefined = undefined;
+
  export let popoverPadding: string | undefined = undefined;
+
  export let popoverPositionBottom: string | undefined = undefined;
+
  export let popoverPositionLeft: string | undefined = undefined;
+
  export let popoverPositionRight: string | undefined = undefined;
+
  export let popoverPositionTop: string | undefined = undefined;
+

+
  export let expanded = false;
+
  let thisComponent: HTMLDivElement;
+

+
  function clickOutside(ev: MouseEvent | TouchEvent) {
+
    if ($focused && !ev.composedPath().includes($focused)) {
+
      closeFocused();
+
    }
+
  }
+

+
  function toggle() {
+
    expanded = !expanded;
+
    if ($focused === thisComponent) {
+
      closeFocused();
+
    } else {
+
      focused.set(thisComponent);
+
    }
+
  }
+

+
  $: expanded = $focused === thisComponent;
+
</script>
+

+
<style>
+
  .container {
+
    position: relative;
+
  }
+
  .popover {
+
    position: absolute;
+
    z-index: 10;
+
  }
+
</style>
+

+
<svelte:window on:click={clickOutside} on:touchstart={clickOutside} />
+

+
<div
+
  bind:this={thisComponent}
+
  class="container"
+
  style:min-width={popoverContainerMinWidth}>
+
  <slot name="toggle" {expanded} {toggle} />
+

+
  {#if expanded}
+
    <div
+
      class="popover"
+
      style:bottom={popoverPositionBottom}
+
      style:left={popoverPositionLeft}
+
      style:right={popoverPositionRight}
+
      style:top={popoverPositionTop}
+
      style:padding={popoverPadding}
+
      style:border-radius={popoverBorderRadius}>
+
      <slot name="popover" {toggle} />
+
    </div>
+
  {/if}
+
</div>
added src/components/ThemeSwitch.svelte
@@ -0,0 +1,65 @@
+
<script lang="ts" context="module">
+
  type Theme = "dark" | "light";
+

+
  export const theme = writable<Theme>(loadTheme());
+

+
  function loadTheme(): Theme {
+
    const { matches } = window.matchMedia("(prefers-color-scheme: dark)");
+
    const storedTheme = localStorage ? localStorage.getItem("theme") : null;
+

+
    if (storedTheme === null) {
+
      return matches ? "dark" : "light";
+
    } else {
+
      return storedTheme as Theme;
+
    }
+
  }
+

+
  export function storeTheme(newTheme: Theme): void {
+
    theme.set(newTheme);
+
    if (localStorage) {
+
      localStorage.setItem("theme", newTheme);
+
    } else {
+
      console.warn(
+
        "localStorage isn't available, not able to persist the selected theme without it.",
+
      );
+
    }
+
  }
+
</script>
+

+
<script lang="ts">
+
  import { writable } from "svelte/store";
+

+
  import Border from "./Border.svelte";
+
  import Fill from "./Fill.svelte";
+
  import Icon from "./Icon.svelte";
+
</script>
+

+
<div style="display: flex; gap: 1rem;">
+
  <Border styleHeight="32px" variant="secondary">
+
    <Fill
+
      stylePadding="0 0.5rem"
+
      variant={$theme === "dark" ? "secondary" : "transparent"}
+
      on:click={() => {
+
        storeTheme("dark");
+
      }}>
+
      <Icon name="moon" />
+
      Dark
+
    </Fill>
+

+
    <Fill
+
      stylePadding="0 0.5rem"
+
      variant={$theme === "light" ? "secondary" : "transparent"}
+
      on:click={() => {
+
        storeTheme("light");
+
      }}>
+
      <span
+
        style="display: flex; align-items: center; gap: 0.5rem"
+
        style:color={$theme === "light"
+
          ? "var(--color-foreground-white)"
+
          : "var(--color-foreground-contrast)"}>
+
        <Icon name="sun" />
+
        Light
+
      </span>
+
    </Fill>
+
  </Border>
+
</div>
added src/views/DesignSystem.svelte
@@ -0,0 +1,42 @@
+
<script lang="ts">
+
  import Border from "@app/components/Border.svelte";
+
  import Button from "@app/components/Button.svelte";
+
  import Fill from "@app/components/Fill.svelte";
+
  import Icon from "@app/components/Icon.svelte";
+

+
  let theme = "dark";
+

+
  $: document.documentElement.setAttribute("data-theme", theme);
+
</script>
+

+
<div style="display: flex; gap: 1rem;">
+
  <div style="display: flex; gap: 1rem;">
+
    <Border variant="secondary">
+
      <Fill
+
        stylePadding="0 0.5rem"
+
        variant={theme === "dark" ? "secondary" : "transparent"}
+
        on:click={() => {
+
          theme = "dark";
+
        }}>
+
        <Icon name="seedling" />
+
        Dark
+
      </Fill>
+

+
      <Fill
+
        stylePadding="0 0.5rem"
+
        variant={theme === "light" ? "secondary" : "transparent"}
+
        on:click={() => {
+
          theme = "light";
+
        }}>
+
        <Icon name="seedling" />
+
        Light
+
      </Fill>
+
    </Border>
+
  </div>
+

+
  <div style="display: flex; gap: 1rem;">
+
    <Button variant="primary"><Icon name="seedling" />Press me</Button>
+
    <Button variant="secondary"><Icon name="seedling" /> Press me</Button>
+
    <Button variant="ghost"><Icon name="seedling" /> Press me</Button>
+
  </div>
+
</div>
modified src/views/Startup.svelte
@@ -3,7 +3,7 @@
  import { onMount } from "svelte";
  import { Link } from "svelte-routing";

-
  import warningIcon from "/images/warning.png";
+
  import Icon from "@app/components/Icon.svelte";

  let loading = true;
  let error: { err: string; hint?: string } | undefined = undefined;
@@ -11,6 +11,7 @@
  onMount(async () => {
    try {
      await invoke("authenticate");
+
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
    } catch (e: any) {
      error = e;
    } finally {
@@ -30,7 +31,9 @@

<main>
  {#if error}
-
    <img height="32" src={warningIcon} alt="warning" />
+
    <div style="display: flex; justify-content: center;">
+
      <Icon name="warning" size="32" />
+
    </div>
    <p class="txt-medium">{error.err}</p>
    {#if error.hint}
      <p class="txt-small">{@html error.hint}</p>