Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
Redesign app with new UI kit
Rūdolfs Ošiņš committed 1 month ago
commit 010cac037765db03a41ba7dc5b2763668c175d3f
parent 656ddd1
174 files changed +7877 -10466
modified .github/workflows/check-arch-package.yaml
@@ -1,10 +1,10 @@
on:
  pull_request:
    paths:
-
      - 'arch/**'
+
      - "arch/**"
  push:
    paths:
-
      - 'arch/**'
+
      - "arch/**"
  workflow_dispatch:

jobs:
modified crates/radicle-tauri/capabilities/default.json
@@ -15,6 +15,7 @@
    "core:tray:default",
    "core:webview:default",
    "core:window:allow-set-badge-count",
+
    "core:window:allow-start-dragging",
    "core:window:default",
    "dialog:default",
    "log:default",
modified crates/radicle-tauri/tauri.conf.json
@@ -14,7 +14,9 @@
      {
        "title": "Radicle",
        "minWidth": 960,
-
        "minHeight": 600
+
        "minHeight": 600,
+
        "titleBarStyle": "Overlay",
+
        "hiddenTitle": true
      }
    ],
    "security": {
modified crates/radicle-types/Cargo.toml
@@ -3,6 +3,9 @@ name = "radicle-types"
version = "0.1.0"
edition = "2021"

+
[lib]
+
doctest = false
+

[dependencies]
anyhow = { version = "1.0.90" }
axum = { version = "0.8.1", default-features = false, features = ["json"] }
modified crates/test-http-api/Cargo.toml
@@ -5,6 +5,9 @@ homepage = "https://radicle.xyz"
version = "0.1.0"
edition = "2021"

+
[lib]
+
doctest = false
+

[dependencies]
anyhow = { version = "1.0.90" }
axum = { version = "0.8.1", default-features = false, features = ["json", "query", "tokio", "http1"] }
modified index.html
@@ -3,9 +3,17 @@
  <head>
    <script>
      // Avoid flickering on app start.
-
      if (localStorage.getItem("theme") === "dark") {
+
      const storedTheme = localStorage.getItem("theme");
+
      if (storedTheme === "dark" || storedTheme === "light") {
+
        document.documentElement.setAttribute("data-theme", storedTheme);
+
      } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
        document.documentElement.setAttribute("data-theme", "dark");
      }
+
      const codefont = localStorage.getItem("codefont");
+
      document.documentElement.setAttribute(
+
        "data-codefont",
+
        codefont === "system" ? "system" : "jetbrains",
+
      );
    </script>
    <meta charset="UTF-8" />
    <link rel="icon" href="/radicle.svg" type="image/svg+xml" />
@@ -13,25 +21,19 @@
    <title>Radicle</title>
    <link
      rel="preload"
-
      href="/fonts/Inter-Regular.woff2"
+
      href="/fonts/Booton-Regular.woff2"
      as="font"
      type="font/woff2"
      crossorigin="anonymous" />
    <link
      rel="preload"
-
      href="/fonts/Inter-Medium.woff2"
+
      href="/fonts/Booton-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"
+
      href="/fonts/Booton-SemiBold.woff2"
      as="font"
      type="font/woff2"
      crossorigin="anonymous" />
modified package-lock.json
@@ -10,6 +10,7 @@
      "hasInstallScript": true,
      "license": "GPL-3.0-only",
      "dependencies": {
+
        "@floating-ui/dom": "^1.7.6",
        "@tauri-apps/api": "^2.5.0",
        "@tauri-apps/plugin-clipboard-manager": "^2.2.2",
        "@tauri-apps/plugin-dialog": "^2.2.1",
@@ -633,6 +634,31 @@
        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
      }
    },
+
    "node_modules/@floating-ui/core": {
+
      "version": "1.7.5",
+
      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
+
      "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
+
      "license": "MIT",
+
      "dependencies": {
+
        "@floating-ui/utils": "^0.2.11"
+
      }
+
    },
+
    "node_modules/@floating-ui/dom": {
+
      "version": "1.7.6",
+
      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
+
      "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
+
      "license": "MIT",
+
      "dependencies": {
+
        "@floating-ui/core": "^1.7.5",
+
        "@floating-ui/utils": "^0.2.11"
+
      }
+
    },
+
    "node_modules/@floating-ui/utils": {
+
      "version": "0.2.11",
+
      "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
+
      "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
+
      "license": "MIT"
+
    },
    "node_modules/@hapi/hoek": {
      "version": "9.3.0",
      "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
modified package.json
@@ -25,6 +25,7 @@
  },
  "license": "GPL-3.0-only",
  "dependencies": {
+
    "@floating-ui/dom": "^1.7.6",
    "@tauri-apps/api": "^2.5.0",
    "@tauri-apps/plugin-clipboard-manager": "^2.2.2",
    "@tauri-apps/plugin-dialog": "^2.2.1",
modified public/colors.css
@@ -1,133 +1,313 @@
+
/* Light theme. */
:root {
-
  --color-background-default: #ebebff;
-
  --color-background-float: #f5f5ff;
-
  --color-background-dip: #dbdbff;
-
  --color-foreground-contrast: #232563;
-
  --color-foreground-dim: #5c5c70;
-
  --color-foreground-emphasized: #7070ff;
-
  --color-foreground-emphasized-hover: #7070ff;
-
  --color-foreground-match-background: #ebebff;
-
  --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: #ccceff;
-
  --color-border-focus: #7070ff;
-
  --color-border-contrast: #25262d;
-
  --color-border-error: #ce97af;
-
  --color-border-merged: #ffe5ff;
-
  --color-border-match-background: #ebebff;
-
  --color-border-primary: #ff1aff;
-
  --color-border-primary-hover: #ff55ff;
-
  --color-border-selected: #dbdbff;
-
  --color-border-warning: #ffe609;
-
  --color-border-success: #97ceb0;
-
  --color-fill-secondary: #5555ff;
-
  --color-fill-secondary-hover: #7070ff;
-
  --color-fill-ghost: #dbdbff;
-
  --color-fill-ghost-hover: #ebebff;
-
  --color-fill-separator: #dbdbff;
-
  --color-fill-primary: #ff55ff;
-
  --color-fill-primary-hover: #ff70ff;
-
  --color-fill-primary-shade: #ff1aff;
-
  --color-fill-danger-shade: #8b3e5e;
-
  --color-fill-danger: #aa5078;
-
  --color-fill-danger-hover: #be7495;
-
  --color-fill-danger-counter: #ce97af;
-
  --color-fill-yellow: #ffe609;
-
  --color-fill-yellow-iconic: #ffff55;
-
  --color-fill-gray: #9b9bb1;
-
  --color-fill-secondary-shade: #4545ef;
-
  --color-fill-diff-red: #efdce4;
-
  --color-fill-diff-red-light: #f7eef2;
-
  --color-fill-success-counter: #97ceb0;
-
  --color-fill-success-hover: #71bc93;
-
  --color-fill-success: #4fa877;
-
  --color-fill-success-shade: #408760;
-
  --color-fill-diff-green: #badeca;
-
  --color-fill-diff-green-light: #dcefe5;
-
  --color-fill-float: #f5f5ff;
-
  --color-fill-float-hover: #fafaff;
-
  --color-fill-merged: #ffeeff;
-
  --color-fill-selected: #ebebff;
-
  --color-fill-warning: #ffffe5;
-
  --color-fill-counter: #dbdbff;
-
  --color-fill-counter-emphasized: #ebebff;
-
  --color-fill-delegate: #ffe5ff;
-
  --color-fill-private: #fff5d6;
-
  --color-fill-secondary-counter: #b2b5ff;
-
  --color-fill-primary-counter: #ff8fff;
-
  --color-fill-ghost-shade: #ccceff;
+
  --color-surface-base: var(--color-neutrals-opaque-light-50);
+
  --color-surface-canvas: var(--color-neutrals-opaque-light-0);
+
  --color-surface-subtle: var(--color-neutrals-opaque-light-100);
+
  --color-surface-mid: var(--color-neutrals-opaque-light-150);
+
  --color-surface-strong: var(--color-neutrals-opaque-light-200);
+
  --color-surface-alpha-subtle: var(--color-neutrals-alpha-light-50);
+
  --color-surface-alpha-mid: var(--color-neutrals-alpha-light-100);
+
  --color-surface-alpha-strong: var(--color-neutrals-alpha-light-200);
+
  --color-surface-scrim: var(--color-neutrals-alpha-light-500);
+
  --color-surface-open: var(--color-accent-green-200);
+
  --color-surface-merged: var(--color-accent-blue-200);
+
  --color-surface-draft: var(--color-neutrals-opaque-light-150);
+
  --color-surface-closed: var(--color-accent-blue-200);
+
  --color-surface-archived: var(--color-accent-pink-200);
+

+
  --color-text-primary: var(--color-neutrals-opaque-light-900);
+
  --color-text-secondary: var(--color-neutrals-opaque-light-700);
+
  --color-text-tertiary: var(--color-neutrals-opaque-light-600);
+
  --color-text-quaternary: var(--color-neutrals-opaque-light-500);
+
  --color-text-disabled: var(--color-neutrals-alpha-light-400);
+
  --color-text-open: var(--color-accent-green-800);
+
  --color-text-merged: var(--color-accent-blue-800);
+
  --color-text-draft: var(--color-neutrals-opaque-light-600);
+
  --color-text-closed: var(--color-accent-blue-800);
+
  --color-text-archived: var(--color-accent-pink-800);
+

+
  --color-border-subtle: var(--color-neutrals-opaque-light-150);
+
  --color-border-mid: var(--color-neutrals-opaque-light-200);
+
  --color-border-strong: var(--color-neutrals-opaque-light-300);
+
  --color-border-alpha-subtle: var(--color-neutrals-alpha-light-100);
+
  --color-border-alpha-mid: var(--color-neutrals-alpha-light-200);
+

+
  --color-feedback-success-text: var(--color-semantic-green-800);
+
  --color-feedback-success-border: var(--color-semantic-green-600);
+
  --color-feedback-success-bg: var(--color-semantic-green-100);
+
  --color-feedback-success-bg-selected: var(--color-semantic-green-200);
+
  --color-feedback-warning-text: var(--color-semantic-amber-800);
+
  --color-feedback-warning-border: var(--color-semantic-amber-600);
+
  --color-feedback-warning-bg: var(--color-semantic-amber-100);
+
  --color-feedback-error-text: var(--color-semantic-red-800);
+
  --color-feedback-error-border: var(--color-semantic-red-600);
+
  --color-feedback-error-bg: var(--color-semantic-red-100);
+
  --color-feedback-error-bg-selected: var(--color-semantic-red-200);
+

+
  --color-feedback-success-fill: var(--color-semantic-green-600);
+
  --color-feedback-success-fill-hover: var(--color-semantic-green-500);
+
  --color-feedback-success-fill-active: var(--color-semantic-green-700);
+
  --color-feedback-error-fill: var(--color-semantic-red-600);
+
  --color-feedback-error-fill-hover: var(--color-semantic-red-500);
+
  --color-feedback-error-fill-active: var(--color-semantic-red-700);
+

+
  --color-code-keywords: var(--color-accent-blue-700);
+
  --color-code-strings: var(--color-accent-green-700);
+
  --color-code-numbers: var(--color-accent-purple-700);
+
  --color-code-comments: var(--color-neutrals-opaque-light-700);
+
  --color-code-error: var(--color-semantic-red-700);
+
  --color-code-functions: var(--color-accent-emerald-700);
+

+
  /* Brand. */
+
  --color-brand-bg: var(--color-accent-blue-600);
+
  --color-brand-hover: var(--color-accent-blue-500);
+
  --color-brand-text-light: var(--color-accent-blue-600);
+
  --color-brand-text-dark: var(--color-accent-blue-400);
+
  --color-text-on-brand: var(--color-neutrals-opaque-light-0);
+

+
  --color-surface-brand-primary: var(--color-brand-bg);
+
  --color-surface-brand-secondary: var(--color-brand-hover);
+
  --color-border-brand: var(--color-brand-hover);
+
  --color-text-brand: var(--color-brand-text-light);
}

+
/* Dark theme. */
:root[data-theme="dark"] {
-
  --color-background-default: #0a0e0f;
-
  --color-background-float: #25262d;
-
  --color-background-dip: #000000;
-
  --color-foreground-contrast: #f9f9fb;
-
  --color-foreground-dim: #9b9bb1;
-
  --color-foreground-emphasized: #7070ff;
-
  --color-foreground-emphasized-hover: #b2b5ff;
-
  --color-foreground-match-background: #0a0e0f;
-
  --color-foreground-white: #ffffff;
-
  --color-foreground-black: #000000;
-
  --color-foreground-primary: #ff55ff;
-
  --color-foreground-primary-hover: #ff8fff;
-
  --color-foreground-success: #4fa877;
-
  --color-foreground-red: #be7495;
-
  --color-foreground-yellow: #e5c001;
-
  --color-foreground-disabled: #5c5c70;
-
  --color-border-hint: #2e2f38;
-
  --color-border-default: #393a46;
-
  --color-border-focus: #7070ff;
-
  --color-border-contrast: #ebebff;
-
  --color-border-error: #6b2b42;
-
  --color-border-merged: #6b006b;
-
  --color-border-match-background: #0a0e0f;
-
  --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: #b2b5ff;
-
  --color-fill-secondary-shade: #5555ff;
-
  --color-fill-ghost: #2e2f38;
-
  --color-fill-ghost-hover: #393a46;
-
  --color-fill-separator: #2e2f38;
-
  --color-fill-primary: #ff1aff;
-
  --color-fill-primary-hover: #ff4dff;
-
  --color-fill-primary-shade: #e500e5;
-
  --color-fill-danger-shade: #8b3e5e;
-
  --color-fill-danger: #aa5078;
-
  --color-fill-danger-hover: #be7495;
-
  --color-fill-danger-counter: #ce97af;
-
  --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-counter: #97ceb0;
-
  --color-fill-success-hover: #71bc93;
-
  --color-fill-success: #4fa877;
-
  --color-fill-success-shade: #408760;
-
  --color-fill-diff-green: #183425;
-
  --color-fill-diff-green-light: #142a1d;
-
  --color-fill-float: #25262d;
-
  --color-fill-float-hover: #2e2f38;
-
  --color-fill-merged: #1a001a;
-
  --color-fill-selected: #16173d;
-
  --color-fill-warning: #191500;
-
  --color-fill-counter: #393a46;
-
  --color-fill-counter-emphasized: #5c5c70;
-
  --color-fill-delegate: #3d003d;
-
  --color-fill-private: #4c4000;
-
  --color-fill-secondary-counter: #ccceff;
-
  --color-fill-primary-counter: #ff8fff;
-
  --color-fill-ghost-shade: #25262d;
+
  --color-surface-base: var(--color-neutrals-opaque-dark-0);
+
  --color-surface-canvas: var(--color-neutrals-opaque-dark-50);
+
  --color-surface-subtle: var(--color-neutrals-opaque-dark-100);
+
  --color-surface-mid: var(--color-neutrals-opaque-dark-150);
+
  --color-surface-strong: var(--color-neutrals-opaque-dark-200);
+
  --color-surface-alpha-subtle: var(--color-neutrals-alpha-dark-50);
+
  --color-surface-alpha-mid: var(--color-neutrals-alpha-dark-100);
+
  --color-surface-alpha-strong: var(--color-neutrals-alpha-dark-200);
+
  --color-surface-scrim: var(--color-neutrals-alpha-light-700);
+
  --color-surface-open: var(--color-accent-green-900);
+
  --color-surface-draft: var(--color-neutrals-opaque-dark-150);
+
  --color-surface-closed: var(--color-accent-blue-900);
+
  --color-surface-merged: var(--color-accent-blue-900);
+
  --color-surface-archived: var(--color-accent-pink-900);
+

+
  --color-text-primary: var(--color-neutrals-opaque-dark-900);
+
  --color-text-secondary: var(--color-neutrals-opaque-dark-700);
+
  --color-text-tertiary: var(--color-neutrals-opaque-dark-600);
+
  --color-text-quaternary: var(--color-neutrals-opaque-dark-500);
+
  --color-text-disabled: var(--color-neutrals-alpha-dark-400);
+
  --color-text-open: var(--color-accent-green-500);
+
  --color-text-draft: var(--color-neutrals-opaque-dark-600);
+
  --color-text-closed: var(--color-accent-blue-500);
+
  --color-text-merged: var(--color-accent-blue-500);
+
  --color-text-archived: var(--color-accent-pink-500);
+

+
  --color-border-subtle: var(--color-neutrals-opaque-dark-150);
+
  --color-border-mid: var(--color-neutrals-opaque-dark-200);
+
  --color-border-strong: var(--color-neutrals-opaque-dark-300);
+
  --color-border-alpha-subtle: var(--color-neutrals-alpha-dark-100);
+
  --color-border-alpha-mid: var(--color-neutrals-alpha-dark-200);
+

+
  --color-feedback-success-text: var(--color-semantic-green-400);
+
  --color-feedback-success-border: var(--color-semantic-green-700);
+
  --color-feedback-success-bg: var(--color-semantic-green-900);
+
  --color-feedback-success-bg-selected: var(--color-semantic-green-800);
+
  --color-feedback-warning-text: var(--color-semantic-amber-400);
+
  --color-feedback-warning-border: var(--color-semantic-amber-700);
+
  --color-feedback-warning-bg: var(--color-semantic-amber-900);
+
  --color-feedback-error-text: var(--color-semantic-red-400);
+
  --color-feedback-error-border: var(--color-semantic-red-700);
+
  --color-feedback-error-bg: var(--color-semantic-red-900);
+
  --color-feedback-error-bg-selected: var(--color-semantic-red-800);
+

+
  --color-code-keywords: var(--color-accent-blue-400);
+
  --color-code-strings: var(--color-accent-green-400);
+
  --color-code-numbers: var(--color-accent-purple-400);
+
  --color-code-comments: var(--color-neutrals-opaque-dark-700);
+
  --color-code-error: var(--color-semantic-red-400);
+
  --color-code-functions: var(--color-accent-emerald-400);
+

+
  --color-text-brand: var(--color-brand-text-dark);
+
}
+

+
/* Internal color system, don't use directly! */
+
:root {
+
  --color-neutrals-opaque-light-0: #ffffff;
+
  --color-neutrals-opaque-light-50: #f8f9fa;
+
  --color-neutrals-opaque-light-100: #f1f3f5;
+
  --color-neutrals-opaque-light-150: #e9ebef;
+
  --color-neutrals-opaque-light-200: #e1e4e8;
+
  --color-neutrals-opaque-light-250: #d6d9e0;
+
  --color-neutrals-opaque-light-300: #c9cdd4;
+
  --color-neutrals-opaque-light-400: #a2a7b1;
+
  --color-neutrals-opaque-light-500: #7a8190;
+
  --color-neutrals-opaque-light-600: #5a5f6b;
+
  --color-neutrals-opaque-light-700: #3a3f49;
+
  --color-neutrals-opaque-light-800: #1f232b;
+
  --color-neutrals-opaque-light-900: #0b0d12;
+

+
  --color-neutrals-opaque-dark-0: #00060f;
+
  --color-neutrals-opaque-dark-50: #0a1018;
+
  --color-neutrals-opaque-dark-100: #141a22;
+
  --color-neutrals-opaque-dark-150: #1f242c;
+
  --color-neutrals-opaque-dark-200: #29303a;
+
  --color-neutrals-opaque-dark-250: #33383f;
+
  --color-neutrals-opaque-dark-300: #3d4248;
+
  --color-neutrals-opaque-dark-400: #52565c;
+
  --color-neutrals-opaque-dark-500: #666a6f;
+
  --color-neutrals-opaque-dark-600: #8f9296;
+
  --color-neutrals-opaque-dark-700: #b8babc;
+
  --color-neutrals-opaque-dark-800: #e0e1e2;
+
  --color-neutrals-opaque-dark-900: #ffffff;
+

+
  --color-neutrals-alpha-light-50: #00060f0a;
+
  --color-neutrals-alpha-light-100: #00060f14;
+
  --color-neutrals-alpha-light-200: #00060f1f;
+
  --color-neutrals-alpha-light-300: #00060f29;
+
  --color-neutrals-alpha-light-400: #00060f3d;
+
  --color-neutrals-alpha-light-500: #00060f52;
+
  --color-neutrals-alpha-light-600: #00060f7a;
+
  --color-neutrals-alpha-light-700: #00060fa3;
+
  --color-neutrals-alpha-light-800: #00060fcc;
+
  --color-neutrals-alpha-light-900: #00060feb;
+

+
  --color-neutrals-alpha-dark-50: #ffffff0a;
+
  --color-neutrals-alpha-dark-100: #ffffff14;
+
  --color-neutrals-alpha-dark-200: #ffffff1f;
+
  --color-neutrals-alpha-dark-300: #ffffff29;
+
  --color-neutrals-alpha-dark-400: #ffffff3d;
+
  --color-neutrals-alpha-dark-500: #ffffff52;
+
  --color-neutrals-alpha-dark-600: #ffffff7a;
+
  --color-neutrals-alpha-dark-700: #ffffffa3;
+
  --color-neutrals-alpha-dark-800: #ffffffcc;
+
  --color-neutrals-alpha-dark-900: #ffffffeb;
+

+
  --color-accent-blue-0: #f4f9ff;
+
  --color-accent-blue-100: #d6e9ff;
+
  --color-accent-blue-200: #afcfff;
+
  --color-accent-blue-300: #7fb0ff;
+
  --color-accent-blue-400: #4d94ff;
+
  --color-accent-blue-500: #1c77ff;
+
  --color-accent-blue-600: #165fcc;
+
  --color-accent-blue-700: #104799;
+
  --color-accent-blue-800: #0b3266;
+
  --color-accent-blue-900: #061d33;
+

+
  --color-accent-green-0: #f7fff2;
+
  --color-accent-green-100: #dffcc6;
+
  --color-accent-green-200: #bef98a;
+
  --color-accent-green-300: #99f24c;
+
  --color-accent-green-400: #73e926;
+
  --color-accent-green-500: #58e600;
+
  --color-accent-green-600: #46ba00;
+
  --color-accent-green-700: #358f00;
+
  --color-accent-green-800: #256400;
+
  --color-accent-green-900: #0a1f00;
+

+
  --color-accent-cyan-0: #f1fefe;
+
  --color-accent-cyan-100: #ccf9fa;
+
  --color-accent-cyan-200: #99eff0;
+
  --color-accent-cyan-300: #66e5e7;
+
  --color-accent-cyan-400: #33dbde;
+
  --color-accent-cyan-500: #00d4da;
+
  --color-accent-cyan-600: #00a8ae;
+
  --color-accent-cyan-700: #007d82;
+
  --color-accent-cyan-800: #005256;
+
  --color-accent-cyan-900: #00292b;
+

+
  --color-accent-purple-0: #f7f5ff;
+
  --color-accent-purple-100: #e0daff;
+
  --color-accent-purple-200: #c0b5ff;
+
  --color-accent-purple-300: #a08fff;
+
  --color-accent-purple-400: #8a74fa;
+
  --color-accent-purple-500: #886bf2;
+
  --color-accent-purple-600: #6c54c2;
+
  --color-accent-purple-700: #503f91;
+
  --color-accent-purple-800: #352a61;
+
  --color-accent-purple-900: #1b1530;
+

+
  --color-accent-pink-0: #fff6ff;
+
  --color-accent-pink-100: #ffedff;
+
  --color-accent-pink-200: #ffdbff;
+
  --color-accent-pink-300: #ffc9ff;
+
  --color-accent-pink-400: #ffb7ff;
+
  --color-accent-pink-500: #ffa5ff;
+
  --color-accent-pink-600: #cc84cc;
+
  --color-accent-pink-700: #996399;
+
  --color-accent-pink-800: #664266;
+
  --color-accent-pink-900: #332133;
+

+
  --color-accent-emerald-0: #f2fcf9;
+
  --color-accent-emerald-100: #ccf4e4;
+
  --color-accent-emerald-200: #99e9ca;
+
  --color-accent-emerald-300: #66ddb0;
+
  --color-accent-emerald-400: #33d196;
+
  --color-accent-emerald-500: #009f67;
+
  --color-accent-emerald-600: #007f52;
+
  --color-accent-emerald-700: #005f3e;
+
  --color-accent-emerald-800: #004029;
+
  --color-accent-emerald-900: #002015;
+

+
  --color-accent-citrus-0: #fcfff2;
+
  --color-accent-citrus-100: #f1ffbf;
+
  --color-accent-citrus-200: #e4ff80;
+
  --color-accent-citrus-300: #d6ff40;
+
  --color-accent-citrus-400: #caff20;
+
  --color-accent-citrus-500: #ccff38;
+
  --color-accent-citrus-600: #a4cc2d;
+
  --color-accent-citrus-700: #7c991f;
+
  --color-accent-citrus-800: #536613;
+
  --color-accent-citrus-900: #2b3307;
+

+
  --color-accent-olive-0: #fafaed;
+
  --color-accent-olive-100: #e6e5ba;
+
  --color-accent-olive-200: #d1cf86;
+
  --color-accent-olive-300: #bdb950;
+
  --color-accent-olive-400: #9e9900;
+
  --color-accent-olive-500: #585600;
+
  --color-accent-olive-600: #464400;
+
  --color-accent-olive-700: #343300;
+
  --color-accent-olive-800: #232200;
+
  --color-accent-olive-900: #111100;
+

+
  --color-semantic-red-0: #fff5f5;
+
  --color-semantic-red-100: #ffd6d6;
+
  --color-semantic-red-200: #ffafaf;
+
  --color-semantic-red-300: #ff8686;
+
  --color-semantic-red-400: #ff5c5c;
+
  --color-semantic-red-500: #ff4d4f;
+
  --color-semantic-red-600: #cc3e3f;
+
  --color-semantic-red-700: #992f2f;
+
  --color-semantic-red-800: #661f20;
+
  --color-semantic-red-900: #330f10;
+

+
  --color-semantic-amber-0: #fffdf2;
+
  --color-semantic-amber-100: #fff1b8;
+
  --color-semantic-amber-200: #ffe58f;
+
  --color-semantic-amber-300: #ffd666;
+
  --color-semantic-amber-400: #ffc53d;
+
  --color-semantic-amber-500: #faad14;
+
  --color-semantic-amber-600: #d48806;
+
  --color-semantic-amber-700: #ad6800;
+
  --color-semantic-amber-800: #874d00;
+
  --color-semantic-amber-900: #613400;
+

+
  --color-semantic-green-0: #f6fff1;
+
  --color-semantic-green-100: #d9f7be;
+
  --color-semantic-green-200: #b7eb8f;
+
  --color-semantic-green-300: #95de64;
+
  --color-semantic-green-400: #73d13d;
+
  --color-semantic-green-500: #52c41a;
+
  --color-semantic-green-600: #3d9914;
+
  --color-semantic-green-700: #2e7010;
+
  --color-semantic-green-800: #1e470a;
+
  --color-semantic-green-900: #0f2305;
+

+
  --color-semantic-blue-0: #f0f9ff;
+
  --color-semantic-blue-100: #c6e4ff;
+
  --color-semantic-blue-200: #91caff;
+
  --color-semantic-blue-300: #5bafff;
+
  --color-semantic-blue-400: #2896ff;
+
  --color-semantic-blue-500: #1890ff;
+
  --color-semantic-blue-600: #1373cc;
+
  --color-semantic-blue-700: #0e5799;
+
  --color-semantic-blue-800: #093c66;
+
  --color-semantic-blue-900: #051f33;
}
added public/flower.png
added public/fonts/Booton-Medium.woff2
added public/fonts/Booton-Regular.woff2
added public/fonts/Booton-SemiBold.woff2
deleted public/fonts/Inter-Bold.woff2
deleted public/fonts/Inter-Medium.woff2
deleted public/fonts/Inter-Regular.woff2
deleted public/fonts/Inter-SemiBold.woff2
modified public/index.css
@@ -2,6 +2,14 @@
  box-sizing: border-box;
}

+
:root {
+
  --border-radius-xs: 0.0625rem;
+
  --border-radius-sm: 0.125rem;
+
  --border-radius-md: 0.25rem;
+
  --border-radius-lg: 0.5rem;
+
  --border-radius-full: 99rem;
+
}
+

html {
  height: 100%;
  width: 100%;
@@ -18,8 +26,8 @@ body {
  width: 100%;
  margin: 0;
  padding: 0;
-
  color: var(--color-foreground-contrast);
-
  background-color: var(--color-background-default);
+
  color: var(--color-text-primary);
+
  background-color: var(--color-surface-base);
}

a,
@@ -44,28 +52,8 @@ a {
}

::selection {
-
  background: var(--color-fill-yellow);
-
  color: var(--color-foreground-black);
-
}
-

-
.global-oid {
-
  color: var(--color-foreground-emphasized);
-
  font-size: var(--font-size-small);
-
  font-family: var(--font-family-monospace);
-
  font-weight: var(--font-weight-regular);
-
}
-

-
.global-commit {
-
  color: var(--color-foreground-dim);
-
  font-size: var(--font-size-small);
-
  font-family: var(--font-family-monospace);
-
  font-weight: var(--font-weight-semibold);
-
}
-

-
.global-none {
-
  color: var(--color-foreground-disabled);
-
  font-weight: var(--font-weight-bold);
-
  font-family: var(--font-family-monospace);
+
  background: var(--color-feedback-warning-border);
+
  color: var(--color-text-primary);
}

.global-flex {
@@ -76,7 +64,7 @@ a {

.global-tab {
  padding-left: 0.5rem;
-
  border-left: 1px solid var(--color-fill-ghost-hover);
+
  border-left: 1px solid var(--color-border-mid);
  margin-left: 1rem;
  flex-direction: column;
  justify-content: space-between;
@@ -84,21 +72,35 @@ a {
  column-gap: 0.5rem;
}

-
.global-counter {
+
.global-chip {
  display: flex;
  align-items: center;
  justify-content: center;
-
  background-color: var(--color-fill-counter);
-
  clip-path: var(--1px-corner-fill);
+
  background-color: var(--color-surface-strong);
+
  border-radius: var(--border-radius-sm);
  height: 1.5rem;
  padding: 0 0.5rem;
  min-width: 1.5rem;
-
  font-weight: var(--font-weight-regular);
+
  font: var(--txt-body-s-regular);
+
  flex-shrink: 0;
+
}
+

+
.global-counter-badge {
+
  display: inline-flex;
+
  align-items: center;
+
  justify-content: center;
+
  gap: 0.25rem;
+
  min-width: 1.25rem;
+
  padding: 0.125rem 0.25rem;
+
  border-radius: 1px;
+
  background-color: var(--color-surface-alpha-subtle);
+
  color: var(--color-text-secondary);
+
  font: var(--txt-body-s-regular);
  flex-shrink: 0;
}

.global-link {
-
  color: var(--color-foreground-default);
+
  color: var(--color-text-secondary);
  text-decoration: none;
}
.global-link:hover {
@@ -113,9 +115,9 @@ a {
    --os-handle-border-radius: 4px;
    --os-padding-perpendicular: 4px;
    --os-padding-axis: 2px;
-
    --os-handle-bg: var(--color-border-default);
-
    --os-handle-bg-hover: var(--color-border-default);
-
    --os-handle-bg-active: var(--color-border-default);
+
    --os-handle-bg: var(--color-border-mid);
+
    --os-handle-bg-hover: var(--color-border-mid);
+
    --os-handle-bg-active: var(--color-border-mid);
    --os-track-bg: transparent;
  }
}
@@ -128,170 +130,6 @@ a {
  --elevation-low: 0 0 16px 0 #00000022;
}

-
:root {
-
  --1px-corner-fill: 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)
-
  );
-

-
  --1px-top-corner-fill: polygon(
-
    0 2px,
-
    2px 2px,
-
    2px 0,
-
    calc(100% - 2px) 0,
-
    calc(100% - 2px) 2px,
-
    100% 2px,
-
    100% calc(100% - 2px),
-
    100% 4px,
-
    100% 100%,
-
    0 100%
-
  );
-

-
  --1px-bottom-corner-fill: polygon(
-
    0 0,
-
    100% 0,
-
    100% calc(100% - 2px),
-
    calc(100% - 2px) calc(100% - 2px),
-
    calc(100% - 2px) 100%,
-
    2px 100%,
-
    2px calc(100% - 2px),
-
    0 calc(100% - 2px)
-
  );
-

-
  --2px-corner-fill: polygon(
-
    0 4px,
-
    2px 4px,
-
    2px 2px,
-
    4px 2px,
-
    4px 0,
-
    calc(100% - 4px) 0,
-
    calc(100% - 4px) 2px,
-
    calc(100% - 2px) 2px,
-
    calc(100% - 2px) 4px,
-
    100% 4px,
-
    100% calc(100% - 4px),
-
    calc(100% - 2px) calc(100% - 4px),
-
    calc(100% - 2px) calc(100% - 2px),
-
    calc(100% - 4px) calc(100% - 2px),
-
    calc(100% - 4px) 100%,
-
    4px 100%,
-
    4px calc(100% - 2px),
-
    2px calc(100% - 2px),
-
    2px calc(100% - 4px),
-
    0 calc(100% - 4px)
-
  );
-

-
  --2px-top-corner-fill: polygon(
-
    0 4px,
-
    2px 4px,
-
    2px 2px,
-
    4px 2px,
-
    4px 0,
-
    calc(100% - 4px) 0,
-
    calc(100% - 4px) 2px,
-
    calc(100% - 2px) 2px,
-
    calc(100% - 2px) 4px,
-
    100% 4px,
-
    100% 100%,
-
    0 100%
-
  );
-

-
  --2px-bottom-corner-fill: polygon(
-
    0 0,
-
    100% 0,
-
    100% calc(100% - 4px),
-
    calc(100% - 2px) calc(100% - 4px),
-
    calc(100% - 2px) calc(100% - 2px),
-
    calc(100% - 4px) calc(100% - 2px),
-
    calc(100% - 4px) 100%,
-
    4px 100%,
-
    4px calc(100% - 2px),
-
    2px calc(100% - 2px),
-
    2px calc(100% - 4px),
-
    0 calc(100% - 4px)
-
  );
-

-
  --3px-corner-fill: 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)
-
  );
-

-
  --3px-top-corner-fill: 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% 100%,
-
    0 100%
-
  );
-

-
  --3px-bottom-corner-fill: 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)
-
  );
-
}
-

/*
  Breakpoints
  ===========
modified public/radicle.svg
@@ -1,63 +1,4 @@
-
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
-
  <g shape-rendering="crispEdges">
-
    <rect x="8" y="0" width="4" height="4" fill="#5555FF"/>
-
    <rect x="32" y="0" width="4" height="4" fill="#5555FF"/>
-
    <rect x="12" y="4" width="4" height="4" fill="#5555FF"/>
-
    <rect x="28" y="4" width="4" height="4" fill="#5555FF"/>
-
    <rect x="12" y="8" width="4" height="4" fill="#5555FF"/>
-
    <rect x="16" y="8" width="4" height="4" fill="#3333DD"/>
-
    <rect x="20" y="8" width="4" height="4" fill="#5555FF"/>
-
    <rect x="24" y="8" width="4" height="4" fill="#3333DD"/>
-
    <rect x="28" y="8" width="4" height="4" fill="#5555FF"/>
-
    <rect x="8" y="12" width="4" height="4" fill="#5555FF"/>
-
    <rect x="12" y="12" width="4" height="4" fill="#5555FF"/>
-
    <rect x="16" y="12" width="4" height="4" fill="#5555FF"/>
-
    <rect x="20" y="12" width="4" height="4" fill="#5555FF"/>
-
    <rect x="24" y="12" width="4" height="4" fill="#5555FF"/>
-
    <rect x="28" y="12" width="4" height="4" fill="#5555FF"/>
-
    <rect x="32" y="12" width="4" height="4" fill="#5555FF"/>
-
    <rect x="4" y="16" width="4" height="4" fill="#5555FF"/>
-
    <rect x="8" y="16" width="4" height="4" fill="#5555FF"/>
-
    <rect x="12" y="16" width="4" height="4" fill="#F4F4F4"/>
-
    <rect x="16" y="16" width="4" height="4" fill="#F4F4F4"/>
-
    <rect x="20" y="16" width="4" height="4" fill="#5555FF"/>
-
    <rect x="24" y="16" width="4" height="4" fill="#5555FF"/>
-
    <rect x="28" y="16" width="4" height="4" fill="#F4F4F4"/>
-
    <rect x="32" y="16" width="4" height="4" fill="#F4F4F4"/>
-
    <rect x="36" y="16" width="4" height="4" fill="#5555FF"/>
-
    <rect x="4" y="20" width="4" height="4" fill="#5555FF"/>
-
    <rect x="8" y="20" width="4" height="4" fill="#5555FF"/>
-
    <rect x="12" y="20" width="4" height="4" fill="#F4F4F4"/>
-
    <rect x="16" y="20" width="4" height="4" fill="#FF55FF"/>
-
    <rect x="20" y="20" width="4" height="4" fill="#5555FF"/>
-
    <rect x="24" y="20" width="4" height="4" fill="#5555FF"/>
-
    <rect x="28" y="20" width="4" height="4" fill="#F4F4F4"/>
-
    <rect x="32" y="20" width="4" height="4" fill="#FF55FF"/>
-
    <rect x="36" y="20" width="4" height="4" fill="#5555FF"/>
-
    <rect x="0" y="24" width="4" height="4" fill="#3333DD"/>
-
    <rect x="4" y="24" width="4" height="4" fill="#3333DD"/>
-
    <rect x="8" y="24" width="4" height="4" fill="#3333DD"/>
-
    <rect x="12" y="24" width="4" height="4" fill="#3333DD"/>
-
    <rect x="16" y="24" width="4" height="4" fill="#3333DD"/>
-
    <rect x="20" y="24" width="4" height="4" fill="#3333DD"/>
-
    <rect x="24" y="24" width="4" height="4" fill="#3333DD"/>
-
    <rect x="28" y="24" width="4" height="4" fill="#3333DD"/>
-
    <rect x="32" y="24" width="4" height="4" fill="#3333DD"/>
-
    <rect x="36" y="24" width="4" height="4" fill="#3333DD"/>
-
    <rect x="40" y="24" width="4" height="4" fill="#3333DD"/>
-
    <rect x="8" y="28" width="4" height="4" fill="#3333DD"/>
-
    <rect x="16" y="28" width="4" height="4" fill="#5555FF"/>
-
    <rect x="24" y="28" width="4" height="4" fill="#5555FF"/>
-
    <rect x="32" y="28" width="4" height="4" fill="#3333DD"/>
-
    <rect x="8" y="32" width="4" height="4" fill="#3333DD"/>
-
    <rect x="16" y="32" width="4" height="4" fill="#5555FF"/>
-
    <rect x="24" y="32" width="4" height="4" fill="#5555FF"/>
-
    <rect x="32" y="32" width="4" height="4" fill="#3333DD"/>
-
    <rect x="16" y="36" width="4" height="4" fill="#5555FF"/>
-
    <rect x="24" y="36" width="4" height="4" fill="#5555FF"/>
-
    <rect x="12" y="40" width="4" height="4" fill="#5555FF"/>
-
    <rect x="16" y="40" width="4" height="4" fill="#5555FF"/>
-
    <rect x="24" y="40" width="4" height="4" fill="#5555FF"/>
-
    <rect x="28" y="40" width="4" height="4" fill="#5555FF"/>
-
  </g>
+
<svg width="44" height="22" viewBox="0 0 44 22" fill="none" xmlns="http://www.w3.org/2000/svg">
+
<path d="M44 22H22V0H44V22ZM32.999 3.75C28.9895 3.75011 25.7393 7.00122 25.7393 11.0107C25.7395 15.0201 28.9897 18.2704 32.999 18.2705C37.0085 18.2705 40.2586 15.0202 40.2588 11.0107C40.2588 7.00116 37.0086 3.75 32.999 3.75Z" fill="black"/>
+
<path d="M5.17188 10.9966C5.17188 7.77682 7.78204 5.16666 11.0019 5.16666C14.2217 5.16666 16.8318 7.77682 16.8318 10.9966C16.8318 14.2165 14.2217 16.8266 11.0019 16.8266C7.78204 16.8266 5.17188 14.2165 5.17188 10.9966Z" fill="black"/>
</svg>
modified public/typography.css
@@ -1,30 +1,23 @@
@font-face {
-
  font-family: "Inter";
+
  font-family: "Booton";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
-
  src: url("fonts/Inter-Regular.woff2");
+
  src: url("fonts/Booton-Regular.woff2");
}

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

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

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

@font-face {
@@ -59,94 +52,144 @@

:root {
  --font-size: 16px;
-
  --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-size-large: 1.5rem; /* 24px */
-
  --font-size-x-large: 2rem; /* 32px */
-
  --font-size-xx-large: 3rem; /* 48px */
+

+
  --txt-heading-xxs: 600 0.75rem/1rem Booton, sans-serif;
+
  --txt-heading-xs: 600 0.875rem/1.25rem Booton, sans-serif;
+
  --txt-heading-s: 600 1rem/1.25rem Booton, sans-serif;
+
  --txt-heading-m: 600 1.125rem/1.25rem Booton, sans-serif;
+
  --txt-heading-l: 600 1.375rem/1.75rem Booton, sans-serif;
+
  --txt-heading-xl: 600 1.75rem/2rem Booton, sans-serif;
+
  --txt-heading-xxl: 600 2rem/2.25rem Booton, sans-serif;
+

+
  --txt-body-s-regular: 400 0.75rem/1rem Booton, sans-serif;
+
  --txt-body-s-medium: 468 0.75rem/1rem Booton, sans-serif;
+
  --txt-body-s-semibold: 600 0.75rem/1rem Booton, sans-serif;
+

+
  --txt-body-m-regular: 400 0.875rem/1.25rem Booton, sans-serif;
+
  --txt-body-m-medium: 468 0.875rem/1.25rem Booton, sans-serif;
+
  --txt-body-m-semibold: 600 0.875rem/1.25rem Booton, sans-serif;
+

+
  --txt-body-l-regular: 400 1rem/1.5rem Booton, sans-serif;
+
  --txt-body-l-medium: 468 1rem/1.5rem Booton, sans-serif;
+
  --txt-body-l-semibold: 600 1rem/1.5rem Booton, sans-serif;
}

[data-codefont="system"] {
-
  --font-family-monospace: monospace;
+
  --txt-code-regular: 400 0.875rem/1.25rem monospace;
+
  --txt-code-semibold: 600 0.875rem/1.25rem monospace;
}

[data-codefont="jetbrains"] {
-
  --font-family-monospace: "JetBrains Mono";
+
  --txt-code-regular: 400 0.875rem/1.25rem "JetBrains Mono";
+
  --txt-code-semibold: 600 0.875rem/1.25rem "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";
+
  font-family: Booton, sans-serif;
  /* The root element font size has to be set in px,
   * otherwise Safari breaks. */
  font-size: var(--font-size);
-
  font-weight: var(--font-weight-regular);
+
  font-weight: 400;
+
  line-height: 1.25;
+
}
+

+
.txt-heading-xxs {
+
  font: var(--txt-heading-xxs);
+
}
+

+
.txt-heading-xs {
+
  font: var(--txt-heading-xs);
+
}
+

+
.txt-heading-s {
+
  font: var(--txt-heading-s);
+
}
+

+
.txt-heading-m {
+
  font: var(--txt-heading-m);
}

-
body {
-
  /* On Safari this aligns text with different font-faces properly vertically. */
-
  line-height: 1.375rem;
+
.txt-heading-l {
+
  font: var(--txt-heading-l);
}

-
p {
-
  margin: 1rem 0;
+
.txt-heading-xl {
+
  font: var(--txt-heading-xl);
}
-
.txt-tiny {
-
  font-size: var(--font-size-tiny);
+

+
.txt-heading-xxl {
+
  font: var(--txt-heading-xxl);
}
-
.txt-small {
-
  font-size: var(--font-size-small);
+

+
.txt-body-s-regular {
+
  font: var(--txt-body-s-regular);
}
-
.txt-regular {
-
  font-size: var(--font-size-regular);
+

+
.txt-body-s-medium {
+
  font: var(--txt-body-s-medium);
}
-
.txt-medium {
-
  font-size: var(--font-size-medium);
+

+
.txt-body-s-semibold {
+
  font: var(--txt-body-s-semibold);
}
-
.txt-large {
-
  font-size: var(--font-size-large);
+

+
.txt-body-m-regular {
+
  font: var(--txt-body-m-regular);
}
-
.txt-huge {
-
  font-size: var(--font-size-x-large);
+

+
.txt-body-m-medium {
+
  font: var(--txt-body-m-medium);
}
-
.txt-humongous {
-
  font-size: var(--font-size-xx-large);
+

+
.txt-body-m-semibold {
+
  font: var(--txt-body-m-semibold);
}

-
.txt-monospace {
-
  font-family: var(--font-family-monospace);
+
.txt-body-l-regular {
+
  font: var(--txt-body-l-regular);
}
-
.txt-bold {
-
  font-weight: var(--font-weight-bold) !important;
+

+
.txt-body-l-medium {
+
  font: var(--txt-body-l-medium);
}
-
.txt-semibold {
-
  font-weight: var(--font-weight-semibold) !important;
+

+
.txt-body-l-semibold {
+
  font: var(--txt-body-l-semibold);
}
+

+
.txt-code-regular {
+
  font: var(--txt-code-regular);
+
}
+

+
.txt-code-semibold {
+
  font: var(--txt-code-semibold);
+
}
+

+
.txt-id {
+
  color: var(--color-text-tertiary);
+
  font: var(--txt-code-regular);
+
}
+

.txt-missing {
-
  color: var(--color-foreground-dim);
+
  color: var(--color-text-secondary);
}
+

.txt-selectable {
  -webkit-touch-callout: initial;
  -webkit-user-select: text;
  user-select: text;
}
+

.txt-emoji {
  height: 1em;
  width: 1em;
  margin: 0 0.05em 0 0.1em;
  vertical-align: -0.1em;
}
+

.txt-overflow {
  overflow: hidden;
  text-overflow: ellipsis;
modified src/App.svelte
@@ -3,8 +3,10 @@
  import type { ErrorWrapper } from "@bindings/error/ErrorWrapper";

  import { listen } from "@tauri-apps/api/event";
+
  import { getCurrentWindow } from "@tauri-apps/api/window";
  import delay from "lodash/delay";
  import { onDestroy, onMount } from "svelte";
+
  import { get } from "svelte/store";

  import {
    decreaseFontSize,
@@ -16,6 +18,7 @@
  import { nodeRunning } from "@app/lib/events";
  import { dynamicInterval } from "@app/lib/interval";
  import { invoke } from "@app/lib/invoke";
+
  import { hide } from "@app/lib/modal";
  import * as router from "@app/lib/router";
  import {
    setUnlistenNodeEvents,
@@ -23,11 +26,16 @@
  } from "@app/lib/startup.svelte";
  import { isMac, unreachable } from "@app/lib/utils";

-
  import { theme } from "@app/components/ThemeSwitch.svelte";
-
  import Auth from "@app/views/booting/Auth.svelte";
-
  import CreateIdentity from "@app/views/booting/CreateIdentity.svelte";
-
  import Repos from "@app/views/home/Repos.svelte";
-
  import CreateIssue from "@app/views/repo/CreateIssue.svelte";
+
  import { codeFont } from "@app/components/CodeFontSwitch.svelte";
+
  import {
+
    followSystemTheme,
+
    loadTheme,
+
    theme,
+
  } from "@app/components/ThemeSwitch.svelte";
+
  import GuideView from "@app/modals/Guide.svelte";
+
  import Auth from "@app/views/auth/Auth.svelte";
+
  import CreateIdentity from "@app/views/auth/CreateIdentity.svelte";
+
  import InboxView from "@app/views/Inbox.svelte";
  import Issue from "@app/views/repo/Issue.svelte";
  import Issues from "@app/views/repo/Issues.svelte";
  import Patch from "@app/views/repo/Patch.svelte";
@@ -36,11 +44,45 @@

  import Command from "./components/Command.svelte";
  import ExternalLink from "./components/ExternalLink.svelte";
+
  import FullscreenModalPortal from "./components/FullscreenModalPortal.svelte";
  import FullWindowError from "./components/FullWindowError.svelte";
  import Spinner from "./components/Spinner.svelte";

  const activeRouteStore = router.activeRouteStore;

+
  const DRAG_REGION_HEIGHT = 32;
+
  const INTERACTIVE_TAGS = new Set([
+
    "a",
+
    "button",
+
    "input",
+
    "select",
+
    "textarea",
+
  ]);
+

+
  function isDraggableArea(e: MouseEvent): boolean {
+
    if (e.clientY > DRAG_REGION_HEIGHT) return false;
+
    let el = e.target as HTMLElement | null;
+
    while (el && el !== document.body) {
+
      if (INTERACTIVE_TAGS.has(el.tagName.toLowerCase())) return false;
+
      if (el.getAttribute("role") === "button") return false;
+
      if (el.classList.contains("txt-selectable")) return false;
+
      el = el.parentElement;
+
    }
+
    return true;
+
  }
+

+
  window
+
    .matchMedia("(prefers-color-scheme: dark)")
+
    .addEventListener("change", ({ matches }) => {
+
      if (get(followSystemTheme)) {
+
        theme.set(matches ? "dark" : "light");
+
      }
+
    });
+

+
  if (get(followSystemTheme)) {
+
    theme.set(loadTheme());
+
  }
+

  let profile = $state<Config>();

  let showSpinner = $state(false);
@@ -90,6 +132,9 @@
    ),
  );
  $effect(() => document.documentElement.setAttribute("data-theme", $theme));
+
  $effect(() =>
+
    document.documentElement.setAttribute("data-codefont", $codeFont),
+
  );
</script>

<style>
@@ -102,11 +147,18 @@
</style>

<svelte:document
+
  onmousedown={e => {
+
    if (window.__TAURI_INTERNALS__ && isDraggableArea(e)) {
+
      void getCurrentWindow().startDragging();
+
    }
+
  }}
  onkeydown={e => {
    const auxiliarKey = isMac() ? e.metaKey : e.ctrlKey;
    // Handles the position of the plus key on different keyboard layouts.
    const plusKey = e.key === "1" || e.key === "=";
-
    if (auxiliarKey && (e.key === "+" || plusKey)) {
+
    if (e.key === "Escape") {
+
      hide();
+
    } else if (auxiliarKey && (e.key === "+" || plusKey)) {
      increaseFontSize();
    } else if (auxiliarKey && e.key === "-") {
      decreaseFontSize();
@@ -114,6 +166,7 @@
      resetFontSize();
    }
  }} />
+
<FullscreenModalPortal />

{#if $activeRouteStore.resource === "booting"}
  {#if startup.error?.code === "IdentityError.MissingProfile"}
@@ -141,12 +194,12 @@
  {:else if showSpinner}
    <div class="spinner"><Spinner /></div>
  {/if}
-
{:else if $activeRouteStore.resource === "home"}
-
  <Repos {...$activeRouteStore.params} />
+
{:else if $activeRouteStore.resource === "inbox"}
+
  <InboxView {...$activeRouteStore.params} />
+
{:else if $activeRouteStore.resource === "guide"}
+
  <GuideView {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "repo.home"}
  <RepoHome {...$activeRouteStore.params} />
-
{:else if $activeRouteStore.resource === "repo.createIssue"}
-
  <CreateIssue {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "repo.issue"}
  <Issue {...$activeRouteStore.params} />
{:else if $activeRouteStore.resource === "repo.issues"}
modified src/components/AddRepoButton.svelte
@@ -3,7 +3,7 @@
</script>

<script lang="ts">
-
  import type { RepoInfo } from "@bindings/repo/RepoInfo";
+
  import type { RepoSummary } from "@bindings/repo/RepoSummary";

  import { z } from "zod";

@@ -13,20 +13,18 @@
  import { parseRepositoryId, twemoji } from "@app/lib/utils";

  import { announce } from "@app/components/AnnounceSwitch.svelte";
-
  import Border from "@app/components/Border.svelte";
  import Button from "@app/components/Button.svelte";
  import Command from "@app/components/Command.svelte";
  import ExternalLink from "@app/components/ExternalLink.svelte";
  import Icon from "@app/components/Icon.svelte";
  import { closeFocused } from "@app/components/Popover.svelte";
  import Popover from "@app/components/Popover.svelte";
-
  import Tab from "@app/components/Tab.svelte";
  import TextInput from "@app/components/TextInput.svelte";

  interface Props {
    onOpen: () => void;
    reload: () => Promise<void>;
-
    repos: RepoInfo[];
+
    repos: RepoSummary[];
    seededNotReplicated: string[];
  }

@@ -107,188 +105,160 @@
  }
</style>

-
<Popover
-
  popoverPositionRight="0"
-
  popoverPositionTop="3rem"
-
  bind:expanded={popoverExpanded}>
+
<Popover placement="bottom-start" bind:expanded={popoverExpanded}>
  {#snippet toggle(onclick)}
    <Button
+
      variant="naked"
      id={addRepoPopoverToggleId}
-
      styleHeight="2.5rem"
-
      variant="secondary"
      onclick={() => {
        onOpen();
        onclick();
      }}
      active={popoverExpanded}>
-
      <Icon name="add" />Add repo
+
      <Icon name="plus" />
    </Button>
  {/snippet}

  {#snippet popover()}
-
    <Border
-
      stylePosition="relative"
-
      variant="ghost"
-
      flatBottom
-
      styleDisplay="flex"
-
      styleWidth="100%"
-
      styleGap="1rem"
-
      styleMinWidth="27rem"
-
      stylePadding="0 1rem">
-
      <Tab
-
        active={tab.value === "seed"}
-
        onclick={() => {
-
          tab.value = "seed";
-
        }}>
-
        Seed a repo
-
      </Tab>
-
      <Tab
-
        active={tab.value === "publish"}
-
        onclick={() => {
-
          tab.value = "publish";
-
        }}>
-
        Publish existing repo
-
      </Tab>
-
    </Border>
+
    <div
+
      class="txt-body-m-regular"
+
      style:line-height="1.625rem"
+
      style:padding="1rem"
+
      style:border-radius="var(--border-radius-md)"
+
      style:border="1px solid var(--color-border-subtle)"
+
      style:background-color="var(--color-surface-canvas)"
+
      style:width="32rem">
+
      <div class="global-flex" style:margin-bottom="1rem">
+
        <Button
+
          variant="naked"
+
          active={tab.value === "seed"}
+
          onclick={() => {
+
            tab.value = "seed";
+
          }}>
+
          Seed a repo
+
        </Button>
+
        <Button
+
          variant="naked"
+
          active={tab.value === "publish"}
+
          onclick={() => {
+
            tab.value = "publish";
+
          }}>
+
          Publish existing
+
        </Button>
+
      </div>

-
    <div style:margin-top="-2px">
-
      <Border
-
        variant="ghost"
-
        flatTop
-
        stylePadding="1rem"
-
        styleDisplay="block"
-
        styleFlexDirection="column"
-
        styleAlignItems="flex-start">
-
        <div class="txt-small" style:line-height="1.625rem">
-
          {#if tab.value === "seed"}
-
            <!-- prettier-ignore -->
-
            <div style:margin-bottom="1rem">
+
      {#if tab.value === "seed"}
+
        <!-- prettier-ignore -->
+
        <div style:margin-bottom="1rem" style:color="var(--color-text-primary)">
              You can search for Radicle repos by name or description at
              <ExternalLink href="https://search.radicle.xyz">
                search.radicle.xyz
              </ExternalLink>.
            </div>
+
        <div style:width="100%">
+
          <div class="txt-body-l-semibold" style:margin-bottom="0.5rem"></div>
+
          <div
+
            class="global-flex"
+
            style:flex-direction="column"
+
            style:align-items="flex-start"
+
            style:gap="1rem">
            <div style:width="100%">
-
              <div class="txt-semibold" style:margin-bottom="0.5rem"></div>
-
              <div
-
                class="global-flex"
-
                style:flex-direction="column"
-
                style:align-items="flex-start"
-
                style:gap="1rem">
-
                <div style:width="100%">
-
                  <div class="global-flex" style:width="100%">
-
                    <TextInput
-
                      autofocus
-
                      valid={validationMessage === undefined}
-
                      bind:value={rid}
-
                      onSubmit={submit}
-
                      placeholder="RID, e.g. rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5" />
-
                    <Button
-
                      variant="ghost"
-
                      styleHeight="2.5rem"
-
                      onclick={submit}
-
                      disabled={rid.trim() === ""}>
-
                      Seed
-
                    </Button>
-
                  </div>
-
                  {#if validationMessage}
-
                    <div
-
                      class="txt-small global-flex"
-
                      style:color="var(--color-foreground-red)"
-
                      style:padding="0.25rem 0 0 0.25rem"
-
                      style:gap="0.25rem">
-
                      <Icon name="warning" />
-
                      {validationMessage}
-
                    </div>
-
                  {/if}
-
                </div>
+
              <div class="global-flex" style:width="100%">
+
                <TextInput
+
                  autofocus
+
                  valid={validationMessage === undefined}
+
                  bind:value={rid}
+
                  onSubmit={submit}
+
                  placeholder="RID, e.g. rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5" />
+
                <Button
+
                  variant="secondary"
+
                  onclick={submit}
+
                  disabled={rid.trim() === ""}>
+
                  <Icon name="seed" />
+
                  Seed
+
                </Button>
              </div>
+
              {#if validationMessage}
+
                <div
+
                  class="txt-body-m-regular global-flex"
+
                  style:color="var(--color-feedback-error-text)"
+
                  style:padding="0.25rem 0 0 0.25rem"
+
                  style:gap="0.25rem">
+
                  <Icon name="warning" />
+
                  {validationMessage}
+
                </div>
+
              {/if}
            </div>
-
            <div
-
              class="global-flex txt-missing"
-
              style:align-items="flex-start"
-
              style:margin-top="2rem">
-
              <span style:margin-top="0.25rem">
-
                <Icon name="info" />
-
              </span>
-
              By seeding a repository, your node fetches it from the network, allowing
-
              you to interact with it locally while also making it available to others.
+
          </div>
+
        </div>
+
        <div
+
          class="global-flex txt-missing"
+
          style:align-items="flex-start"
+
          style:margin-top="2rem">
+
          By seeding a repository, your node fetches it from the network,
+
          allowing you to interact with it locally while also making it
+
          available to others.
+
        </div>
+
        {#if !$nodeRunning}
+
          <div
+
            class="global-flex txt-missing"
+
            style:align-items="flex-start"
+
            style:margin-top="1rem">
+
            <div>
+
              Your node is Offline. You can still add repos, but they will only
+
              be fetched once your node is back online.
            </div>
-
            {#if !$nodeRunning}
-
              <div
-
                class="global-flex txt-missing"
-
                style:align-items="flex-start"
-
                style:margin-top="1rem">
-
                <span style:margin-top="0.25rem">
-
                  <Icon name="bulb" />
-
                </span>
+
          </div>
+
        {/if}
+
      {:else if tab.value === "publish"}
+
        <p style="margin: 0 0 1rem 0" style:color="var(--color-text-primary)">
+
          Navigate to an existing Git repo in your terminal
+
          <code
+
            style:white-space="nowrap"
+
            style:padding="0.125rem 0.25rem"
+
            style:background-color="var(--color-surface-subtle)">
+
            cd path/to/your/repo
+
          </code>
+
          and run the following command:
+
        </p>

-
                <div>
-
                  Your node is
-
                  <span class="txt-semibold">
-
                    <span
-
                      style:display="inline-block"
-
                      style:vertical-align="text-top">
-
                      <Icon name="offline" />
-
                    </span>
-
                    Offline.
-
                  </span>
-
                  You can still add repos, but they will only be fetched once your
-
                  node is back online.
-
                </div>
-
              </div>
-
            {/if}
-
          {:else if tab.value === "publish"}
-
            <p style="margin: 0 0 1rem 0">
-
              Navigate to an existing Git repo in your terminal
-
              <code
-
                style:white-space="nowrap"
-
                style:padding="0.125rem 0.25rem"
-
                style:background-color="var(--color-fill-ghost)">
-
                cd path/to/your/repo
-
              </code>
-
              and run the following command:
-
            </p>
+
        <Command styleWidth="fit-content" command="rad init" />

-
            <Command styleWidth="fit-content" command="rad init" />
+
        <p style="margin: 1rem 0 0 0" style:color="var(--color-text-primary)">
+
          Follow the setup prompts to initialize the repo and publish it on the
+
          Radicle network:
+
        </p>

-
            <p style="margin: 1rem 0 0 0">
-
              Follow the setup prompts to initialize the repo and publish it on
-
              the Radicle network:
-
            </p>
-

-
            <ul style:padding="0 1rem">
-
              <li>
-
                <strong>Repository Name:</strong>
-
                The name of your repo.
-
              </li>
-
              <li>
-
                <strong>Description:</strong>
-
                A brief summary of what your repo does.
-
              </li>
-
              <!-- prettier-ignore -->
-
              <li>
+
        <ul style:padding="0 1rem">
+
          <li>
+
            <strong>Repository Name:</strong>
+
            The name of your repo.
+
          </li>
+
          <li>
+
            <strong>Description:</strong>
+
            A brief summary of what your repo does.
+
          </li>
+
          <!-- prettier-ignore -->
+
          <li>
                <strong>Default Branch:</strong>
                Typically
                <strong>main</strong>
                or
                <strong>master</strong>.
              </li>
-
              <li>
-
                <strong>Visibility:</strong>
-
                Choose
-
                <strong>public</strong>
-
                to share with others or
-
                <strong>private</strong>
-
                to not publish it to the network yet.
-
              </li>
-
            </ul>
-
            <p use:twemoji style:margin="2rem 0 0 0">
-
              That's it! Your repo is now on the Radicle network. 🚀
-
            </p>
-
          {/if}
-
        </div>
-
      </Border>
+
          <li>
+
            <strong>Visibility:</strong>
+
            Choose
+
            <strong>public</strong>
+
            to share with others or
+
            <strong>private</strong>
+
            to not publish it to the network yet.
+
          </li>
+
        </ul>
+
        <p use:twemoji style:margin="2rem 0 0 0">
+
          That's it! Your repo is now on the Radicle network.
+
        </p>
+
      {/if}
    </div>
  {/snippet}
</Popover>
modified src/components/AnnounceSwitch.svelte
@@ -40,8 +40,8 @@

<div class="container">
  <Button
-
    flatRight
    variant="ghost"
+
    flatRight
    active={$announce}
    onclick={() => {
      storeAnnounce(true);
@@ -50,8 +50,8 @@
  </Button>

  <Button
-
    flatLeft
    variant="ghost"
+
    flatLeft
    active={!$announce}
    onclick={() => {
      storeAnnounce(false);
added src/components/AppSidebar.svelte
@@ -0,0 +1,222 @@
+
<script lang="ts">
+
  import type { RepoInfo } from "@bindings/repo/RepoInfo";
+

+
  import { getCurrentWindow } from "@tauri-apps/api/window";
+
  import { onMount } from "svelte";
+
  import { boolean } from "zod";
+

+
  import { checkRadicleCLI } from "@app/lib/checkRadicleCLI.svelte";
+
  import { dynamicInterval } from "@app/lib/interval";
+
  import { invoke } from "@app/lib/invoke";
+
  import { modalStore, show } from "@app/lib/modal";
+
  import { notificationCount } from "@app/lib/notificationCount.svelte";
+
  import * as router from "@app/lib/router";
+
  import type { SidebarData } from "@app/lib/router/definitions";
+
  import { updateChecker } from "@app/lib/updateChecker.svelte";
+
  import useLocalStorage from "@app/lib/useLocalStorage.svelte";
+
  import { isMac } from "@app/lib/utils";
+

+
  import { badgeCounter } from "@app/components/BadgeCounterSwitch.svelte";
+
  import Button from "@app/components/Button.svelte";
+
  import Icon from "@app/components/Icon.svelte";
+
  import IdentityButton from "@app/components/IdentityButton.svelte";
+
  import NodeStatusButton from "@app/components/NodeStatusButton.svelte";
+
  import SidebarRepoList from "@app/components/SidebarRepoList.svelte";
+
  import SettingsView from "@app/modals/Settings.svelte";
+

+
  interface Props {
+
    sidebarData: SidebarData;
+
    activeRepo?: RepoInfo;
+
  }
+

+
  const { sidebarData, activeRepo = undefined }: Props = $props();
+

+
  const firstLaunchStorage = useLocalStorage(
+
    "appFirstLaunch",
+
    boolean(),
+
    true,
+
    !window.localStorage,
+
  );
+

+
  onMount(async () => {
+
    try {
+
      await checkRadicleCLI();
+
    } catch {
+
      dynamicInterval("checkRadicleCLI", checkRadicleCLI, 1_000);
+
    }
+

+
    const isDefaultRoute =
+
      window.location.pathname === "/" || window.location.pathname === "/inbox";
+
    if (firstLaunchStorage.value === true && isDefaultRoute) {
+
      await router.push({ resource: "guide" });
+
      firstLaunchStorage.value = false;
+
    }
+

+
    await updateNotificationCount();
+
    dynamicInterval("notificationCount", updateNotificationCount, 3_000);
+
  });
+

+
  async function updateNotificationCount() {
+
    notificationCount.value = await invoke<number>("notification_count");
+
    if (window.__TAURI_INTERNALS__ && $badgeCounter) {
+
      await getCurrentWindow().setBadgeCount(
+
        notificationCount.value === 0 ? undefined : notificationCount.value,
+
      );
+
    } else if (window.__TAURI_INTERNALS__) {
+
      await getCurrentWindow().setBadgeCount(undefined);
+
    }
+
  }
+

+
  $effect(() => {
+
    if (window.__TAURI_INTERNALS__) {
+
      void getCurrentWindow().setBadgeCount(
+
        $badgeCounter && notificationCount.value > 0
+
          ? notificationCount.value
+
          : undefined,
+
      );
+
    }
+
  });
+

+
  const activeRoute = router.activeRouteStore;
+

+
  function isInbox(): boolean {
+
    return $activeRoute.resource === "inbox";
+
  }
+

+
  function isGuide(): boolean {
+
    return $activeRoute.resource === "guide";
+
  }
+

+
  function isSettings(): boolean {
+
    return $modalStore?.component === SettingsView;
+
  }
+
</script>
+

+
<style>
+
  .sidebar {
+
    display: flex;
+
    flex-direction: column;
+
    height: 100%;
+
    min-height: 0;
+
    width: 16.5rem;
+
    border-right: 1px solid var(--color-border-subtle);
+
  }
+
  .top {
+
    padding: 0 0.5rem;
+
    height: 1.75rem;
+
    display: flex;
+
    align-items: center;
+
    justify-content: flex-end;
+
    gap: 0.25rem;
+
    flex-shrink: 0;
+
  }
+
  .nav {
+
    flex: 1;
+
    overflow: visible;
+
    padding: 0.5rem;
+
    display: flex;
+
    flex-direction: column;
+
    gap: 0.25rem;
+
    min-height: 0;
+
  }
+
  .bottom {
+
    padding: 0.5rem;
+
    display: flex;
+
    flex-direction: column;
+
    gap: 0.25rem;
+
  }
+
  .nav-item {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.5rem;
+
    padding: 0.375rem 0.5rem;
+
    border-radius: var(--border-radius-sm);
+
    font: var(--txt-body-m-regular);
+
    color: var(--color-text-primary);
+
    cursor: pointer;
+
    width: 100%;
+
    text-decoration: none;
+
  }
+
  .nav-item:hover {
+
    background-color: var(--color-surface-subtle);
+
  }
+
  .nav-item.active {
+
    background-color: var(--color-surface-subtle);
+
  }
+
  .nav-item .global-counter-badge {
+
    margin-left: auto;
+
  }
+
  .icon {
+
    color: var(--color-text-tertiary);
+
  }
+
  .update-badge {
+
    margin-left: auto;
+
    font: var(--txt-body-s-regular);
+
    color: var(--color-text-tertiary);
+
  }
+
</style>
+

+
<div class="sidebar">
+
  <div
+
    class="top"
+
    style:height={isMac() ? "2.75rem" : "1.75rem"}
+
    data-tauri-drag-region>
+
    <Button
+
      variant="naked"
+
      onclick={() => window.history.back()}
+
      stylePadding="0 4px">
+
      <span class="icon"><Icon name="arrow-left" /></span>
+
    </Button>
+
    <Button
+
      variant="naked"
+
      onclick={() => window.history.forward()}
+
      stylePadding="0 4px">
+
      <span class="icon"><Icon name="arrow-right" /></span>
+
    </Button>
+
  </div>
+

+
  <div class="nav">
+
    <IdentityButton config={sidebarData.config} />
+

+
    <a
+
      class="nav-item"
+
      class:active={isInbox()}
+
      href={router.routeToPath({ resource: "inbox" })}>
+
      <span class="icon"><Icon name="inbox" /></span>
+
      Inbox
+
      {#if notificationCount.value > 0}
+
        <span class="global-counter-badge">{notificationCount.value}</span>
+
      {/if}
+
    </a>
+

+
    <SidebarRepoList
+
      initialRepos={sidebarData.repos}
+
      initialSeededNotReplicated={sidebarData.seededNotReplicated}
+
      {activeRepo} />
+
  </div>
+

+
  <div class="bottom">
+
    <Button
+
      variant="naked"
+
      styleWidth="100%"
+
      styleJustifyContent="flex-start"
+
      active={isGuide()}
+
      onclick={() => router.push({ resource: "guide" })}>
+
      <span class="icon"><Icon name="guide" /></span>
+
      Guide
+
    </Button>
+
    <Button
+
      variant="naked"
+
      styleWidth="100%"
+
      styleJustifyContent="flex-start"
+
      active={isSettings()}
+
      onclick={() => show({ component: SettingsView, props: {} })}>
+
      <span class="icon"><Icon name="settings" /></span>
+
      Settings
+
      {#if updateChecker.newVersion}
+
        <span class="update-badge">New Update</span>
+
      {/if}
+
    </Button>
+
    <NodeStatusButton />
+
  </div>
+
</div>
modified src/components/AssigneeInput.svelte
@@ -8,6 +8,7 @@
    publicKeyFromDid,
  } from "@app/lib/utils";

+
  import Button from "@app/components/Button.svelte";
  import Icon from "@app/components/Icon.svelte";
  import NodeId from "@app/components/NodeId.svelte";
  import TextInput from "@app/components/TextInput.svelte";
@@ -17,6 +18,7 @@
    assignees: Author[];
    submitInProgress: boolean;
    save: (updatedAssignees: Author[]) => void;
+
    preview?: boolean;
  }

  const {
@@ -24,6 +26,7 @@
    assignees = $bindable(),
    submitInProgress = false,
    save,
+
    preview = false,
  }: Props = $props();

  let updatedAssignees: Author[] = $state([]);
@@ -94,33 +97,25 @@
</script>

<style>
-
  .add-icon {
-
    display: none;
-
  }
-
  .title-button:hover .add-icon {
-
    display: flex;
-
  }
-
  .title-button {
-
    font-size: var(--font-size-small);
-
    color: var(--color-foreground-dim);
-
  }
-
  .body {
+
  .row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
-
    flex-direction: row;
-
    gap: 1rem;
-
    font-size: var(--font-size-small);
-
    margin-top: 1rem;
+
    gap: 0.5rem;
  }
  .validation-message {
    display: flex;
    align-items: center;
    gap: 0.25rem;
-
    color: var(--color-foreground-red);
+
    color: var(--color-feedback-error-text);
    position: relative;
    margin-top: 0.5rem;
  }
+
  .input-row {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.5rem;
+
  }
  button {
    border: 0;
    cursor: pointer;
@@ -128,86 +123,79 @@
    background-color: transparent;
    border: none;
    display: flex;
-
    color: var(--color-foreground-default);
+
    color: var(--color-text-secondary);
    padding: 0;
    align-items: center;
  }
</style>

-
<div class="global-flex">
-
  <button
-
    disabled={!allowedToEdit}
-
    style:color={allowedToEdit
-
      ? "var(--color-foreground-dim)"
-
      : "var(--color-foreground-disabled)"}
-
    title={allowedToEdit
-
      ? undefined
-
      : "Only delegates are allowed to add assignees"}
-
    style:cursor={allowedToEdit ? "pointer" : "default"}
-
    class="title-button"
-
    onclick={() => {
-
      inputValue = "";
-
      showInput = !showInput;
-
    }}>
-
    {#if updatedAssignees.length === 0}
-
      Add assignees
-
    {:else}
-
      Assignees
-
    {/if}
-

-
    {#if !showInput && allowedToEdit}
-
      <span class="add-icon">
-
        <Icon name="add" />
+
{#if preview}
+
  <div class="row">
+
    <Button variant="outline" disabled>
+
      <Icon name="avatar-incognito" />
+
      {#if updatedAssignees.length === 0}
+
        Add assignees
+
      {:else}
+
        Assignees
+
      {/if}
+
    </Button>
+
    {#each updatedAssignees as assignee}
+
      <span style:color="var(--color-text-secondary)">
+
        <NodeId {...authorForNodeId(assignee)} />
      </span>
-
    {/if}
-
  </button>
-

-
  {#if allowedToEdit}
-
    <div class="global-flex edit-icons">
-
      {#if showInput}
-
        <Icon
-
          onclick={addAssignee}
-
          name="checkmark"
-
          disabled={!valid || inputValue === ""} />
-
        <Icon
+
    {/each}
+
  </div>
+
{:else}
+
  <div class="row">
+
    {#if showInput}
+
      <div class="input-row">
+
        <div style:flex="1" style:min-width="0">
+
          <TextInput
+
            autofocus
+
            {valid}
+
            disabled={submitInProgress}
+
            placeholder="Assignee DID, e.g. did:key:z6MkwPUeUS2…"
+
            bind:value={inputValue}
+
            onSubmit={addAssignee} />
+
        </div>
+
        <Button
+
          variant="outline"
          onclick={() => {
-
            inputValue = "";
            showInput = false;
-
          }}
-
          name="cross" />
-
      {/if}
-
    </div>
-
  {/if}
-
</div>
-

-
{#if showInput}
-
  <div style:margin-top="1rem">
-
    <TextInput
-
      autofocus
-
      {valid}
-
      disabled={submitInProgress}
-
      placeholder="Assignee DID, e.g. did:key:z6MkwPUeUS2…"
-
      bind:value={inputValue}
-
      onSubmit={addAssignee} />
-
    {#if !valid && validationMessage}
-
      <div class="validation-message">
-
        <Icon name="warning" />{validationMessage}
+
            inputValue = "";
+
          }}>
+
          <Icon name="close" />
+
        </Button>
      </div>
+
    {:else}
+
      <Button
+
        variant="outline"
+
        disabled={!allowedToEdit}
+
        title={allowedToEdit
+
          ? undefined
+
          : "Only delegates are allowed to add assignees"}
+
        onclick={() => {
+
          inputValue = "";
+
          showInput = true;
+
        }}>
+
        <Icon name="avatar-incognito" />
+
        {#if updatedAssignees.length === 0}
+
          Add assignees
+
        {:else}
+
          Assignees
+
        {/if}
+
      </Button>
    {/if}
-
  </div>
-
{/if}

-
{#if updatedAssignees.length > 0}
-
  <div class="body">
    {#if allowedToEdit}
      {#each updatedAssignees as assignee}
        <button
-
          class="txt-small"
+
          class="txt-body-m-regular"
          onclick={() =>
            (removeToggles[assignee.did] = !removeToggles[assignee.did])}>
          <NodeId {...authorForNodeId(assignee)} />
          {#if removeToggles[assignee.did]}
-
            <Icon name="cross" onclick={() => removeAssignee(assignee)} />
+
            <Icon name="close" onclick={() => removeAssignee(assignee)} />
          {/if}
        </button>
      {/each}
@@ -217,4 +205,10 @@
      {/each}
    {/if}
  </div>
+

+
  {#if !valid && validationMessage}
+
    <div class="validation-message">
+
      <Icon name="warning" />{validationMessage}
+
    </div>
+
  {/if}
{/if}
deleted src/components/Avatar.svelte
@@ -1,34 +0,0 @@
-
<script lang="ts">
-
  import { createIcon } from "@app/lib/blockies";
-

-
  const { publicKey }: { publicKey: string } = $props();
-

-
  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: var(--1px-corner-fill);
-
  }
-
</style>
-

-
<img
-
  title={publicKey}
-
  src={createContainer(publicKey)}
-
  class="avatar"
-
  alt="avatar" />
added src/components/BadgeCounterSwitch.svelte
@@ -0,0 +1,45 @@
+
<script lang="ts" module>
+
  import { writable } from "svelte/store";
+

+
  function loadBadgeCounter(): boolean {
+
    const stored = localStorage ? localStorage.getItem("badgeCounter") : null;
+
    return stored === null ? true : stored === "true";
+
  }
+

+
  export const badgeCounter = writable<boolean>(loadBadgeCounter());
+

+
  export function storeBadgeCounter(enabled: boolean): void {
+
    badgeCounter.set(enabled);
+
    if (localStorage) {
+
      localStorage.setItem("badgeCounter", enabled.toString());
+
    }
+
  }
+
</script>
+

+
<script lang="ts">
+
  import Button from "@app/components/Button.svelte";
+
</script>
+

+
<style>
+
  .container {
+
    display: flex;
+
    align-items: center;
+
  }
+
</style>
+

+
<div class="container">
+
  <Button
+
    variant="ghost"
+
    flatRight
+
    active={$badgeCounter}
+
    onclick={() => storeBadgeCounter(true)}>
+
    Show
+
  </Button>
+
  <Button
+
    variant="ghost"
+
    flatLeft
+
    active={!$badgeCounter}
+
    onclick={() => storeBadgeCounter(false)}>
+
    Hide
+
  </Button>
+
</div>
deleted src/components/Border.svelte
@@ -1,293 +0,0 @@
-
<script lang="ts">
-
  import type { Snippet } from "svelte";
-

-
  interface Props {
-
    children: Snippet;
-
    variant:
-
      | "primary"
-
      | "secondary"
-
      | "ghost"
-
      | "float"
-
      | "danger"
-
      | "success"
-
      | "outline";
-
    innerElement?: HTMLElement;
-
    hoverable?: boolean;
-
    onclick?: (e: MouseEvent) => void;
-
    stylePosition?: string;
-
    stylePadding?: string;
-
    styleHeight?: string;
-
    styleMaxHeight?: string;
-
    styleMaxWidth?: string;
-
    styleMinHeight?: string;
-
    styleMinWidth?: string;
-
    styleWidth?: string;
-
    styleDisplay?: string;
-
    styleCursor?: "default" | "pointer" | "text";
-
    styleGap?: string;
-
    styleOverflow?: string;
-
    flatTop?: boolean;
-
    flatBottom?: boolean;
-
    styleBackgroundColor?: string;
-
    styleFlexDirection?: string;
-
    styleAlignSelf?: string;
-
    styleAlignItems?: string;
-
    styleJustifyContent?: string;
-
  }
-

-
  /* eslint-disable prefer-const */
-
  let {
-
    children,
-
    variant,
-
    hoverable = false,
-
    innerElement = $bindable(),
-
    onclick,
-
    stylePadding,
-
    styleHeight,
-
    styleMaxHeight,
-
    styleMaxWidth,
-
    styleMinHeight,
-
    stylePosition,
-
    styleWidth,
-
    styleDisplay = "flex",
-
    styleCursor = "default",
-
    styleGap = "0.5rem",
-
    styleMinWidth,
-
    styleOverflow,
-
    flatTop = false,
-
    flatBottom = false,
-
    styleBackgroundColor = "var(--color-background-default)",
-
    styleFlexDirection = "row",
-
    styleAlignSelf,
-
    styleAlignItems = "center",
-
    styleJustifyContent,
-
  }: Props = $props();
-

-
  const style = $derived(
-
    `--local-background-color: ${styleBackgroundColor};` +
-
      `--local-button-color-1: var(--color-fill-${variant});` +
-
      `--local-hover-background-color: ${hoverable ? "var(--color-background-float)" : styleBackgroundColor}`,
-
  );
-
</script>
-

-
<style>
-
  .container {
-
    -webkit-touch-callout: none;
-
    -webkit-user-select: none;
-
    user-select: none;
-

-
    flex: 1;
-
    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";
-
  }
-

-
  .container:hover > .p2-3,
-
  .container:hover > .p3-2,
-
  .container:hover > .p3-3,
-
  .container:hover > .p3-4,
-
  .container:hover > .p4-3 {
-
    background-color: var(--local-hover-background-color);
-
  }
-

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

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

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

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

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

-
  .flat-top > .p1-3,
-
  .flat-top > .p2-2,
-
  .flat-top > .p2-4 {
-
    background-color: transparent;
-
  }
-

-
  .flat-top > .p1-1,
-
  .flat-top > .p1-5,
-
  .flat-top > .p2-1,
-
  .flat-top > .p2-5 {
-
    background-color: var(--local-button-color-1);
-
  }
-

-
  .flat-bottom > .p4-2,
-
  .flat-bottom > .p4-4 {
-
    background-color: transparent;
-
  }
-

-
  .flat-bottom > .p4-1,
-
  .flat-bottom > .p4-5,
-
  .flat-bottom > .p5-3,
-
  .flat-bottom > .p5-1,
-
  .flat-bottom > .p5-2,
-
  .flat-bottom > .p5-4,
-
  .flat-bottom > .p5-5 {
-
    background-color: var(--local-button-color-1);
-
  }
-
</style>
-

-
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
-
<div
-
  style:width={styleWidth}
-
  style:max-width={styleMaxWidth}
-
  style:align-self={styleAlignSelf}
-
  style:cursor={styleCursor}
-
  class="container"
-
  class:flat-top={flatTop}
-
  class:flat-bottom={flatBottom}
-
  {onclick}
-
  role={onclick !== undefined ? "button" : undefined}
-
  tabindex={onclick !== undefined ? 0 : undefined}
-
  {style}
-
  style:min-height={styleMinHeight}
-
  style:height={styleHeight}>
-
  <div class="p1-1"></div>
-
  <div class="p1-2"></div>
-
  <div class="p1-3"></div>
-
  <div class="p1-4"></div>
-
  <div class="p1-5"></div>
-

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

-
  <div class="p3-1"></div>
-
  <div class="p3-2"></div>
-
  <div
-
    class="p3-3"
-
    bind:this={innerElement}
-
    style:max-height={styleMaxHeight}
-
    style:min-width={styleMinWidth}
-
    style:display={styleDisplay}
-
    style:position={stylePosition}
-
    style:padding={stylePadding}
-
    style:gap={styleGap}
-
    style:overflow={styleOverflow}
-
    style:justify-content={styleJustifyContent}
-
    style:align-items={styleAlignItems}
-
    style:flex-direction={styleFlexDirection}>
-
    {@render children()}
-
  </div>
-
  <div class="p3-4"></div>
-
  <div class="p3-5"></div>
-

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

-
  <div class="p5-1"></div>
-
  <div class="p5-2"></div>
-
  <div class="p5-3"></div>
-
  <div class="p5-4"></div>
-
  <div class="p5-5"></div>
-
</div>
modified src/components/Button.svelte
@@ -1,23 +1,29 @@
<script lang="ts">
  import type { Snippet } from "svelte";

+
  type Variant = "secondary" | "ghost" | "naked" | "outline";
+

  interface Props {
    id?: string;
    children: Snippet;
-
    variant: "primary" | "secondary" | "ghost" | "success" | "danger";
-
    onclick?: () => void;
+
    variant?: Variant;
+
    onclick?: (e: MouseEvent) => void;
    disabled?: boolean;
    active?: boolean;
    flatLeft?: boolean;
    flatRight?: boolean;
    title?: string;
    styleHeight?: "2rem" | "2.5rem";
+
    styleWidth?: string;
+
    styleJustifyContent?: string;
+
    stylePadding?: string;
+
    keyShortcuts?: string;
  }

  const {
    id,
    children,
-
    variant,
+
    variant = "ghost",
    onclick = undefined,
    disabled = false,
    active = false,
@@ -25,396 +31,149 @@
    flatRight = false,
    title,
    styleHeight = "2rem",
+
    styleWidth = undefined,
+
    styleJustifyContent = undefined,
+
    stylePadding = "0 0.5rem",
+
    keyShortcuts,
  }: Props = $props();

-
  const style = $derived(
-
    `--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);` +
-
      `--text-color: ${variant === "ghost" ? "var(--color-foreground-contrast)" : "var(--color-foreground-white)"};` +
-
      `--text-color-active: ${variant === "ghost" ? "var(--color-foreground-emphasized)" : "var(--color-foreground-white)"};`,
-
  );
+
  const fills: Record<Variant, string> = {
+
    secondary: "var(--color-surface-brand-secondary)",
+
    ghost: "var(--color-surface-subtle)",
+
    naked: "transparent",
+
    outline: "transparent",
+
  };
+
  const fillsHover: Record<Variant, string> = {
+
    secondary: "var(--color-surface-brand-secondary)",
+
    ghost: "var(--color-surface-mid)",
+
    naked: "var(--color-surface-subtle)",
+
    outline: "var(--color-surface-subtle)",
+
  };
+
  const fillsActive: Record<Variant, string> = {
+
    secondary: "var(--color-surface-brand-primary)",
+
    ghost: "var(--color-surface-strong)",
+
    naked: "var(--color-surface-strong)",
+
    outline: "var(--color-surface-strong)",
+
  };
+
  const colors: Record<Variant, string> = {
+
    secondary: "var(--color-text-on-brand)",
+
    ghost: "var(--color-text-primary)",
+
    naked: "inherit",
+
    outline: "inherit",
+
  };
+
  const colorsHover: Record<Variant, string> = {
+
    secondary: "var(--color-text-on-brand)",
+
    ghost: "var(--color-text-primary)",
+
    naked: "inherit",
+
    outline: "var(--color-text-primary)",
+
  };
+
  const colorsActive: Record<Variant, string> = {
+
    secondary: "var(--color-text-on-brand)",
+
    ghost: "var(--color-text-primary)",
+
    naked: "inherit",
+
    outline: "var(--color-text-primary)",
+
  };
</script>

<style>
-
  .container {
+
  .button {
    white-space: nowrap;

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

-
    color: var(--text-color);
-

-
    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";
-
  }
-

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

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

-
  .container:hover:not(.disabled) .p3-4,
-
  .container:hover:not(.disabled) .p3-5,
-
  .container:hover:not(.disabled) .p4-3,
-
  .container:hover:not(.disabled) .p4-4,
-
  .container:hover:not(.disabled) .p5-3 {
-
    background-color: var(--button-color-1);
+
    display: inline-flex;
+
    align-items: center;
+
    gap: 0.5rem;
+
    border-radius: var(--border-radius-sm);
+
    border: none;
+
    background-color: var(--color-fill);
+
    color: var(--color-text);
+
    transition: background-color 0.1s ease;
  }

-
  .container.active:not(.disabled) .p1-3,
-
  .container.active:not(.disabled) .p2-2,
-
  .container.active:not(.disabled) .p2-4,
-
  .container.active:not(.disabled) .p3-1,
-
  .container.active:not(.disabled) .p3-3,
-
  .container.active:not(.disabled) .p3-5,
-
  .container.active:not(.disabled) .p4-2,
-
  .container.active:not(.disabled) .p4-4,
-
  .container.active:not(.disabled) .p5-3,
-
  .container:active:not(.disabled) .p1-3,
-
  .container:active:not(.disabled) .p2-2,
-
  .container:active:not(.disabled) .p2-4,
-
  .container:active:not(.disabled) .p3-1,
-
  .container:active:not(.disabled) .p3-3,
-
  .container:active:not(.disabled) .p3-5,
-
  .container:active:not(.disabled) .p4-2,
-
  .container:active:not(.disabled) .p4-4,
-
  .container:active:not(.disabled) .p5-3 {
-
    background-color: var(--button-color-1);
+
  .button:hover:not(.disabled) {
+
    background-color: var(--color-fill-hover);
+
    color: var(--color-text-hover);
  }

-
  .container.active:not(.disabled) .p2-3,
-
  .container.active:not(.disabled) .p3-2,
-
  .container:active:not(.disabled) .p2-3,
-
  .container:active:not(.disabled) .p3-2 {
-
    background-color: var(--button-color-3);
+
  .button.active:not(.disabled),
+
  .button:active:not(.disabled) {
+
    background-color: var(--color-fill-active);
+
    color: var(--color-text-active);
  }

-
  .container.active:not(.disabled) .p3-4,
-
  .container.active:not(.disabled) .p4-3,
-
  .container:active:not(.disabled) .p3-4,
-
  .container:active:not(.disabled) .p4-3 {
-
    background-color: var(--button-color-2);
+
  .button.disabled {
+
    cursor: default;
+
    color: var(--color-text-disabled);
  }

-
  .container.disabled {
-
    color: var(--color-foreground-disabled);
-
  }
-
  .container.active:not(.disabled) {
-
    color: var(--text-color-active);
+
  .button.secondary.disabled,
+
  .button.ghost.disabled {
+
    background-color: var(--color-surface-subtle);
  }

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

-
  .flat-right .p1-4,
-
  .flat-right .p1-5,
-
  .flat-right .p2-5,
-
  .flat-right .p3-4 {
-
    background-color: var(--button-color-1);
-
  }
-
  .flat-right .p2-4 {
-
    background-color: var(--button-color-2);
-
  }
-
  .flat-right .p4-5,
-
  .flat-right .p5-4,
-
  .flat-right .p5-5 {
-
    background-color: var(--button-color-3);
+
  .button.outline {
+
    border: 1px solid var(--color-fill);
  }

-
  .container:hover:not(.disabled).flat-right .p1-4,
-
  .container:hover:not(.disabled).flat-right .p1-5,
-
  .container:hover:not(.disabled).flat-right .p2-5,
-
  .container:hover:not(.disabled).flat-right .p3-4 {
-
    background-color: var(--button-color-2);
-
  }
-
  .container:hover:not(.disabled).flat-right .p2-4 {
-
    background-color: var(--button-color-4);
-
  }
-
  .container:hover:not(.disabled).flat-right .p4-5,
-
  .container:hover:not(.disabled).flat-right .p5-4,
-
  .container:hover:not(.disabled).flat-right .p5-5 {
-
    background-color: var(--button-color-1);
+
  .button.outline.active:not(.disabled),
+
  .button.outline:active:not(.disabled) {
+
    border-color: var(--color-fill-active);
  }

-
  .container.active:not(.disabled).flat-right .p1-4,
-
  .container.active:not(.disabled).flat-right .p1-5,
-
  .container.active:not(.disabled).flat-right .p2-5,
-
  .container.active:not(.disabled).flat-right .p3-4,
-
  .container.active:not(.disabled).flat-right .p4-5,
-
  .container.active:not(.disabled).flat-right .p5-4,
-
  .container.active:not(.disabled).flat-right .p5-5,
-
  .container:active:not(.disabled).flat-right .p1-4,
-
  .container:active:not(.disabled).flat-right .p1-5,
-
  .container:active:not(.disabled).flat-right .p2-5,
-
  .container:active:not(.disabled).flat-right .p3-4,
-
  .container:active:not(.disabled).flat-right .p4-5,
-
  .container:active:not(.disabled).flat-right .p5-4,
-
  .container:active:not(.disabled).flat-right .p5-5 {
-
    background-color: var(--button-color-1);
-
  }
-
  .container.active:not(.disabled).flat-right .p2-4,
-
  .container:active:not(.disabled).flat-right .p2-4 {
-
    background-color: var(--button-color-3);
-
  }
-
  .container.active:not(.disabled).flat-right .p3-5,
-
  .container.active:not(.disabled).flat-right .p4-4,
-
  .container:active:not(.disabled).flat-right .p3-5,
-
  .container:active:not(.disabled).flat-right .p4-4 {
-
    background-color: var(--button-color-2);
+
  .button.outline.disabled {
+
    border-color: var(--color-border-subtle);
  }

-
  .flat-left .p1-1,
-
  .flat-left .p1-2,
-
  .flat-left .p2-1,
-
  .flat-left .p3-2 {
-
    background-color: var(--button-color-1);
-
  }
-
  .flat-left .p2-2,
-
  .flat-left .p3-1 {
-
    background-color: var(--button-color-2);
-
  }
-
  .flat-left .p4-1,
-
  .flat-left .p4-2,
-
  .flat-left .p5-1,
-
  .flat-left .p5-2 {
-
    background-color: var(--button-color-3);
-
  }
-

-
  .container:hover:not(.disabled).flat-left .p1-1,
-
  .container:hover:not(.disabled).flat-left .p1-2,
-
  .container:hover:not(.disabled).flat-left .p2-1,
-
  .container:hover:not(.disabled).flat-left .p3-2 {
-
    background-color: var(--button-color-2);
-
  }
-
  .container:hover:not(.disabled).flat-left .p2-2,
-
  .container:hover:not(.disabled).flat-left .p3-1 {
-
    background-color: var(--button-color-4);
-
  }
-
  .container:hover:not(.disabled).flat-left .p4-1,
-
  .container:hover:not(.disabled).flat-left .p4-2,
-
  .container:hover:not(.disabled).flat-left .p5-1,
-
  .container:hover:not(.disabled).flat-left .p5-2 {
-
    background-color: var(--button-color-1);
-
  }
-

-
  .container.active:not(.disabled).flat-left .p1-1,
-
  .container.active:not(.disabled).flat-left .p1-2,
-
  .container.active:not(.disabled).flat-left .p2-1,
-
  .container.active:not(.disabled).flat-left .p3-2,
-
  .container.active:not(.disabled).flat-left .p4-1,
-
  .container.active:not(.disabled).flat-left .p4-2,
-
  .container.active:not(.disabled).flat-left .p5-1,
-
  .container.active:not(.disabled).flat-left .p5-2,
-
  .container:active:not(.disabled).flat-left .p1-1,
-
  .container:active:not(.disabled).flat-left .p1-2,
-
  .container:active:not(.disabled).flat-left .p2-1,
-
  .container:active:not(.disabled).flat-left .p3-2,
-
  .container:active:not(.disabled).flat-left .p4-1,
-
  .container:active:not(.disabled).flat-left .p4-2,
-
  .container:active:not(.disabled).flat-left .p5-1,
-
  .container:active:not(.disabled).flat-left .p5-2 {
-
    background-color: var(--button-color-1);
-
  }
-
  .container.active:not(.disabled).flat-left .p2-2,
-
  .container.active:not(.disabled).flat-left .p3-1,
-
  .container:active:not(.disabled).flat-left .p2-2,
-
  .container:active:not(.disabled).flat-left .p3-1 {
-
    background-color: var(--button-color-3);
-
  }
-
  .container.active:not(.disabled).flat-left .p4-2,
-
  .container:active:not(.disabled).flat-left .p4-2 {
-
    background-color: var(--button-color-2);
-
  }
-

-
  .p1-1 {
-
    grid-area: p1-1;
-
    background-color: transparent;
-
  }
-
  .p1-2 {
-
    grid-area: p1-2;
-
    background-color: transparent;
-
  }
-
  .p1-3 {
-
    grid-area: p1-3;
-
    background-color: var(--button-color-1);
-
  }
-
  .p1-4 {
-
    grid-area: p1-4;
-
    background-color: transparent;
-
  }
-
  .p1-5 {
-
    grid-area: p1-5;
-
    background-color: transparent;
+
  .button.flat-left {
+
    border-top-left-radius: 0;
+
    border-bottom-left-radius: 0;
  }

-
  .p2-1 {
-
    grid-area: p2-1;
-
    background-color: transparent;
-
  }
-
  .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;
-
    background-color: transparent;
-
  }
-

-
  .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: 0 0.5rem;
-
    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;
-
    background-color: transparent;
-
  }
-
  .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;
-
    background-color: transparent;
-
  }
-

-
  .p5-1 {
-
    grid-area: p5-1;
-
    background-color: transparent;
-
  }
-
  .p5-2 {
-
    grid-area: p5-2;
-
    background-color: transparent;
-
  }
-
  .p5-3 {
-
    grid-area: p5-3;
-
    background-color: var(--button-color-3);
-
  }
-
  .p5-4 {
-
    grid-area: p5-4;
-
    background-color: transparent;
-
  }
-
  .p5-5 {
-
    grid-area: p5-5;
-
    background-color: transparent;
+
  .button.flat-right {
+
    border-top-right-radius: 0;
+
    border-bottom-right-radius: 0;
  }
</style>

<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
  {id}
-
  class="container active"
-
  style:cursor={!disabled ? "pointer" : "default"}
-
  style:height={styleHeight}
+
  class="button txt-body-m-medium"
+
  class:secondary={variant === "secondary"}
+
  class:ghost={variant === "ghost"}
+
  class:naked={variant === "naked"}
+
  class:outline={variant === "outline"}
  class:disabled
  class:active
-
  class:flat-right={flatRight}
  class:flat-left={flatLeft}
+
  class:flat-right={flatRight}
+
  style:cursor={disabled
+
    ? variant === "naked"
+
      ? "inherit"
+
      : "default"
+
    : "pointer"}
+
  style:height={styleHeight}
+
  style:width={styleWidth}
+
  style:padding={stylePadding}
+
  style:justify-content={styleJustifyContent ??
+
    (styleWidth ? "center" : undefined)}
+
  style:--color-fill={fills[variant]}
+
  style:--color-fill-hover={fillsHover[variant]}
+
  style:--color-fill-active={fillsActive[variant]}
+
  style:--color-text={colors[variant]}
+
  style:--color-text-hover={colorsHover[variant]}
+
  style:--color-text-active={colorsActive[variant]}
+
  aria-keyshortcuts={keyShortcuts}
  onclick={!disabled ? onclick : undefined}
  role="button"
  tabindex="0"
-
  {title}
-
  {style}>
-
  <div class="p1-1"></div>
-
  <div class="p1-2"></div>
-
  <div class="p1-3"></div>
-
  <div class="p1-4"></div>
-
  <div class="p1-5"></div>
-

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

-
  <div class="p3-1"></div>
-
  <div class="p3-2"></div>
-
  <div class="p3-3 txt-semibold txt-small">
-
    {@render children()}
-
  </div>
-
  <div class="p3-4"></div>
-
  <div class="p3-5"></div>
-

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

-
  <div class="p5-1"></div>
-
  <div class="p5-2"></div>
-
  <div class="p5-3"></div>
-
  <div class="p5-4"></div>
-
  <div class="p5-5"></div>
+
  {title}>
+
  {@render children()}
</div>
modified src/components/Changes.svelte
@@ -5,13 +5,13 @@
  import { cachedGetDiff, cachedListCommits } from "@app/lib/invoke";
  import { pluralize } from "@app/lib/utils";

+
  import Button from "@app/components/Button.svelte";
  import Changeset from "@app/components/Changeset.svelte";
  import CobCommitTeaser from "@app/components/CobCommitTeaser.svelte";
  import CommitsContainer from "@app/components/CommitsContainer.svelte";
  import Icon from "@app/components/Icon.svelte";
  import Id from "@app/components/Id.svelte";
  import JobCob from "@app/components/JobCob.svelte";
-
  import NakedButton from "@app/components/NakedButton.svelte";

  interface Props {
    patchId: string;
@@ -66,11 +66,11 @@
    position: relative;
    display: flex;
    flex-direction: column;
-
    font-size: 0.875rem;
+
    font: var(--txt-body-m-regular);
    margin-left: 0.5rem;
    gap: 0.5rem;
    padding: 1rem 0.5rem 0.5rem 1rem;
-
    border-left: 1px solid var(--color-fill-separator);
+
    border-left: 1px solid var(--color-border-subtle);
  }
  .commit:last-of-type::after {
    content: "";
@@ -78,7 +78,7 @@
    left: -18.5px;
    top: 14px;
    bottom: -0.5rem;
-
    border-left: 4px solid var(--color-background-default);
+
    border-left: 4px solid var(--color-surface-canvas);
  }
  .commit-dot {
    width: 0.25rem;
@@ -86,30 +86,30 @@
    position: absolute;
    top: 0.625rem;
    left: -18.5px;
-
    background-color: var(--color-fill-separator);
+
    background-color: var(--color-border-subtle);
  }
  .commit {
    cursor: pointer;
  }
  .commit-dot.active {
-
    background-color: var(--color-border-focus);
+
    background-color: var(--color-border-brand);
  }
  .commit:hover:not(.single-commit) .commit-dot:not(.active) {
-
    background-color: var(--color-foreground-contrast);
+
    background-color: var(--color-text-primary);
  }
  .commit:hover:not(.single-commit) {
-
    background-color: var(--color-background-float);
+
    background-color: var(--color-surface-canvas);
  }
  .disabled {
-
    color: var(--color-foreground-disabled) !important;
+
    color: var(--color-text-disabled) !important;
  }
  .summary {
    cursor: pointer;
    padding: 0.25rem 0;
  }
  .summary:hover:not(.single-commit) {
-
    background-color: var(--color-background-float);
-
    color: var(--color-foreground-contrast) !important;
+
    background-color: var(--color-surface-canvas);
+
    color: var(--color-text-primary) !important;
  }
  .single-commit {
    cursor: default !important;
@@ -117,30 +117,25 @@
</style>

<div
-
  class="txt-semibold global-flex"
+
  class="txt-body-m-regular global-flex"
  style:margin-bottom={hideChanges ? undefined : "1rem"}>
  <div class="global-flex">
-
    <NakedButton
-
      variant="ghost"
-
      onclick={() => (hideChanges = !hideChanges)}
-
      stylePadding="0 4px">
+
    <Button variant="naked" onclick={() => (hideChanges = !hideChanges)}>
      <Icon name={hideChanges ? "chevron-right" : "chevron-down"} />
-
    </NakedButton>
-
    <div class="txt-semibold global-flex txt-regular">Changes</div>
+
    </Button>
+
    <div class="txt-body-m-regular global-flex">Changes</div>
  </div>
  {#if !hideChanges}
    <div style:margin-left="auto">
-
      <NakedButton
-
        variant="ghost"
-
        onclick={() => (filesExpanded = !filesExpanded)}>
+
      <Button variant="naked" onclick={() => (filesExpanded = !filesExpanded)}>
        {#if filesExpanded === true}
-
          <Icon name="collapse" />
+
          <Icon name="collapse-vertical" />
          Collapse all
        {:else}
-
          <Icon name="expand" />
+
          <Icon name="expand-vertical" />
          Expand all
        {/if}
-
      </NakedButton>
+
      </Button>
    </div>
  {/if}
</div>
@@ -150,13 +145,13 @@
    <div style:margin-bottom="1rem">
      <CommitsContainer>
        {#snippet leftHeader()}
-
          <div class="txt-semibold">Commits</div>
+
          <div class="txt-body-m-regular">Commits</div>
        {/snippet}
        <div style:padding="0 1rem">
          <!-- svelte-ignore a11y_no_static_element_interactions -->
          <!-- svelte-ignore a11y_click_events_have_key_events -->
          <div
-
            class="global-flex txt-small summary"
+
            class="global-flex txt-body-m-regular summary"
            class:single-commit={commits.length === 1}
            class:disabled={selectedCommit}
            onclick={() => {
@@ -169,11 +164,8 @@
            <Icon name="branch" />
            {commits.length}
            {pluralize("commit", commits.length)} on base
-
            <Id
-
              id={revision.base}
-
              clipboard={revision.base}
-
              variant={selectedCommit ? "none" : "commit"} />
-
            <div class="global-counter">Base</div>
+
            <Id id={revision.base} clipboard={revision.base} />
+
            <div class="global-chip">Base</div>
          </div>
          <div class="commits">
            {#each [...commits].reverse() as commit, idx}
@@ -211,7 +203,7 @@
  {/await}

  {#await cachedGetDiff(rid, { base, head, unified: 3, highlight: true })}
-
    <span class="txt-small">Loading…</span>
+
    <span class="txt-body-m-regular">Loading…</span>
  {:then diff}
    <Changeset expanded={filesExpanded} {head} {diff} {codeComments} />
  {/await}
modified src/components/CheckoutPatchButton.svelte
@@ -1,10 +1,9 @@
<script lang="ts">
  import { formatOid } from "@app/lib/utils";

-
  import Border from "@app/components/Border.svelte";
+
  import Button from "@app/components/Button.svelte";
  import Command from "@app/components/Command.svelte";
  import Icon from "@app/components/Icon.svelte";
-
  import NakedButton from "@app/components/NakedButton.svelte";
  import Popover from "@app/components/Popover.svelte";

  interface Props {
@@ -26,34 +25,32 @@
  let popoverExpanded: boolean = $state(false);
</script>

-
<Popover
-
  popoverPositionRight="0"
-
  popoverPositionTop="3rem"
-
  bind:expanded={popoverExpanded}>
+
<Popover placement="bottom-end" bind:expanded={popoverExpanded}>
  {#snippet toggle(onclick)}
-
    <NakedButton
+
    <Button
+
      variant="naked"
      title="Checkout patch"
-
      styleHeight="2.5rem"
-
      variant="ghost"
      {onclick}
      active={popoverExpanded}>
      <Icon name="checkout" />
      <span class="global-hide-on-medium-desktop-down">Checkout patch</span>
-
    </NakedButton>
+
    </Button>
  {/snippet}
  {#snippet popover()}
-
    <Border
-
      styleAlignItems="flex-start"
-
      styleBackgroundColor="var(--color-background-float)"
-
      styleFlexDirection="column"
-
      styleGap="0.5rem"
-
      stylePadding="1rem"
-
      styleWidth="max-content"
-
      variant="ghost">
-
      <span class="txt-small">
+
    <div
+
      style:border="1px solid var(--color-border-subtle)"
+
      style:border-radius="var(--border-radius-sm)"
+
      style:display="flex"
+
      style:gap="0.5rem"
+
      style:align-items="flex-start"
+
      style:background-color="var(--color-surface-canvas)"
+
      style:flex-direction="column"
+
      style:padding="1rem"
+
      style:width="max-content">
+
      <span class="txt-body-m-regular">
        To checkout this patch in your working copy, run:
      </span>
      <Command command={checkoutCommand} styleWidth="100%" />
-
    </Border>
+
    </div>
  {/snippet}
</Popover>
modified src/components/CheckoutRepoButton.svelte
@@ -1,5 +1,4 @@
<script lang="ts">
-
  import Border from "@app/components/Border.svelte";
  import Button from "@app/components/Button.svelte";
  import Command from "@app/components/Command.svelte";
  import Icon from "@app/components/Icon.svelte";
@@ -14,13 +13,10 @@
  let popoverExpanded: boolean = $state(false);
</script>

-
<Popover
-
  popoverPositionRight="0"
-
  popoverPositionTop="3rem"
-
  bind:expanded={popoverExpanded}>
+
<Popover placement="bottom-end" bind:expanded={popoverExpanded}>
  {#snippet toggle(onclick)}
    <Button
-
      styleHeight="2.5rem"
+
      styleHeight="2rem"
      variant="secondary"
      {onclick}
      active={popoverExpanded}>
@@ -29,18 +25,20 @@
  {/snippet}

  {#snippet popover()}
-
    <Border
-
      styleAlignItems="flex-start"
-
      styleBackgroundColor="var(--color-background-float)"
-
      styleFlexDirection="column"
-
      styleGap="0.5rem"
-
      stylePadding="1rem"
-
      styleWidth="max-content"
-
      variant="ghost">
-
      <span class="txt-small">
+
    <div
+
      style:border="1px solid var(--color-border-subtle)"
+
      style:border-radius="var(--border-radius-sm)"
+
      style:display="flex"
+
      style:gap="0.5rem"
+
      style:align-items="flex-start"
+
      style:background-color="var(--color-surface-canvas)"
+
      style:flex-direction="column"
+
      style:padding="1rem"
+
      style:width="max-content">
+
      <span class="txt-body-m-regular">
        To checkout a working copy of this repo, run:
      </span>
      <Command command={`rad checkout ${rid}`} styleWidth="100%" />
-
    </Border>
+
    </div>
  {/snippet}
</Popover>
modified src/components/Clipboard.svelte
@@ -1,6 +1,13 @@
<svelte:options customElement="radicle-clipboard" />

<script lang="ts">
+
  import {
+
    autoUpdate,
+
    computePosition,
+
    flip,
+
    offset,
+
    shift,
+
  } from "@floating-ui/dom";
  import debounce from "lodash/debounce";

  import { writeToClipboard } from "@app/lib/invoke";
@@ -9,24 +16,61 @@

  interface Props {
    text: string;
+
    noPopover?: boolean;
  }

-
  const { text }: Props = $props();
+
  const { text, noPopover = false }: Props = $props();

  let icon: "copy" | "checkmark" = $state("copy");
+
  let tooltip = $state("Click to copy");
+
  let visible = $state(false);
+
  let anchorEl: HTMLElement | undefined = $state();
+
  let floatingEl: HTMLDivElement | undefined = $state();

  const restoreIcon = debounce(() => {
    icon = "copy";
-
  }, 800);
+
    tooltip = "Click to copy";
+
    visible = false;
+
  }, 1000);
+

+
  const setVisible = debounce((value: boolean) => {
+
    visible = value;
+
  }, 50);
+

+
  $effect(() => {
+
    void tooltip;
+
    if (floatingEl && anchorEl) {
+
      const cleanup = autoUpdate(anchorEl, floatingEl, () => {
+
        void computePosition(anchorEl!, floatingEl!, {
+
          placement: "top",
+
          middleware: [offset(6), flip(), shift({ padding: 8 })],
+
        }).then(({ x, y }) => {
+
          if (floatingEl) {
+
            floatingEl.style.left = `${x}px`;
+
            floatingEl.style.top = `${y}px`;
+
            floatingEl.style.visibility = "visible";
+
          }
+
        });
+
      });
+
      return cleanup;
+
    }
+
  });

  export async function copy() {
    await writeToClipboard(text);
    icon = "checkmark";
+
    tooltip = "Copied to clipboard";
+
    if (!noPopover) {
+
      setVisible(true);
+
    }
    restoreIcon();
  }
</script>

<style>
+
  .container {
+
    display: inline-flex;
+
  }
  .clipboard {
    width: 1.5rem;
    height: 1.5rem;
@@ -36,9 +80,43 @@
    align-items: center;
    user-select: none;
  }
+
  .popover {
+
    position: fixed;
+
    top: 0;
+
    left: 0;
+
    visibility: hidden;
+
    display: flex;
+
    align-items: center;
+
    flex-direction: row;
+
    gap: 0.5rem;
+
    justify-content: center;
+
    z-index: 20;
+
    background: var(--color-surface-subtle);
+
    color: var(--color-text-primary);
+
    border: 1px solid var(--color-border-subtle);
+
    border-radius: var(--border-radius-md);
+
    box-shadow: var(--elevation-low);
+
    font: var(--txt-body-m-regular);
+
    white-space: nowrap;
+
    width: max-content;
+
    padding: 0.25rem 0.5rem;
+
  }
</style>

<!-- svelte-ignore a11y_click_events_have_key_events -->
-
<span role="button" tabindex="0" class="clipboard" onclick={copy}>
-
  <Icon name={icon} />
+
<span
+
  role="group"
+
  class="container"
+
  bind:this={anchorEl}
+
  onmouseenter={() => !noPopover && setVisible(true)}
+
  onmouseleave={() => !noPopover && setVisible(false)}>
+
  <span role="button" tabindex="0" class="clipboard" onclick={copy}>
+
    <Icon name={icon} />
+
  </span>
+
  {#if visible}
+
    <div bind:this={floatingEl} class="popover">
+
      <Icon name={icon} />
+
      {tooltip}
+
    </div>
+
  {/if}
</span>
added src/components/CobCacheWarning.svelte
@@ -0,0 +1,62 @@
+
<script lang="ts">
+
  import type { CacheEvent } from "@bindings/cob/CacheEvent";
+

+
  import Button from "@app/components/Button.svelte";
+
  import Icon from "@app/components/Icon.svelte";
+
  import Spinner from "@app/components/Spinner.svelte";
+

+
  interface Props {
+
    noun: string;
+
    cacheState: CacheEvent | undefined;
+
    onRebuild: () => Promise<void>;
+
  }
+

+
  const { noun, cacheState, onRebuild }: Props = $props();
+
</script>
+

+
<style>
+
  .container {
+
    padding: 1rem;
+
  }
+
  .banner {
+
    display: flex;
+
    align-items: center;
+
    gap: 1rem;
+
    padding: 0.25rem 0.5rem;
+
    overflow: hidden;
+
    border: 1px solid var(--color-feedback-warning-border);
+
    border-radius: var(--border-radius-md);
+
    background-color: var(--color-feedback-warning-bg);
+
  }
+
  .action {
+
    margin-left: auto;
+
  }
+
</style>
+

+
<div class="container">
+
  <div class="banner">
+
    <div class="txt-overflow txt-body-m-regular global-flex">
+
      <Icon name="warning" />
+
      <span class="txt-overflow">
+
        There's a problem with your COB cache, so some {noun} may not be displayed.
+
        You can rebuild the cache to resolve this.
+
      </span>
+
    </div>
+
    <div class="action">
+
      <Button
+
        variant="naked"
+
        onclick={onRebuild}
+
        disabled={cacheState !== undefined}>
+
        {#if cacheState?.event === "started" || cacheState?.event === "progress"}
+
          Rebuilding
+
          <Spinner />
+
        {:else if cacheState?.event === "finished"}
+
          Done
+
          <Icon name="checkmark" />
+
        {:else}
+
          Rebuild cache
+
        {/if}
+
      </Button>
+
    </div>
+
  </div>
+
</div>
modified src/components/CobCommitTeaser.svelte
@@ -4,11 +4,11 @@

  import { twemoji } from "@app/lib/utils";

+
  import Button from "@app/components/Button.svelte";
  import CompactCommitAuthorship from "@app/components/CompactCommitAuthorship.svelte";
  import Icon from "@app/components/Icon.svelte";
  import Id from "@app/components/Id.svelte";
  import InlineTitle from "@app/components/InlineTitle.svelte";
-
  import NakedButton from "@app/components/NakedButton.svelte";

  interface Props {
    children?: Snippet;
@@ -32,7 +32,7 @@
<style>
  .teaser {
    display: flex;
-
    font-size: var(--font-size-small);
+
    font: var(--txt-body-m-regular);
    align-items: start;
    padding: 0.125rem 0;
  }
@@ -59,7 +59,7 @@
    height: 1.3125rem;
  }
  .commit-message {
-
    font-size: var(--font-size-tiny);
+
    font: var(--txt-body-s-regular);
    -webkit-touch-callout: initial;
    -webkit-user-select: text;
    user-select: text;
@@ -70,7 +70,7 @@
    align-items: center;
  }
  .disabled {
-
    color: var(--color-foreground-disabled) !important;
+
    color: var(--color-text-disabled) !important;
  }
  pre {
    white-space: pre-wrap;
@@ -88,15 +88,15 @@
      </div>
      {#if commit.message.trim() !== commit.summary.trim()}
        <div class="commit-expand-button">
-
          <NakedButton
+
          <Button
+
            variant="naked"
            stylePadding="0 0.25rem"
-
            variant="ghost"
            onclick={e => {
              e.stopPropagation();
              commitMessageVisible = !commitMessageVisible;
            }}>
            <Icon name="ellipsis" />
-
          </NakedButton>
+
          </Button>
        </div>
      {/if}
    </div>
@@ -109,10 +109,7 @@
  <div class="right">
    {@render children?.()}
    <CompactCommitAuthorship {commit}>
-
      <Id
-
        id={commit.id}
-
        clipboard={commit.id}
-
        variant={disabled ? "none" : "commit"} />
+
      <Id id={commit.id} clipboard={commit.id} />
    </CompactCommitAuthorship>
  </div>
</div>
added src/components/CodeFontSwitch.svelte
@@ -0,0 +1,48 @@
+
<script lang="ts" module>
+
  import { writable } from "svelte/store";
+

+
  type CodeFont = "jetbrains" | "system";
+

+
  export const codeFont = writable<CodeFont>(loadCodeFont());
+

+
  function loadCodeFont(): CodeFont {
+
    const stored = localStorage ? localStorage.getItem("codefont") : null;
+
    return stored === "system" ? "system" : "jetbrains";
+
  }
+

+
  export function storeCodeFont(font: CodeFont): void {
+
    codeFont.set(font);
+
    if (localStorage) {
+
      localStorage.setItem("codefont", font);
+
    }
+
  }
+
</script>
+

+
<script lang="ts">
+
  import Button from "@app/components/Button.svelte";
+
</script>
+

+
<style>
+
  .container {
+
    display: flex;
+
    align-items: center;
+
  }
+
</style>
+

+
<div class="container">
+
  <Button
+
    variant="ghost"
+
    flatRight
+
    active={$codeFont === "jetbrains"}
+
    onclick={() => storeCodeFont("jetbrains")}>
+
    JetBrains Mono
+
  </Button>
+

+
  <Button
+
    variant="ghost"
+
    flatLeft
+
    active={$codeFont === "system"}
+
    onclick={() => storeCodeFont("system")}>
+
    System
+
  </Button>
+
</div>
modified src/components/Command.svelte
@@ -1,5 +1,4 @@
<script lang="ts">
-
  import Border from "@app/components/Border.svelte";
  import Clipboard from "@app/components/Clipboard.svelte";

  interface Props {
@@ -15,27 +14,38 @@

<style>
  .cmd {
-
    color: var(--color-foreground-dim);
+
    color: var(--color-text-secondary);
  }
  .cmd:hover {
-
    color: var(--color-foreground-contrast);
+
    color: var(--color-text-primary);
+
  }
+
  .hoverable:hover {
+
    background-color: var(--color-surface-canvas);
  }
</style>

-
<div class="cmd txt-monospace txt-small" style:width={styleWidth}>
-
  <Border
-
    hoverable
-
    onclick={() => clipboard.copy()}
-
    styleOverflow="hidden"
-
    styleBackgroundColor="var(--color-background-float)"
-
    styleCursor="pointer"
-
    styleJustifyContent="space-between"
-
    stylePadding="0.25rem 0.5rem"
-
    {styleWidth}
-
    variant="ghost">
+
<div class="cmd txt-code-regular" style:width={styleWidth}>
+
  <!-- svelte-ignore a11y_click_events_have_key_events -->
+
  <div
+
    class="hoverable"
+
    style:border="1px solid var(--color-border-subtle)"
+
    style:border-radius="var(--border-radius-sm)"
+
    style:display="flex"
+
    style:gap="0.5rem"
+
    style:align-items="center"
+
    style:background-color="var(--color-surface-canvas)"
+
    style:overflow="hidden"
+
    style:cursor="pointer"
+
    style:justify-content="space-between"
+
    style:height="2rem"
+
    style:padding="0 0.5rem"
+
    style:width={styleWidth}
+
    role="button"
+
    tabindex="0"
+
    onclick={() => clipboard.copy()}>
    <span class="txt-overflow">
      {showPrompt ? "$ " : ""}{command}
    </span>
-
    <Clipboard bind:this={clipboard} text={command} />
-
  </Border>
+
    <Clipboard bind:this={clipboard} text={command} noPopover />
+
  </div>
</div>
modified src/components/Comment.svelte
@@ -94,11 +94,11 @@
    padding: 0 0.75rem;
    min-height: 1.5rem;
    gap: 0.5rem;
-
    font-size: var(--font-size-small);
+
    font: var(--txt-body-m-regular);
  }
  .card-metadata {
-
    color: var(--color-fill-gray);
-
    font-size: var(--font-size-small);
+
    color: var(--color-text-quaternary);
+
    font: var(--txt-body-m-regular);
  }
  .header-right {
    display: flex;
@@ -110,7 +110,7 @@
    align-items: center;
    min-height: 1.625rem;
    word-wrap: break-word;
-
    font-size: var(--font-size-small);
+
    font: var(--txt-body-m-regular);
    padding: 0 0.75rem;
  }
  .actions {
@@ -121,8 +121,8 @@
    margin-left: 1rem;
  }
  .timestamp {
-
    font-size: var(--font-size-small);
-
    color: var(--color-fill-gray);
+
    font: var(--txt-body-m-regular);
+
    color: var(--color-text-quaternary);
  }
  .icon-button {
    cursor: pointer;
@@ -135,7 +135,7 @@
      <NodeId {...utils.authorForNodeId(author)} />
      {caption}
      {#if id}
-
        <Id {id} clipboard={id} variant="oid" />
+
        <Id {id} clipboard={id} />
      {/if}
      {#if beforeTimestamp}
        {@render beforeTimestamp()}
@@ -158,7 +158,7 @@
      <div class="header-right">
        {#if editComment}
          <div class="icon-button">
-
            <Icon name="pen" onclick={toggleEdit} />
+
            <Icon name="edit" onclick={toggleEdit} />
          </div>
        {/if}
        {#if deleteComment}
@@ -168,8 +168,7 @@
        {/if}
        {#if reactions && reactOnComment}
          <ReactionSelector
-
            popoverPositionRight="0"
-
            popoverPositionBottom="1.5rem"
+
            placement="top-end"
            {reactions}
            select={async ({ authors, emoji }) => {
              try {
@@ -186,7 +185,7 @@

  {#if (body === undefined || body?.trim() === "") && state === "read"}
    <div class="card-body">
-
      <span class="txt-missing txt-small" style:line-height="1.625rem">
+
      <span class="txt-missing txt-body-m-regular" style:line-height="1.625rem">
        No description.
      </span>
    </div>
@@ -233,8 +232,7 @@
    <div class="actions">
      {#if id && reactions && reactOnComment}
        <ReactionSelector
-
          popoverPositionLeft="0"
-
          popoverPositionBottom="1.5rem"
+
          placement="top-start"
          {reactions}
          select={async ({ authors, emoji }) => {
            try {
modified src/components/CommentToggleInput.svelte
@@ -1,7 +1,6 @@
<script lang="ts">
  import type { Embed } from "@bindings/cob/thread/Embed";

-
  import Border from "@app/components/Border.svelte";
  import ExtendedTextarea from "@app/components/ExtendedTextarea.svelte";

  interface Props {
@@ -39,9 +38,14 @@
<style>
  .inactive {
    padding: 0 0.75rem;
-
    font-size: var(--font-size-small);
-
    color: var(--color-fill-gray);
-
    font-family: var(--font-family-sans-serif);
+
    font: var(--txt-body-m-regular);
+
    color: var(--color-text-quaternary);
+
  }
+
  .hoverable {
+
    background-color: var(--color-surface-base);
+
  }
+
  .hoverable:hover {
+
    background-color: var(--color-surface-canvas);
  }
</style>

@@ -71,12 +75,19 @@
      }
    }} />
{:else}
-
  <Border
-
    hoverable
-
    styleCursor="text"
-
    variant="ghost"
-
    styleHeight="2.5rem"
-
    styleWidth="100%"
+
  <!-- svelte-ignore a11y_click_events_have_key_events -->
+
  <div
+
    class="hoverable"
+
    style:border="1px solid var(--color-border-subtle)"
+
    style:border-radius="var(--border-radius-sm)"
+
    style:display="flex"
+
    style:gap="0.5rem"
+
    style:align-items="center"
+
    style:cursor="text"
+
    style:height="2rem"
+
    style:width="100%"
+
    role="button"
+
    tabindex="0"
    onclick={e => {
      e.preventDefault();
      e.stopPropagation();
@@ -89,5 +100,5 @@
    <div style:width="100%" class="inactive">
      {placeholder}
    </div>
-
  </Border>
+
  </div>
{/if}
modified src/components/CommitsContainer.svelte
@@ -1,9 +1,8 @@
<script lang="ts">
  import type { Snippet } from "svelte";

-
  import Border from "@app/components/Border.svelte";
+
  import Button from "@app/components/Button.svelte";
  import Icon from "@app/components/Icon.svelte";
-
  import NakedButton from "@app/components/NakedButton.svelte";

  interface Props {
    leftHeader: Snippet;
@@ -19,10 +18,9 @@
  .header {
    display: flex;
    align-items: center;
-
    height: 2rem;
+
    height: 2.5rem;
    padding-left: 0.25rem;
-
    font-size: var(--font-size-small);
-
    background-color: var(--color-background-default);
+
    font: var(--txt-body-m-regular);
  }

  .left {
@@ -30,39 +28,33 @@
    gap: 0.5rem;
    align-items: center;
  }
-
  .divider {
-
    width: calc(100% + 4px);
-
    position: relative;
-
    top: -6px;
-
    left: -2px;
-
    z-index: 1;
-
    height: 2px;
-
    background-color: var(--color-fill-ghost);
-
  }
</style>

-
<Border
-
  variant="ghost"
-
  styleFlexDirection="column"
-
  styleAlignItems="flex-start">
+
<div
+
  style:border="1px solid var(--color-border-subtle)"
+
  style:border-radius="var(--border-radius-md)"
+
  style:display="flex"
+
  style:align-items="flex-start"
+
  style:background-color="var(--color-surface-canvas)"
+
  style:flex-direction="column">
  <div class="header" class:collapsed={!expanded}>
    <div class="left">
-
      <NakedButton
-
        stylePadding="0 0.25rem"
-
        variant="ghost"
+
      <Button
+
        variant="naked"
        onclick={async () => {
          expanded = !expanded;
        }}>
        <Icon name={expanded ? "chevron-down" : "chevron-right"} />
-
      </NakedButton>
+
      </Button>
      {@render leftHeader()}
    </div>
  </div>

  {#if expanded}
-
    <div class="divider"></div>
-
    <div style:width="100%">
+
    <div
+
      style:width="100%"
+
      style:border-top="1px solid var(--color-border-subtle)">
      {@render children()}
    </div>
  {/if}
-
</Border>
+
</div>
modified src/components/CompactCommitAuthorship.svelte
@@ -17,7 +17,7 @@
<style>
  .authorship {
    display: flex;
-
    font-size: var(--font-size-small);
+
    font: var(--txt-body-m-regular);
    column-gap: 0.5rem;
    align-items: center;
    white-space: nowrap;
@@ -28,25 +28,19 @@
    flex-wrap: nowrap;
    white-space: nowrap;
    gap: 0.5rem;
-
    font-family: var(--font-family-monospace);
-
    font-weight: var(--font-weight-semibold);
+
    font: var(--txt-code-regular);
  }
  .label {
-
    font-family: var(--font-family-sans-serif);
-
    font-weight: var(--font-weight-regular);
-
    color: var(--color-foreground-dim);
+
    color: var(--color-text-secondary);
  }
  .avatar {
    width: 1rem;
    height: 1rem;
-
    clip-path: var(--1px-corner-fill);
  }
</style>

<div class="authorship">
-
  <HoverPopover
-
    stylePopoverPositionLeft="-8rem"
-
    stylePopoverPositionBottom="1.5rem">
+
  <HoverPopover placement="top-start">
    {#snippet toggle()}
      <div style="display: flex;">
        {#if commit.author.email === commit.committer.email}
modified src/components/ConfirmClear.svelte
@@ -1,8 +1,6 @@
<script lang="ts">
  import Button from "@app/components/Button.svelte";
  import Icon from "@app/components/Icon.svelte";
-
  import NakedButton from "@app/components/NakedButton.svelte";
-
  import OutlineButton from "@app/components/OutlineButton.svelte";

  interface Props {
    clear: () => void;
@@ -15,22 +13,22 @@
</script>

{#if closed}
-
  <NakedButton
+
  <Button
+
    variant="naked"
    stylePadding="0 0.25rem"
-
    variant="ghost"
    onclick={() => (closed = false)}>
-
    <Icon name="broom-double" />
-
  </NakedButton>
+
    <Icon name="clear-all" />
+
  </Button>
{:else}
-
  <div class="global-flex txt-small">
+
  <div class="global-flex txt-body-m-regular">
    <div class="global-flex" style:justify-content="space-between">
      <Button variant="ghost" onclick={clear}>
-
        <Icon name="broom-double" />
+
        <Icon name="clear-all" />
        Clear all {count}
      </Button>
-
      <OutlineButton variant="ghost" onclick={() => (closed = true)}>
-
        <Icon name="cross" />Cancel
-
      </OutlineButton>
+
      <Button variant="outline" onclick={() => (closed = true)}>
+
        <Icon name="close" />Cancel
+
      </Button>
    </div>
  </div>
{/if}
modified src/components/CopyableId.svelte
@@ -7,7 +7,13 @@
    inline = false,
    children,
    id,
-
  }: { inline?: boolean; children?: Snippet; id: string } = $props();
+
    styleFont = undefined,
+
  }: {
+
    inline?: boolean;
+
    children?: Snippet;
+
    id: string;
+
    styleFont?: string;
+
  } = $props();

  let clipboard: Clipboard;
</script>
@@ -15,12 +21,12 @@
<style>
  .copyable-id {
    cursor: pointer;
-
    color: var(--color-foreground-dim);
+
    color: var(--color-text-secondary);
    gap: 0.25rem;
  }

  .copyable-id:hover {
-
    color: var(--color-foreground-contrast);
+
    color: var(--color-text-primary);
  }
  .inline {
    display: inline-flex;
@@ -35,7 +41,8 @@
  tabindex="0"
  onclick={() => clipboard.copy()}
  class:inline
-
  class="copyable-id global-flex txt-small txt-monospace">
+
  class="copyable-id global-flex txt-code-regular"
+
  style:font={styleFont}>
  {#if children}
    {@render children()}
  {:else}
modified src/components/Diff.svelte
@@ -229,8 +229,7 @@
    /* Make space for the box-shadow border, otherwise it gets cut off due to
       overflow: hide on the container. */
    padding: 0.5rem 0.0625rem;
-
    font-size: var(--font-size-small);
-
    font-family: var(--font-family-monospace);
+
    font: var(--txt-code-regular);
  }
  .line {
    display: flex;
@@ -238,38 +237,38 @@
    white-space: pre-wrap;
  }
  .hunk-header {
-
    color: var(--color-foreground-dim);
+
    color: var(--color-text-secondary);
  }
  .hunk-header > .left,
  .hunk-header > .right {
    cursor: default;
  }
  .addition {
-
    background-color: var(--color-fill-diff-green);
+
    background-color: var(--color-feedback-success-bg);
  }
  .deletion {
-
    background-color: var(--color-fill-diff-red);
+
    background-color: var(--color-feedback-error-bg);
  }
  .addition > .left,
  .addition > .right,
  .addition > .sign {
-
    color: var(--color-foreground-success);
+
    color: var(--color-feedback-success-text);
  }
  .deletion > .left,
  .deletion > .right,
  .deletion > .sign {
-
    color: var(--color-foreground-red);
+
    color: var(--color-feedback-error-text);
  }
  .context > .left,
  .context > .right,
  .context > .sign {
-
    color: var(--color-foreground-disabled);
+
    color: var(--color-text-disabled);
  }
  .marker {
-
    color: var(--color-foreground-contrast) !important;
+
    color: var(--color-text-primary) !important;
  }
  .selected {
-
    box-shadow: 0 0 0 1px var(--color-fill-secondary);
+
    box-shadow: 0 0 0 1px var(--color-border-brand);
    z-index: 1;
  }
  .left,
@@ -286,7 +285,7 @@
  .right:hover:not(.selection-disabled),
  .left:active:not(.selection-disabled),
  .right:active:not(.selection-disabled) {
-
    color: var(--color-foreground-contrast);
+
    color: var(--color-text-primary);
  }
  .sign {
    min-width: 1.5rem;
@@ -305,22 +304,21 @@
    align-self: flex-start;
  }
  .thread {
-
    background-color: var(--color-background-default);
-
    box-shadow: inset 0 0 0 2px var(--color-border-hint);
+
    background-color: var(--color-surface-base);
+
    font: var(--txt-body-m-regular);
    padding: 0.5rem;
  }
  .comment-form {
-
    background-color: var(--color-background-default);
-
    box-shadow: inset 0 0 0 2px var(--color-border-default);
-
    font-family: var(--font-family-sans-serif);
+
    background-color: var(--color-surface-base);
    display: flex;
    flex-direction: column;
+
    font: var(--txt-body-m-regular);
    padding: 1rem;
  }
  .comment-header {
    display: flex;
-
    background-color: var(--color-fill-ghost);
-
    clip-path: var(--1px-corner-fill);
+
    background-color: var(--color-surface-subtle);
+
    border-radius: var(--border-radius-sm);
    padding: 0 0.5rem;
    width: fit-content;
  }
@@ -411,7 +409,7 @@
            {#if thread.root.resolved}
              <div title="Unresolve comment thread">
                <Icon
-
                  name="unresolve"
+
                  name="close"
                  onclick={partial(
                    codeComments.changeCommentStatus,
                    thread.root.id,
modified src/components/DiffStatBadge.svelte
@@ -11,27 +11,28 @@
<style>
  .diff-stat-badge {
    display: flex;
-
    clip-path: var(--1px-corner-fill);
+
    border: 1px solid var(--color-border-subtle);
+
    border-radius: var(--border-radius-sm);
    height: 1.5rem;
+
    overflow: hidden;
  }

  .insertions {
    display: flex;
    padding: 0 0.5rem;
    align-items: center;
-
    color: var(--color-foreground-success);
-
    background-color: var(--color-fill-diff-green-light);
+
    color: var(--color-feedback-success-text);
  }
  .deletions {
    display: flex;
    padding: 0 0.5rem;
    align-items: center;
-
    color: var(--color-foreground-red);
-
    background-color: var(--color-fill-diff-red-light);
+
    color: var(--color-feedback-error-text);
+
    border-left: 1px solid var(--color-border-subtle);
  }
</style>

-
<div class="diff-stat-badge txt-mono txt-semibold txt-small">
+
<div class="diff-stat-badge txt-code-regular">
  <div class="insertions">+{stats.insertions}</div>
  <div class="deletions">-{stats.deletions}</div>
</div>
modified src/components/Discussion.svelte
@@ -11,9 +11,9 @@
  import * as roles from "@app/lib/roles";
  import { scrollIntoView } from "@app/lib/utils";

+
  import Button from "@app/components/Button.svelte";
  import CommentToggleInput from "@app/components/CommentToggleInput.svelte";
  import Icon from "@app/components/Icon.svelte";
-
  import NakedButton from "@app/components/NakedButton.svelte";
  import ThreadComponent from "@app/components/Thread.svelte";

  interface Props {
@@ -84,29 +84,28 @@

<style>
  .connector {
-
    width: 2px;
+
    width: 1px;
    height: 1rem;
    margin-left: 1.25rem;
-
    background-color: var(--color-border-hint);
+
    background-color: var(--color-border-subtle);
  }
</style>

<div style:margin={hideDiscussion ? "1.5rem 0" : "1.5rem 0 2.5rem 0"}>
  <div class="global-flex">
    <div class="global-flex">
-
      <NakedButton
-
        variant="ghost"
-
        stylePadding="0 4px"
+
      <Button
+
        variant="naked"
        disabled={commentThreads.length === 0}
        onclick={() => (hideDiscussion = !hideDiscussion)}>
        <Icon name={hideDiscussion ? "chevron-right" : "chevron-down"} />
-
      </NakedButton>
+
      </Button>
      <div
-
        class="txt-semibold global-flex txt-regular"
+
        class="txt-body-m-regular global-flex"
        style:color={commentThreads.length === 0
-
          ? "var(--color-foreground-disabled)"
+
          ? "var(--color-text-disabled)"
          : undefined}>
-
        Discussion <span style:font-weight="var(--font-weight-regular)">
+
        Discussion <span>
          {sum(
            commentThreads.map(t => {
              return t.replies.length + 1;
@@ -116,8 +115,8 @@
      </div>
    </div>
    <div style:margin-left="auto">
-
      <NakedButton
-
        variant="ghost"
+
      <Button
+
        variant="naked"
        active={topLevelReplyOpen}
        onclick={async () => {
          if (hideDiscussion) {
@@ -130,8 +129,8 @@
          await toggleReply();
        }}>
        <Icon name="comment" />
-
        <span class="txt-small">Comment</span>
-
      </NakedButton>
+
        <span class="txt-body-m-regular">Comment</span>
+
      </Button>
    </div>
  </div>
  <div
modified src/components/DropdownList.svelte
@@ -38,8 +38,8 @@
    overflow-y: auto;
  }
  .dropdown-item {
-
    padding: 2px;
-
    font-size: var(--font-size-small);
+
    padding: 1px;
+
    font: var(--txt-body-m-regular);
  }
</style>

modified src/components/DropdownListItem.svelte
@@ -33,32 +33,30 @@
    min-height: 2rem;
    padding: 0 0.75rem;
    white-space: nowrap;
-
    font-size: var(--font-size-small);
-
    font-weight: var(--font-weight-regular);
-
    color: var(--color-foreground-contrast);
-
    clip-path: var(--1px-corner-fill);
+
    font: var(--txt-body-m-regular);
+
    color: var(--color-text-primary);
+
    border-radius: var(--border-radius-sm);
  }
  .item.disabled {
-
    color: var(--color-foreground-disabled);
+
    color: var(--color-text-disabled);
  }
  .item:hover,
  .selected {
-
    background-color: var(--color-fill-ghost);
+
    background-color: var(--color-surface-subtle);
  }
  .selected {
-
    font-weight: var(--font-weight-semibold);
-
    color: var(--color-foreground-contrast);
-
    background-color: var(--color-fill-ghost);
+
    color: var(--color-text-primary);
+
    background-color: var(--color-surface-subtle);
  }
  .item:hover.selected {
-
    background-color: var(--color-fill-ghost-hover);
+
    background-color: var(--color-surface-mid);
  }
  .item:hover.selected.disabled {
-
    background-color: var(--color-fill-ghost);
+
    background-color: var(--color-surface-subtle);
  }
  .item:hover.disabled {
    cursor: not-allowed;
-
    background-color: var(--color-background-float);
+
    background-color: var(--color-surface-canvas);
  }
</style>

modified src/components/EditableTitle.svelte
@@ -1,7 +1,7 @@
<script lang="ts">
+
  import Button from "@app/components/Button.svelte";
  import Icon from "@app/components/Icon.svelte";
  import InlineTitle from "@app/components/InlineTitle.svelte";
-
  import NakedButton from "@app/components/NakedButton.svelte";
  import TextInput from "@app/components/TextInput.svelte";

  interface Props {
@@ -40,14 +40,13 @@

<style>
  .title {
-
    font-size: var(--font-size-medium);
-
    font-weight: var(--font-weight-medium);
+
    font: var(--txt-heading-m);
    -webkit-user-select: text;
    user-select: text;
    display: flex;
    align-items: center;
    word-break: break-word;
-
    min-height: 2.5rem;
+
    min-height: 2rem;
    width: 100%;
  }
  .edit-title-icon {
@@ -70,22 +69,20 @@
        editingTitle = false;
      }} />
    <div class="global-flex" style:margin-left="0.5rem">
-
      <NakedButton
-
        variant="ghost"
-
        styleHeight="2.5rem"
+
      <Button
+
        variant="naked"
        disabled={!(newTitle.trim().length > 0)}
        onclick={save}>
        <Icon name="checkmark" />
-
      </NakedButton>
-
      <NakedButton
-
        variant="ghost"
-
        styleHeight="2.5rem"
+
      </Button>
+
      <Button
+
        variant="naked"
        onclick={() => {
          newTitle = title;
          editingTitle = false;
        }}>
-
        <Icon name="cross" />
-
      </NakedButton>
+
        <Icon name="close" />
+
      </Button>
    </div>
  {:else}
    <div class="global-flex" style:gap="0">
@@ -102,7 +99,7 @@
        tabindex="0">
        <InlineTitle content={title} fontSize="medium" />
        {#if allowedToEdit}
-
          <div class="edit-title-icon"><Icon name="pen" /></div>
+
          <div class="edit-title-icon"><Icon name="edit" /></div>
        {/if}
      </div>
    </div>
modified src/components/ExtendedTextarea.svelte
@@ -1,7 +1,7 @@
<script lang="ts">
  import type { Embed } from "@bindings/cob/thread/Embed";
  import type { UnlistenFn } from "@tauri-apps/api/event";
-
  import type { ComponentProps } from "svelte";
+
  import type { ComponentProps, Snippet } from "svelte";

  import { listen } from "@tauri-apps/api/event";
  import { open } from "@tauri-apps/plugin-dialog";
@@ -14,7 +14,6 @@
  import Button from "@app/components/Button.svelte";
  import Icon from "@app/components/Icon.svelte";
  import Markdown from "@app/components/Markdown.svelte";
-
  import OutlineButton from "@app/components/OutlineButton.svelte";
  import Textarea from "@app/components/Textarea.svelte";

  interface Props {
@@ -45,6 +44,8 @@
    // visible but disabled and uses the string as the title to indicate the
    // reason for disabling. Defaults to `false`
    disableAttachments?: boolean | string;
+
    hideDiscard?: boolean;
+
    belowTextarea?: Snippet;
  }

  /* eslint-disable prefer-const */
@@ -69,6 +70,8 @@
    submit,
    close,
    disableAttachments: attachDisabled = false,
+
    hideDiscard = false,
+
    belowTextarea,
  }: Props = $props();
  /* eslint-enable prefer-const */

@@ -255,7 +258,6 @@
    gap: 1rem;
    width: 100%;
    flex: 1;
-
    font-family: var(--font-family-sans-serif);
  }
  .inline {
    border: 0;
@@ -276,9 +278,8 @@

  .preview {
    width: 100%;
-
    font-size: var(--font-size-small);
+
    font: var(--txt-body-m-regular);
    min-height: 109px;
-
    padding: 0.75rem;
    margin-left: 1px;
    margin-top: 1px;
    flex: 1;
@@ -287,7 +288,7 @@

<div class="comment-section" aria-label="extended-textarea" class:inline>
  {#if preview}
-
    <div class="preview">
+
    <div class="preview" style:min-height={styleMinHeight}>
      {#if body.trim().length === 0}
        <span class="txt-missing">Nothing to preview.</span>
      {:else}
@@ -310,24 +311,27 @@
      bind:value={body}
      {placeholder} />
  {/if}
+
  {@render belowTextarea?.()}
  <div class="actions">
-
    <OutlineButton
-
      disabled={submitInProgress}
-
      variant="ghost"
-
      onclick={() => {
-
        preview = false;
-
        close();
-
      }}>
-
      <Icon name="cross" />
-
      <span class="global-hide-on-small-desktop-down">Discard</span>
-
    </OutlineButton>
+
    {#if !hideDiscard}
+
      <Button
+
        variant="outline"
+
        disabled={submitInProgress}
+
        onclick={() => {
+
          preview = false;
+
          close();
+
        }}>
+
        <Icon name="close" />
+
        <span class="global-hide-on-small-desktop-down">Discard</span>
+
      </Button>
+
    {/if}
    {#if !preview}
      <div
        style:display=""
-
        class="txt-overflow txt-small txt-missing"
+
        class="txt-overflow txt-body-m-regular txt-missing"
        title={`${attachEnabled ? "Drag and drop files to add them. " : ""}Markdown is supported. Press ${utils.modifierKey()}↵ to submit.`}>
        {#if embedUploadError}
-
          <span style:color="var(--color-fill-danger)">
+
          <span style:color="var(--color-feedback-error-text)">
            <Icon
              styleDisplay="inline"
              styleVerticalAlign="text-top"
@@ -346,19 +350,19 @@
    {/if}
    <div class="buttons">
      {#if attachEnabled || attachDisabledReason}
-
        <OutlineButton
-
          variant="ghost"
+
        <Button
+
          variant="outline"
          onclick={selectFiles}
          disabled={preview || attachDisabledReason !== undefined}
          title={attachDisabledReason}>
-
          <Icon name="attachment" />
+
          <Icon name="attach" />
          Attach
-
        </OutlineButton>
+
        </Button>
      {/if}
-
      <OutlineButton variant="ghost" onclick={() => (preview = !preview)}>
-
        <Icon name={preview ? "pen" : "eye"} />
+
      <Button variant="outline" onclick={() => (preview = !preview)}>
+
        <Icon name={preview ? "edit" : "eye"} />
        {preview ? "Edit" : "Preview"}
-
      </OutlineButton>
+
      </Button>
      <Button
        variant="ghost"
        title={emptyBodyTooltip}
modified src/components/ExternalLink.svelte
@@ -3,12 +3,15 @@

  import Icon from "@app/components/Icon.svelte";

-
  const { href, children }: { href: string; children?: Snippet } = $props();
+
  const {
+
    href,
+
    title,
+
    children,
+
  }: { href: string; title?: string; children?: Snippet } = $props();
</script>

<style>
  a {
-
    font-weight: var(--font-weight-semibold);
    color: inherit;
    display: inline-flex;
    align-items: center;
@@ -18,25 +21,25 @@
  a:hover {
    text-decoration: underline;
    text-underline-offset: 2px;
-
    color: var(--color-fill-secondary);
+
    color: var(--color-text-brand);
  }

  .icon {
-
    color: var(--color-foreground-dim);
+
    color: var(--color-text-secondary);
    position: relative;
    bottom: 1px;
  }

  a:hover .icon {
-
    color: var(--color-fill-secondary-hover);
+
    color: var(--color-text-brand);
  }
</style>

-
<a {href} target="_blank" rel="noreferrer">
+
<a {href} {title} target="_blank" rel="noreferrer">
  {#if children}
    {@render children()}
+
    <span class="icon"><Icon name="open-external" /></span>
  {:else}
-
    {href}
+
    <Icon name="open-external" />
  {/if}
-
  <span class="icon"><Icon name="open-external" /></span>
</a>
deleted src/components/File.svelte
@@ -1,121 +0,0 @@
-
<script lang="ts">
-
  import type { Snippet } from "svelte";
-

-
  import { tick } from "svelte";
-

-
  import Icon from "@app/components/Icon.svelte";
-
  import NakedButton from "@app/components/NakedButton.svelte";
-

-
  interface Props {
-
    children: Snippet;
-
    expandable?: boolean;
-
    expanded?: boolean;
-
    leftHeader?: Snippet;
-
    rightHeader?: Snippet;
-
    sticky?: boolean;
-
  }
-

-
  /* eslint-disable prefer-const */
-
  let {
-
    children,
-
    expanded = true,
-
    leftHeader,
-
    rightHeader,
-
    sticky = true,
-
    expandable = true,
-
  }: Props = $props();
-
  /* eslint-enable prefer-const */
-

-
  let header: HTMLElement | undefined = $state();
-
</script>
-

-
<style>
-
  .header {
-
    display: flex;
-
    align-items: center;
-
    height: 2.5rem;
-
    padding-left: 0.5rem;
-
    z-index: 2;
-
    font-size: var(--font-size-small);
-
    background-color: var(--color-background-default);
-
    position: relative;
-
  }
-
  .header::after {
-
    position: absolute;
-
    z-index: -1;
-
    content: " ";
-
    background-color: var(--color-fill-float-hover);
-
    clip-path: var(--2px-top-corner-fill);
-
    width: 100%;
-
    height: 100%;
-
    top: 0;
-
    left: 0;
-
  }
-
  .header.collapsed {
-
    clip-path: var(--2px-corner-fill);
-
  }
-

-
  .sticky {
-
    position: sticky;
-
    top: 0;
-
  }
-

-
  .left {
-
    display: flex;
-
    gap: 0.5rem;
-
    margin-right: 1rem;
-
    align-items: center;
-
  }
-

-
  .container {
-
    position: relative;
-
    overflow-x: auto;
-
    z-index: 1;
-
  }
-
  .container::after {
-
    position: absolute;
-
    z-index: -1;
-
    content: " ";
-
    clip-path: var(--2px-bottom-corner-fill);
-
    background-color: var(--color-background-float);
-
    width: 100%;
-
    height: 100%;
-
    top: 0;
-
    left: 0;
-
  }
-
</style>
-

-
<div class="header" class:sticky class:collapsed={!expanded} bind:this={header}>
-
  <div class="left">
-
    {#if expandable}
-
      <NakedButton
-
        stylePadding="0 4px"
-
        variant="ghost"
-
        onclick={async () => {
-
          expanded = !expanded;
-
          if (!expanded && header) {
-
            await tick();
-
            header.scrollIntoView({ behavior: "smooth", block: "nearest" });
-
          }
-
        }}>
-
        <Icon name={expanded ? "chevron-down" : "chevron-right"} />
-
      </NakedButton>
-
    {/if}
-
    {@render leftHeader?.()}
-
  </div>
-
  {#if rightHeader}
-
    <div
-
      class="global-flex"
-
      style:gap="1rem"
-
      style:margin-left="auto"
-
      style:margin-right="1rem">
-
      {@render rightHeader()}
-
    </div>
-
  {/if}
-
</div>
-

-
{#if expanded}
-
  <div class="container">
-
    {@render children()}
-
  </div>
-
{/if}
added src/components/FileBlock.svelte
@@ -0,0 +1,113 @@
+
<script lang="ts">
+
  import type { Snippet } from "svelte";
+

+
  import { tick } from "svelte";
+

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

+
  interface Props {
+
    children: Snippet;
+
    expandable?: boolean;
+
    expanded?: boolean;
+
    leftHeader?: Snippet;
+
    rightHeader?: Snippet;
+
    sticky?: boolean;
+
    border?: boolean;
+
  }
+

+
  /* eslint-disable prefer-const */
+
  let {
+
    children,
+
    expanded = true,
+
    leftHeader,
+
    rightHeader,
+
    sticky = true,
+
    expandable = true,
+
    border = true,
+
  }: Props = $props();
+
  /* eslint-enable prefer-const */
+

+
  let header: HTMLElement | undefined = $state();
+
</script>
+

+
<style>
+
  .header {
+
    display: flex;
+
    align-items: center;
+
    height: 2.5rem;
+
    padding-left: 0.25rem;
+
    z-index: 2;
+
    font: var(--txt-body-m-regular);
+
    position: relative;
+
    background-color: var(--color-surface-canvas);
+
    border-top-left-radius: var(--border-radius-md);
+
    border-top-right-radius: var(--border-radius-md);
+
  }
+

+
  .sticky {
+
    position: sticky;
+
    top: 0;
+
  }
+

+
  .left {
+
    display: flex;
+
    gap: 0.5rem;
+
    margin-right: 1rem;
+
    align-items: center;
+
  }
+

+
  .container {
+
    position: relative;
+
    overflow-x: auto;
+
    z-index: 1;
+
    border-top: none;
+
    border-bottom-left-radius: var(--border-radius-md);
+
    border-bottom-right-radius: var(--border-radius-md);
+
  }
+
</style>
+

+
<div
+
  class="header"
+
  class:sticky
+
  class:collapsed={!expanded}
+
  bind:this={header}
+
  style:border={border ? "1px solid var(--color-border-subtle)" : undefined}
+
  style:border-bottom={border
+
    ? "undefined"
+
    : "1px solid var(--color-border-subtle)"}>
+
  <div class="left">
+
    {#if expandable}
+
      <Button
+
        variant="naked"
+
        onclick={async () => {
+
          expanded = !expanded;
+
          if (!expanded && header) {
+
            await tick();
+
            header.scrollIntoView({ behavior: "smooth", block: "nearest" });
+
          }
+
        }}>
+
        <Icon name={expanded ? "chevron-down" : "chevron-right"} />
+
      </Button>
+
    {/if}
+
    {@render leftHeader?.()}
+
  </div>
+
  {#if rightHeader}
+
    <div
+
      class="global-flex"
+
      style:gap="1rem"
+
      style:margin-left="auto"
+
      style:margin-right="1rem">
+
      {@render rightHeader()}
+
    </div>
+
  {/if}
+
</div>
+

+
{#if expanded}
+
  <div
+
    class="container"
+
    style:border={border ? "1px solid var(--color-border-subtle)" : "undefined"}
+
    style:border-top="none">
+
    {@render children()}
+
  </div>
+
{/if}
modified src/components/FileDiff.svelte
@@ -3,7 +3,7 @@
  import type { FileDiff } from "@bindings/diff/FileDiff";

  import Diff from "@app/components/Diff.svelte";
-
  import File from "@app/components/File.svelte";
+
  import FileBlock from "@app/components/FileBlock.svelte";
  import Icon from "@app/components/Icon.svelte";
  import Path from "@app/components/Path.svelte";

@@ -58,32 +58,30 @@

<style>
  .added {
-
    color: var(--color-foreground-success);
-
    background-color: var(--color-fill-diff-green-light);
+
    color: var(--color-feedback-success-text);
+
    background-color: var(--color-feedback-success-bg);
  }
  .deleted {
-
    color: var(--color-foreground-red);
-
    background-color: var(--color-fill-diff-red-light);
+
    color: var(--color-feedback-error-text);
+
    background-color: var(--color-feedback-error-bg);
  }
  .moved,
  .copied {
-
    color: var(--color-foreground-dim);
-
    background: var(--color-fill-ghost);
+
    color: var(--color-text-secondary);
+
    background: var(--color-surface-subtle);
  }
  .stats {
-
    font-size: var(--font-size-tiny);
-
    font-family: var(--font-family-monospace);
-
    font-weight: var(--font-weight-semibold);
+
    font: var(--txt-code-regular);
  }
</style>

{#snippet emptyPlaceholder()}
  <div class="global-flex" style:margin="1rem 0" style:justify-content="center">
-
    <Icon name="none" />Empty file
+
    Empty file
  </div>
{/snippet}

-
<File {expanded}>
+
<FileBlock {expanded}>
  {#snippet leftHeader()}
    {#if file.status === "moved" || file.status === "copied"}
      <span style="display: flex; align-items: center; flex-wrap: wrap;">
@@ -96,23 +94,23 @@
    {/if}

    {#if file.status === "added"}
-
      <span class="global-counter added">Added</span>
+
      <span class="global-chip added">Added</span>
    {:else if file.status === "deleted"}
-
      <span class="global-counter deleted">Deleted</span>
+
      <span class="global-chip deleted">Deleted</span>
    {:else if file.status === "moved"}
-
      <span class="global-counter moved">Moved</span>
+
      <span class="global-chip moved">Moved</span>
    {:else if file.status === "copied"}
-
      <span class="global-counter copied">Copied</span>
+
      <span class="global-chip copied">Copied</span>
    {/if}
  {/snippet}

  {#snippet rightHeader()}
    {#if file.diff.type === "plain" && file.diff.hunks.length > 0}
      <div class="stats">
-
        <span style:color="var(--color-foreground-success)">
+
        <span style:color="var(--color-feedback-success-text)">
          +{file.diff.stats.additions}
        </span>
-
        <span style:color="var(--color-foreground-red)">
+
        <span style:color="var(--color-feedback-error-text)">
          -{file.diff.stats.deletions}
        </span>
      </div>
@@ -149,4 +147,4 @@
  {:else}
    {@render emptyPlaceholder()}
  {/if}
-
</File>
+
</FileBlock>
added src/components/FileTreeFile.svelte
@@ -0,0 +1,53 @@
+
<script lang="ts">
+
  import Icon from "@app/components/Icon.svelte";
+

+
  const {
+
    name,
+
    fetchBlob,
+
    active,
+
    indent = 0.5,
+
  }: {
+
    name: string;
+
    fetchBlob: () => Promise<void>;
+
    active: boolean;
+
    indent?: number;
+
  } = $props();
+
</script>
+

+
<style>
+
  .file {
+
    width: 100%;
+
    cursor: pointer;
+
    white-space: nowrap;
+
    border-radius: var(--border-radius-sm);
+
  }
+
  .file:hover,
+
  .active {
+
    background-color: var(--color-surface-subtle);
+
  }
+
  .active:hover {
+
    background-color: var(--color-surface-strong);
+
  }
+
  .file:hover .icon,
+
  .active .icon {
+
    color: var(--color-text-primary);
+
  }
+
</style>
+

+
<!-- svelte-ignore a11y_no_static_element_interactions -->
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
+
<div
+
  class="file"
+
  class:active
+
  style:padding-left="{indent}rem"
+
  style:padding-right="0.5rem"
+
  onclick={fetchBlob}>
+
  <div class="global-flex" style:padding="0.25rem 0">
+
    <div class="icon txt-missing">
+
      <Icon name="document" />
+
    </div>
+
    <div class="txt-body-m-regular txt-overflow">
+
      {name}
+
    </div>
+
  </div>
+
</div>
added src/components/FileTreeFolder.svelte
@@ -0,0 +1,84 @@
+
<script lang="ts">
+
  import type { Tree } from "@bindings/source/Tree";
+

+
  import FileTreeFile from "@app/components/FileTreeFile.svelte";
+
  import FileTreeFolder from "@app/components/FileTreeFolder.svelte";
+
  import Icon from "@app/components/Icon.svelte";
+

+
  interface Props {
+
    fetchTree: (path: string) => Promise<Tree>;
+
    fetchBlob: (path: string) => Promise<void>;
+
    currentPath: string;
+
    name: string;
+
    prefix: string;
+
    indent?: number;
+
  }
+

+
  const {
+
    name,
+
    fetchBlob,
+
    currentPath,
+
    prefix,
+
    fetchTree,
+
    indent = 0.5,
+
  }: Props = $props();
+
  let expanded = $derived(currentPath.indexOf(prefix) === 0);
+

+
  const treePromise = $derived(
+
    expanded ? fetchTree(prefix) : Promise.resolve(undefined),
+
  );
+
</script>
+

+
<style>
+
  .folder {
+
    cursor: pointer;
+
    width: 100%;
+
    border-radius: var(--border-radius-sm);
+
  }
+
  .folder:hover {
+
    background-color: var(--color-surface-subtle);
+
  }
+
</style>
+

+
<!-- svelte-ignore a11y_no_static_element_interactions -->
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
+
<div
+
  class="folder"
+
  style:padding-left="{indent}rem"
+
  style:padding-right="0.5rem"
+
  onclick={() => (expanded = !expanded)}>
+
  <div class="global-flex txt-body-m-regular" style:padding="0.25rem 0">
+
    <div class:txt-missing={!expanded}>
+
      <Icon name={expanded ? "folder-open" : "folder"} />
+
    </div>
+
    {name}
+
  </div>
+
</div>
+
{#if expanded}
+
  {#await treePromise then tree}
+
    {#if tree}
+
      <div
+
        style:display="flex"
+
        style:flex-direction="column"
+
        style:gap="0.25rem">
+
        {#each tree.entries as entry (entry.path)}
+
          {#if entry.kind === "tree"}
+
            <FileTreeFolder
+
              {fetchTree}
+
              {fetchBlob}
+
              name={entry.name}
+
              {currentPath}
+
              prefix={`${entry.path}/`}
+
              indent={indent + 1.5} />
+
          {:else if entry.kind === "blob"}
+
            <FileTreeFile
+
              name={entry.name}
+
              fetchBlob={() => fetchBlob(entry.path)}
+
              active={entry.path === currentPath}
+
              indent={indent + 1.5} />
+
          {/if}
+
        {/each}
+
      </div>
+
    {/if}
+
  {/await}
+
{/if}
modified src/components/FontSizeSwitch.svelte
@@ -14,21 +14,21 @@

<div class="global-flex" style:gap="0">
  <Button
-
    flatRight
    variant="ghost"
+
    flatRight
    onclick={() => {
      decreaseFontSize();
    }}>
    <Icon name="minus" />
  </Button>

-
  <Button flatRight flatLeft active variant="ghost">
+
  <Button variant="ghost" flatRight flatLeft active>
    {fontSettings.size}
  </Button>

  <Button
-
    flatLeft
    variant="ghost"
+
    flatLeft
    onclick={() => {
      increaseFontSize();
    }}>
modified src/components/FullWindowError.svelte
@@ -2,7 +2,6 @@
  import type { ErrorWrapper } from "@bindings/error/ErrorWrapper";
  import type { Snippet } from "svelte";

-
  import Border from "@app/components/Border.svelte";
  import Command from "@app/components/Command.svelte";
  import ExternalLink from "@app/components/ExternalLink.svelte";
  import Icon from "@app/components/Icon.svelte";
@@ -24,13 +23,12 @@
  }

  .error-icon {
-
    color: var(--color-fill-);
+
    color: var(--color-text-secondary);
    margin-bottom: 1rem;
  }

  .error-title {
-
    font-size: var(--font-size-large);
-
    font-weight: var(--font-weight-bold);
+
    font: var(--txt-heading-l);
    margin: 0 0 0.75rem 0;
  }

@@ -40,15 +38,18 @@
  }
</style>

-
<div class="error-container txt-small">
-
  <Border
-
    styleMaxWidth="45rem"
-
    variant="float"
-
    styleJustifyContent="center"
-
    styleBackgroundColor="var(--color-background-float)"
-
    styleDisplay="flex"
-
    styleFlexDirection="column"
-
    stylePadding="1.5rem">
+
<div class="error-container txt-body-m-regular">
+
  <div
+
    style:border="1px solid var(--color-border-subtle)"
+
    style:border-radius="var(--border-radius-sm)"
+
    style:display="flex"
+
    style:gap="0.5rem"
+
    style:align-items="center"
+
    style:background-color="var(--color-surface-canvas)"
+
    style:max-width="45rem"
+
    style:justify-content="center"
+
    style:flex-direction="column"
+
    style:padding="1.5rem">
    <div class="error-icon">
      <Icon size="32" name="warning" />
    </div>
@@ -72,5 +73,5 @@
    {#if error?.message}
      <Command styleWidth="30rem" showPrompt={false} command={error.message} />
    {/if}
-
  </Border>
+
  </div>
</div>
added src/components/FullscreenModalPortal.svelte
@@ -0,0 +1,43 @@
+
<script lang="ts">
+
  import { hide, modalStore } from "@app/lib/modal";
+
</script>
+

+
<style>
+
  .container {
+
    height: 100vh;
+
    width: 100vw;
+
    position: fixed;
+
    z-index: 300;
+
    justify-content: center;
+
    overflow: scroll;
+
    display: flex;
+
  }
+

+
  .overlay {
+
    background-color: var(--color-surface-scrim);
+
    height: 100%;
+
    width: 100%;
+
    position: fixed;
+
  }
+

+
  .content {
+
    z-index: 200;
+
    margin: auto;
+
  }
+
</style>
+

+
{#if $modalStore}
+
  <div class="container">
+
    <!-- svelte-ignore a11y_click_events_have_key_events -->
+
    <div
+
      role="button"
+
      tabindex="0"
+
      class="overlay"
+
      onclick={$modalStore.disableScrimClose ? undefined : hide}
+
      style:cursor={$modalStore.disableHide ? "not-allowed" : "default"}>
+
    </div>
+
    <div class="content">
+
      <svelte:component this={$modalStore.component} {...$modalStore.props} />
+
    </div>
+
  </div>
+
{/if}
added src/components/FuzzySearch.svelte
@@ -0,0 +1,62 @@
+
<script lang="ts">
+
  import type { ComponentProps } from "svelte";
+

+
  import Button from "@app/components/Button.svelte";
+
  import Icon from "@app/components/Icon.svelte";
+
  import TextInput from "@app/components/TextInput.svelte";
+

+
  interface Props {
+
    hasItems: boolean;
+
    placeholder: string;
+
    icon?: ComponentProps<typeof Icon>["name"];
+
    show: boolean;
+
    value: string;
+
    onFocus?: () => void;
+
    onSubmit?: () => void;
+
  }
+

+
  /* eslint-disable prefer-const */
+
  let {
+
    hasItems,
+
    placeholder,
+
    icon = "filter",
+
    show = $bindable(),
+
    value = $bindable(),
+
    onFocus,
+
    onSubmit,
+
  }: Props = $props();
+
  /* eslint-enable prefer-const */
+
</script>
+

+
{#if hasItems}
+
  {#if show}
+
    <TextInput
+
      autofocus
+
      {onFocus}
+
      {onSubmit}
+
      onBlur={() => {
+
        if (value === "") {
+
          show = false;
+
        }
+
      }}
+
      onDismiss={() => {
+
        value = "";
+
        show = false;
+
      }}
+
      {placeholder}
+
      keyShortcuts="ctrl+f"
+
      bind:value>
+
      {#snippet left()}
+
        <div
+
          style:color="var(--color-text-secondary)"
+
          style:padding-left="0.5rem">
+
          <Icon name={icon} />
+
        </div>
+
      {/snippet}
+
    </TextInput>
+
  {:else}
+
    <Button variant="naked" keyShortcuts="ctrl+f" onclick={() => (show = true)}>
+
      <Icon name="filter" />
+
    </Button>
+
  {/if}
+
{/if}
deleted src/components/GuideButton.svelte
@@ -1,165 +0,0 @@
-
<script lang="ts" module>
-
  export const guidePopoverToggleId = "guide-popover-toggle";
-
</script>
-

-
<script lang="ts">
-
  import type { Config } from "@bindings/config/Config";
-

-
  import { radicleInstalled } from "@app/lib/checkRadicleCLI.svelte";
-
  import { nodeRunning } from "@app/lib/events";
-
  import { activeRouteStore, push } from "@app/lib/router";
-
  import { sleep } from "@app/lib/sleep";
-
  import { didFromPublicKey, truncateDid } from "@app/lib/utils";
-

-
  import { addRepoPopoverToggleId } from "@app/components/AddRepoButton.svelte";
-
  import Border from "@app/components/Border.svelte";
-
  import Command from "@app/components/Command.svelte";
-
  import CopyableId from "@app/components/CopyableId.svelte";
-
  import Icon from "@app/components/Icon.svelte";
-
  import NakedButton from "@app/components/NakedButton.svelte";
-
  import NodeId from "@app/components/NodeId.svelte";
-
  import Popover from "@app/components/Popover.svelte";
-
  import { settingsPopoverToggleId } from "@app/components/Settings.svelte";
-

-
  interface Props {
-
    config: Config;
-
  }
-
  const { config }: Props = $props();
-

-
  let popoverExpanded: boolean = $state(false);
-
</script>
-

-
<style>
-
  .spacer {
-
    width: 100%;
-
    border-bottom: 1px solid var(--color-border-default);
-
    height: 1px;
-
    margin: 1rem 0;
-
  }
-

-
  button {
-
    text-decoration: underline;
-
    border: 0;
-
    color: var(--color-foreground-contrast);
-
    margin: 0;
-
    padding: 0;
-
    background-color: transparent;
-
    cursor: pointer;
-
  }
-
</style>
-

-
<Popover
-
  popoverPadding="0"
-
  popoverPositionTop="2.5rem"
-
  bind:expanded={popoverExpanded}
-
  popoverPositionRight="-9.3rem">
-
  {#snippet toggle(onclick)}
-
    <NakedButton
-
      id={guidePopoverToggleId}
-
      variant="ghost"
-
      {onclick}
-
      stylePadding="0 0.25rem"
-
      active={popoverExpanded}>
-
      <Icon name="info" /> Guide
-
    </NakedButton>
-
  {/snippet}
-
  {#snippet popover()}
-
    <Border
-
      variant="ghost"
-
      styleGap="0"
-
      stylePadding="1rem"
-
      styleMinWidth="32rem"
-
      styleBackgroundColor="var(--color-background-float)"
-
      styleOverflow="auto"
-
      styleMaxHeight="calc(100vh - 5rem)"
-
      styleAlignItems="flex-start"
-
      styleFlexDirection="column">
-
      <div
-
        style:position="relative"
-
        style:display="flex"
-
        style:line-height="1.625rem"
-
        style:gap="0.5rem"
-
        style:flex-direction="column"
-
        style:width="100%">
-
        <div class="txt-semibold txt-medium" style:margin-bottom="1rem">
-
          Get started
-
        </div>
-
        <div class="txt-small" style:display="inline">
-
          Hello <span style:padding-left="0.25rem">
-
            <NodeId inline publicKey={config.publicKey} alias={config.alias} />,
-
          </span>
-
          your identity has been created and stored on your machine.
-
        </div>
-
        <div class="txt-small">
-
          Your public key is <CopyableId
-
            inline
-
            id={didFromPublicKey(config.publicKey)}>
-
            {truncateDid(config.publicKey)}
-
          </CopyableId>
-
          you can share this with anyone to find you on the network.
-
        </div>
-
        <div class="txt-small" style:margin-top="1rem">
-
          We release a new version of the app every two weeks. To stay up to
-
          date, go to
-
          <button
-
            class="txt-small"
-
            onclick={async () => {
-
              const settingsButton = document.getElementById(
-
                settingsPopoverToggleId,
-
              );
-
              await sleep(1);
-
              settingsButton?.click();
-
            }}>
-
            Settings
-
          </button>
-
          and enable 'Check for updates' to receive notifications about new releases.
-
        </div>
-
        <div class="spacer"></div>
-
        {#if radicleInstalled() || $nodeRunning}
-
          <div class="global-flex txt-small">
-
            <div class="global-flex">
-
              <Icon name="thumb-up" />Radicle CLI is installed, you're good to
-
              go.
-
            </div>
-
            <button
-
              class="txt-small"
-
              onclick={async () => {
-
                if ($activeRouteStore.resource !== "home") {
-
                  await push({
-
                    resource: "home",
-
                    activeTab: "all",
-
                  });
-
                }
-
                await sleep(1);
-
                const addRepoButton = document.getElementById(
-
                  addRepoPopoverToggleId,
-
                );
-
                addRepoButton?.click();
-
              }}>
-
              Try adding a repo!
-
            </button>
-
          </div>
-
        {:else}
-
          <div class="txt-small">
-
            <div class="global-flex" style:padding-bottom="1rem">
-
              <Icon name="warning" />Radicle CLI is not installed
-
            </div>
-
            <div style:padding-bottom="1rem">
-
              To interact with repositories on the Radicle network, you’ll need
-
              to install Radicle node along with its accompanying CLI tools. The
-
              node runs in the background, enabling seamless pushing and pulling
-
              of changes, while the CLI tools let you manage the node and
-
              provide interoperability between Git and Radicle.
-
            </div>
-
            <div style:padding-bottom="0.5rem">
-
              To install Radicle node and CLI tooling, run this in your shell:
-
            </div>
-
            <Command
-
              styleWidth="fit-content"
-
              command="curl -sSf https://radicle.xyz/install | sh" />
-
          </div>
-
        {/if}
-
      </div>
-
    </Border>
-
  {/snippet}
-
</Popover>
deleted src/components/Header.svelte
@@ -1,120 +0,0 @@
-
<script lang="ts">
-
  import type { Config } from "@bindings/config/Config";
-
  import type { Snippet } from "svelte";
-

-
  import { onMount } from "svelte";
-
  import { boolean } from "zod";
-

-
  import { checkRadicleCLI } from "@app/lib/checkRadicleCLI.svelte";
-
  import { dynamicInterval } from "@app/lib/interval";
-
  import { sleep } from "@app/lib/sleep";
-
  import useLocalStorage from "@app/lib/useLocalStorage.svelte";
-

-
  import GuideButton, {
-
    guidePopoverToggleId,
-
  } from "@app/components/GuideButton.svelte";
-
  import Icon from "@app/components/Icon.svelte";
-
  import InboxButton from "@app/components/InboxButton.svelte";
-
  import NakedButton from "@app/components/NakedButton.svelte";
-
  import NodeStatusButton from "@app/components/NodeStatusButton.svelte";
-

-
  const firstLaunchStorage = useLocalStorage(
-
    "appFirstLaunch",
-
    boolean(),
-
    true,
-
    !window.localStorage,
-
  );
-

-
  interface Props {
-
    breadcrumbs?: Snippet;
-
    config: Config;
-
    notificationCount: number;
-
  }
-

-
  const { breadcrumbs, config, notificationCount }: Props = $props();
-

-
  onMount(async () => {
-
    try {
-
      await checkRadicleCLI();
-
      dynamicInterval("checkRadicleCLI", checkRadicleCLI, 30_000);
-
    } catch {
-
      dynamicInterval("checkRadicleCLI", checkRadicleCLI, 1_000);
-
    }
-

-
    if (firstLaunchStorage.value === true) {
-
      const guidePopoverButton = document.getElementById(guidePopoverToggleId);
-
      await sleep(1);
-
      guidePopoverButton?.click();
-
      firstLaunchStorage.value = false;
-
    }
-
  });
-
</script>
-

-
<style>
-
  .header {
-
    height: 3rem;
-
    padding: 0.5rem 1rem;
-
    display: flex;
-
    align-items: flex-start;
-
  }
-
  .header:after {
-
    content: " ";
-
    position: absolute;
-
    top: 0;
-
    left: 0.5rem;
-
    right: 0.5rem;
-
    height: 3rem;
-
    z-index: -1;
-
    background-color: var(--color-background-float);
-
    clip-path: var(--3px-bottom-corner-fill);
-
  }
-
  .wrapper {
-
    display: flex;
-
    flex-direction: column;
-
    width: 100%;
-
    row-gap: 8px;
-
    z-index: 50;
-
  }
-
  .top-row {
-
    display: flex;
-
    width: 100%;
-
    justify-content: space-between;
-
  }
-
</style>
-

-
<div class="header global-flex">
-
  <div class="wrapper">
-
    <div class="top-row">
-
      <div class="global-flex" style:gap="0.25rem">
-
        <NakedButton
-
          variant="ghost"
-
          onclick={() => {
-
            window.history.back();
-
          }}
-
          stylePadding="0 4px">
-
          <Icon name="arrow-left" />
-
        </NakedButton>
-
        <NakedButton
-
          variant="ghost"
-
          onclick={() => {
-
            window.history.forward();
-
          }}
-
          stylePadding="0 4px">
-
          <Icon name="arrow-right" />
-
        </NakedButton>
-
        <div
-
          class="global-flex txt-small txt-semibold"
-
          style:gap="0.25rem"
-
          style:margin-left="0.5rem">
-
          {@render breadcrumbs?.()}
-
        </div>
-
      </div>
-

-
      <div class="global-flex">
-
        <GuideButton {config} />
-
        <NodeStatusButton />
-
        <InboxButton {notificationCount} />
-
      </div>
-
    </div>
-
  </div>
-
</div>
deleted src/components/HomeSidebar.svelte
@@ -1,117 +0,0 @@
-
<script lang="ts">
-
  import type { HomeReposTab } from "@app/views/home/router";
-
  import type { RepoCount } from "@bindings/repo/RepoCount";
-

-
  import * as router from "@app/lib/router";
-

-
  import Border from "@app/components/Border.svelte";
-
  import Icon from "@app/components/Icon.svelte";
-
  import Settings from "@app/components/Settings.svelte";
-

-
  interface Props {
-
    activeTab: HomeReposTab;
-
    repoCount: RepoCount;
-
  }
-

-
  const { activeTab, repoCount }: Props = $props();
-
</script>
-

-
<style>
-
  .container {
-
    display: flex;
-
    flex-direction: column;
-
    height: 100%;
-
    justify-content: space-between;
-
  }
-
  .tab {
-
    align-items: center;
-
    gap: 0.5rem;
-
    background-color: var(--color-background-float);
-
    display: flex;
-
    font-size: var(--font-size-small);
-
    justify-content: space-between;
-
    padding: 0.5rem 0.25rem 0.5rem 0.5rem;
-
    width: 100%;
-
  }
-
  .tab:not(.active) {
-
    color: var(--color-foreground-dim);
-
  }
-
  .tab:not(.active):hover {
-
    background-color: var(--color-fill-float-hover);
-
  }
-
  .active {
-
    background-color: var(--color-background-default);
-
    font-weight: var(--font-weight-semibold);
-
  }
-
</style>
-

-
<div class="container">
-
  <div>
-
    <Border
-
      styleCursor="pointer"
-
      variant="ghost"
-
      styleFlexDirection="column"
-
      styleGap="2px"
-
      styleBackgroundColor="var(--color-background-float)">
-
      <a
-
        tabindex="0"
-
        class="tab txt-small"
-
        class:active={activeTab === "all"}
-
        href={router.routeToPath({
-
          resource: "home",
-
          activeTab: "all",
-
        })}>
-
        <div class="global-flex"><Icon name="repo" />Repositories</div>
-
        <div class="global-counter">
-
          {repoCount.total}
-
        </div>
-
      </a>
-
      <a
-
        class="tab"
-
        class:active={activeTab === "delegate"}
-
        href={router.routeToPath({
-
          resource: "home",
-
          activeTab: "delegate",
-
        })}>
-
        <div class="global-flex">
-
          <Icon name="delegate" />
-
          <div>Delegate</div>
-
        </div>
-
        <div class="global-counter">{repoCount.delegate}</div>
-
      </a>
-
      <a
-
        class="tab"
-
        class:active={activeTab === "contributor"}
-
        href={router.routeToPath({
-
          resource: "home",
-
          activeTab: "contributor",
-
        })}>
-
        <div class="global-flex">
-
          <Icon name="user" />
-
          <div>Contributor</div>
-
        </div>
-
        <div class="global-counter">{repoCount.contributor}</div>
-
      </a>
-
      <a
-
        class="tab"
-
        class:active={activeTab === "private"}
-
        href={router.routeToPath({
-
          resource: "home",
-
          activeTab: "private",
-
        })}>
-
        <div class="global-flex">
-
          <Icon name="lock" />
-
          <div>Private</div>
-
        </div>
-
        <div class="global-counter">{repoCount.private}</div>
-
      </a>
-
    </Border>
-
  </div>
-

-
  <Settings
-
    compact={false}
-
    popoverProps={{
-
      popoverPositionBottom: "3rem",
-
      popoverPositionLeft: "0",
-
    }} />
-
</div>
modified src/components/HoverPopover.svelte
@@ -1,31 +1,63 @@
<script lang="ts">
+
  import type { Placement } from "@floating-ui/dom";
  import type { Snippet } from "svelte";

+
  import {
+
    autoUpdate,
+
    computePosition,
+
    flip,
+
    offset as floatingOffset,
+
    shift,
+
  } from "@floating-ui/dom";
  import debounce from "lodash/debounce";

+
  import { portal } from "@app/lib/portal";
+

  interface Props {
    popover: Snippet;
-
    stylePopoverPositionBottom?: string | undefined;
-
    stylePopoverPositionLeft?: string | undefined;
-
    stylePopoverPositionRight?: string | undefined;
-
    stylePadding?: string | undefined;
+
    placement?: Placement;
+
    offset?: number;
+
    stylePadding?: string;
    toggle: Snippet;
  }

  const {
    popover,
-
    stylePopoverPositionBottom,
-
    stylePopoverPositionLeft,
-
    stylePopoverPositionRight,
+
    placement = "top",
+
    offset: offsetPx = 4,
    stylePadding = "0.5rem 1rem",
    toggle,
  }: Props = $props();

  let visible: boolean = $state(false);
+
  let anchorEl: HTMLDivElement | undefined = $state();
+
  let floatingEl: HTMLDivElement | undefined = $state();
+

+
  const show = debounce(() => {
+
    visible = true;
+
  }, 100);
+

+
  const hide = debounce(() => {
+
    visible = false;
+
  }, 200);

-
  const setVisible = debounce((value: boolean) => {
-
    visible = value;
-
  }, 50);
+
  $effect(() => {
+
    if (floatingEl && anchorEl) {
+
      const cleanup = autoUpdate(anchorEl, floatingEl, () => {
+
        void computePosition(anchorEl!, floatingEl!, {
+
          placement,
+
          middleware: [floatingOffset(offsetPx), flip(), shift({ padding: 8 })],
+
        }).then(({ x, y }) => {
+
          if (floatingEl) {
+
            floatingEl.style.left = `${x}px`;
+
            floatingEl.style.top = `${y}px`;
+
            floatingEl.style.visibility = "visible";
+
          }
+
        });
+
      });
+
      return cleanup;
+
    }
+
  });
</script>

<style>
@@ -34,37 +66,56 @@
    display: inline-block;
  }
  .popover {
-
    background: var(--color-fill-ghost);
+
    background: var(--color-surface-subtle);
+
    border: 1px solid var(--color-border-subtle);
    box-shadow: var(--elevation-low);
-
    position: absolute;
-
    clip-path: var(--2px-corner-fill);
+
    position: fixed;
+
    top: 0;
+
    left: 0;
+
    visibility: hidden;
+
    border-radius: var(--border-radius-md);
    z-index: 10;
  }
</style>

-
<div class="container">
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
+
<div
+
  class="container"
+
  bind:this={anchorEl}
+
  onmouseenter={() => {
+
    hide.cancel();
+
    show();
+
  }}
+
  onmouseleave={() => {
+
    show.cancel();
+
    hide();
+
  }}>
  <!-- svelte-ignore a11y_click_events_have_key_events -->
  <div
    role="button"
    tabindex="0"
    onclick={e => {
      e.stopPropagation();
-
    }}
-
    onmouseenter={() => setVisible(true)}
-
    onmouseleave={() => setVisible(false)}>
+
    }}>
    {@render toggle()}
-

-
    {#if visible}
-
      <div style:position="absolute">
-
        <div
-
          class="popover"
-
          style:padding={stylePadding}
-
          style:left={stylePopoverPositionLeft}
-
          style:right={stylePopoverPositionRight}
-
          style:bottom={stylePopoverPositionBottom}>
-
          {@render popover()}
-
        </div>
-
      </div>
-
    {/if}
  </div>
+

+
  {#if visible}
+
    <!-- svelte-ignore a11y_no_static_element_interactions -->
+
    <div
+
      use:portal
+
      bind:this={floatingEl}
+
      class="popover"
+
      style:padding={stylePadding}
+
      onmouseenter={() => {
+
        hide.cancel();
+
        show();
+
      }}
+
      onmouseleave={() => {
+
        show.cancel();
+
        hide();
+
      }}>
+
      {@render popover()}
+
    </div>
+
  {/if}
</div>
modified src/components/Icon.svelte
@@ -1,100 +1,128 @@
<script lang="ts">
  import { unreachable } from "@app/lib/utils";

+
  type IconName =
+
    | "activity"
+
    | "add-emoji"
+
    | "archive"
+
    | "arrow-down"
+
    | "arrow-left"
+
    | "arrow-right"
+
    | "arrow-up"
+
    | "attach"
+
    | "avatar-incognito"
+
    | "badge"
+
    | "bell"
+
    | "binary"
+
    | "bookmark"
+
    | "bookmark-filled"
+
    | "branch"
+
    | "checkmark"
+
    | "checkout"
+
    | "chevron-down"
+
    | "chevron-left"
+
    | "chevron-left-right"
+
    | "chevron-right"
+
    | "chevron-up"
+
    | "chevron-up-down"
+
    | "clear-all"
+
    | "clipboard"
+
    | "clock"
+
    | "close"
+
    | "code"
+
    | "collapse-in"
+
    | "collapse-vertical"
+
    | "comment"
+
    | "comment-checkmark"
+
    | "comment-cross"
+
    | "commit"
+
    | "copy"
+
    | "cursor"
+
    | "dashboard"
+
    | "device"
+
    | "diff"
+
    | "disconnect"
+
    | "document"
+
    | "download"
+
    | "edit"
+
    | "ellipsis"
+
    | "ellipsis-vertical"
+
    | "emoji"
+
    | "expand-out"
+
    | "expand-vertical"
+
    | "explore"
+
    | "eye"
+
    | "eye-slash"
+
    | "filter"
+
    | "folder"
+
    | "folder-open"
+
    | "fullscreen"
+
    | "git"
+
    | "guide"
+
    | "help"
+
    | "home"
+
    | "hourglass"
+
    | "inbox"
+
    | "issue"
+
    | "issue-closed"
+
    | "key"
+
    | "label"
+
    | "lightbulb"
+
    | "link"
+
    | "lock"
+
    | "logo"
+
    | "mark-read"
+
    | "markdown"
+
    | "menu"
+
    | "minus"
+
    | "moon"
+
    | "none"
+
    | "offline"
+
    | "online"
+
    | "open-external"
+
    | "patch"
+
    | "patch-archived"
+
    | "patch-draft"
+
    | "patch-merged"
+
    | "pin-filled"
+
    | "pin-hollow"
+
    | "placeholder"
+
    | "play"
+
    | "plus"
+
    | "question-mark"
+
    | "reply"
+
    | "repository"
+
    | "revision"
+
    | "sad-emoji"
+
    | "search"
+
    | "seed"
+
    | "seed-filled"
+
    | "settings"
+
    | "share"
+
    | "sidebar-left"
+
    | "sidebar-left-filled"
+
    | "sidebar-right"
+
    | "sidebar-right-filled"
+
    | "stop"
+
    | "sun"
+
    | "thumbs-up"
+
    | "trash"
+
    | "warning"
+
    | "webhooks";
+

  interface Props {
+
    name: IconName;
    size?: "16" | "32";
    onclick?: (e: MouseEvent) => void;
    disabled?: boolean;
    styleDisplay?: string;
    styleVerticalAlign?: string;
-
    name:
-
      | "add"
-
      | "arrow-left"
-
      | "arrow-right"
-
      | "arrow-right-hollow"
-
      | "attachment"
-
      | "binary"
-
      | "branch"
-
      | "broom"
-
      | "broom-double"
-
      | "bulb"
-
      | "checkbox-checked"
-
      | "checkbox-unchecked"
-
      | "checkmark"
-
      | "checkmark-double"
-
      | "checkout"
-
      | "chevron-down"
-
      | "chevron-right"
-
      | "chevron-up"
-
      | "clock"
-
      | "code"
-
      | "collapse"
-
      | "collapse-panel"
-
      | "comment"
-
      | "comment-checkmark"
-
      | "comment-cross"
-
      | "copy"
-
      | "cross"
-
      | "cross-double"
-
      | "dashboard"
-
      | "delegate"
-
      | "diff"
-
      | "ellipsis"
-
      | "expand"
-
      | "expand-panel"
-
      | "eye"
-
      | "eye-closed"
-
      | "face"
-
      | "file"
-
      | "filter"
-
      | "folder-closed"
-
      | "folder-open"
-
      | "help"
-
      | "home"
-
      | "hourglass"
-
      | "inbox"
-
      | "info"
-
      | "issue"
-
      | "issue-closed"
-
      | "label"
-
      | "link"
-
      | "lock"
-
      | "markdown"
-
      | "minus"
-
      | "moon"
-
      | "more-vertical"
-
      | "none"
-
      | "offline"
-
      | "online"
-
      | "open-external"
-
      | "patch"
-
      | "patch-archived"
-
      | "patch-draft"
-
      | "patch-merged"
-
      | "pen"
-
      | "pin"
-
      | "pin-hollow"
-
      | "plus"
-
      | "reply"
-
      | "repo"
-
      | "review"
-
      | "revision"
-
      | "seedling"
-
      | "seedling-filled"
-
      | "settings"
-
      | "stop"
-
      | "sun"
-
      | "thumb-up"
-
      | "trash"
-
      | "unresolve"
-
      | "user"
-
      | "warning";
  }

  const {
+
    name,
    size = "16",
    onclick = undefined,
-
    name,
    disabled = false,
    styleDisplay = "flex",
    styleVerticalAlign = undefined,
@@ -109,13 +137,13 @@
    user-select: none;
  }
  .hoverable {
-
    color: var(--color-foreground-dim);
+
    color: var(--color-text-secondary);
  }
  .hoverable:not(.disabled):hover {
-
    color: var(--color-foreground-default);
+
    color: var(--color-text-secondary);
  }
  .disabled {
-
    color: var(--color-foreground-disabled);
+
    color: var(--color-text-disabled);
  }
</style>

@@ -138,1411 +166,530 @@
  height={size}
  fill="currentColor"
  viewBox="0 0 16 16">
-
  {#if name === "add"}
-
    <path d="M2 3H3V13H2V3Z" />
-
    <path d="M3 13H13V14H3L3 13Z" />
-
    <path d="M3 2H13V3L3 3L3 2Z" />
-
    <path d="M13 3L14 3V13H13V3Z" />
-
    <path d="M7 5H9V11H7V5Z" />
-
    <path d="M5 7H11V9H5V7Z" />
+
  {#if name === "activity"}
+
    <path
+
      d="M9.97559 11.7139L11.5361 7.81445L11.6611 7.5H15V8.5H12.3379L10.0244 14.2861L5.97266 5.1709L4.44727 8.22363L4.30859 8.5H1V7.5H3.69141L6.02637 2.8291L9.97559 11.7139Z" />
+
  {:else if name === "add-emoji"}
+
    <path
+
      d="M1.5 8C1.5 4.41015 4.41015 1.5 8 1.5V2.5C4.96243 2.5 2.5 4.96243 2.5 8C2.5 11.0376 4.96243 13.5 8 13.5C11.0376 13.5 13.5 11.0376 13.5 8H14.5C14.5 11.5898 11.5898 14.5 8 14.5C4.41015 14.5 1.5 11.5898 1.5 8Z" />
+
    <path
+
      d="M11.2993 9.16699C11.127 9.65426 10.8474 10.1022 10.4751 10.4746C9.81875 11.131 8.92778 11.5 7.99951 11.5C7.07135 11.4999 6.1812 11.1309 5.5249 10.4746C5.15236 10.1021 4.87206 9.65436 4.69971 9.16699L5.64307 8.83301C5.7662 9.18125 5.96584 9.50147 6.23193 9.76758C6.70071 10.2363 7.33659 10.4999 7.99951 10.5C8.66247 10.5 9.29828 10.2363 9.76709 9.76758C10.0333 9.50142 10.2339 9.18122 10.3569 8.83301L11.2993 9.16699Z" />
+
    <path
+
      d="M6 7C6.27614 7 6.5 6.77614 6.5 6.5C6.5 6.22386 6.27614 6 6 6C5.72386 6 5.5 6.22386 5.5 6.5C5.5 6.77614 5.72386 7 6 7Z" />
+
    <path
+
      d="M10 7C10.2761 7 10.5 6.77614 10.5 6.5C10.5 6.22386 10.2761 6 10 6C9.72386 6 9.5 6.22386 9.5 6.5C9.5 6.77614 9.72386 7 10 7Z" />
+
    <path
+
      d="M6.25 6.5C6.25 6.36193 6.13807 6.25 6 6.25C5.86193 6.25 5.75 6.36193 5.75 6.5C5.75 6.63807 5.86193 6.75 6 6.75C6.13807 6.75 6.25 6.63807 6.25 6.5ZM10.25 6.5C10.25 6.36193 10.1381 6.25 10 6.25C9.86193 6.25 9.75 6.36193 9.75 6.5C9.75 6.63807 9.86193 6.75 10 6.75C10.1381 6.75 10.25 6.63807 10.25 6.5ZM6.75 6.5C6.75 6.91421 6.41421 7.25 6 7.25C5.58579 7.25 5.25 6.91421 5.25 6.5C5.25 6.08579 5.58579 5.75 6 5.75C6.41421 5.75 6.75 6.08579 6.75 6.5ZM10.75 6.5C10.75 6.91421 10.4142 7.25 10 7.25C9.58579 7.25 9.25 6.91421 9.25 6.5C9.25 6.08579 9.58579 5.75 10 5.75C10.4142 5.75 10.75 6.08579 10.75 6.5Z" />
+
    <path d="M13 1.5V3H14.5V4H13V5.5H12V4H10.5V3H12V1.5H13Z" />
+
  {:else if name === "archive"}
+
    <path
+
      d="M14.5 2.5V6H13.5V14.5H2.5V6H1.5V2.5H14.5ZM3.5 13.5H12.5V6H3.5V13.5ZM2.5 5H13.5V3.5H2.5V5Z" />
+
    <path d="M11 7.5L11 8.5L5 8.5L5 7.5L11 7.5Z" />
+
  {:else if name === "arrow-down"}
+
    <path
+
      d="M7.5 3.5L8.5 3.5V10.793L11 8.29297L11.707 9L8 12.707L4.29297 9L5 8.29297L7.5 10.793L7.5 3.5Z" />
  {:else if name === "arrow-left"}
-
    <path d="M1.99997 7L1.99997 8L2.99997 8L2.99997 7L1.99997 7Z" />
-
    <path d="M2.99997 6L2.99997 7L3.99997 7L3.99997 6L2.99997 6Z" />
-
    <path d="M2.99997 9L2.99997 8L3.99997 8L3.99997 9L2.99997 9Z" />
-
    <path d="M2.99997 9L14 9L14 7L2.99997 7L2.99997 9Z" />
-
    <path d="M3.99997 11L4.99997 11L4.99997 5L3.99997 5L3.99997 11Z" />
-
    <path d="M4.99997 12L5.99997 12L5.99997 4L4.99997 4L4.99997 12Z" />
-
    <path d="M5.99997 13L6.99997 13L6.99997 3L5.99997 3L5.99997 13Z" />
-
    <path d="M2.99997 10L3.99997 10L3.99997 9L2.99997 9L2.99997 10Z" />
-
    <path d="M1.99997 9L2.99997 9L2.99997 8L1.99997 8L1.99997 9Z" />
+
    <path
+
      d="M5.20703 8.5L7.70703 11L7 11.707L3.29297 8L7 4.29297L7.70703 5L5.20703 7.5L12.5 7.5L12.5 8.5L5.20703 8.5Z" />
  {:else if name === "arrow-right"}
-
    <path d="M14 9V8H13V9H14Z" />
-
    <path d="M13 10V9H12V10H13Z" />
-
    <path d="M13 7V8H12V7H13Z" />
-
    <path d="M13 7H2.00003V9H13V7Z" />
-
    <path d="M12 5H11V11H12V5Z" />
-
    <path d="M11 4H10V12H11V4Z" />
-
    <path d="M10 3H9.00003V13H10V3Z" />
-
    <path d="M13 6H12V7H13V6Z" />
-
    <path d="M14 7H13V8H14V7Z" />
-
  {:else if name === "arrow-right-hollow"}
-
    <path d="M9 9L3 9L3 10L9 10L9 9Z" />
-
    <path d="M9 6L3 6L3 7L9 7L9 6Z" />
-
    <path d="M11 4L10 4L10 5L11 5L11 4Z" />
-
    <path d="M10 4L9 4L9 5L10 5L10 4Z" />
-
    <path d="M10 3L9 3L9 6L10 6L10 3Z" />
-
    <path d="M12 5L11 5L11 6L12 6L12 5Z" />
-
    <path d="M13 6L12 6L12 7L13 7L13 6Z" />
-
    <path d="M13 9L12 9L12 10L13 10L13 9Z" />
-
    <path d="M14 8L13 8L13 9L14 9L14 8Z" />
-
    <path d="M14 7L13 7L13 8L14 8L14 7Z" />
-
    <path d="M12 10L11 10L11 11L12 11L12 10Z" />
-
    <path d="M13 9L12 9L12 10L13 10L13 9Z" />
-
    <path d="M11 11L10 11L10 12L11 12L11 11Z" />
-
    <path d="M12 10L11 10L11 11L12 11L12 10Z" />
-
    <path d="M10 11L9 11L9 12L10 12L10 11Z" />
-
    <path d="M10 10L9 10L9 13L10 13L10 10Z" />
-
    <path d="M2 6L3 6L3 10L2 10L2 6Z" />
-
  {:else if name === "checkbox-checked"}
-
    <path d="M2 3H3V13H2V3Z" />
-
    <path d="M13 3H14V13H13V3Z" />
-
    <path d="M3 13H13V14H3L3 13Z" />
-
    <path
-
      d="M6 9.5V8.5H5V7.5L3 7.5L3 2L13 2V4.5L11 4.5V5.5H10L10 6.5H9V7.5H8V8.5H7V9.5H6Z" />
-
    <path
-
      d="M3 8.5H4L4 9.5L5 9.5L5 10.5H6V11.5H7L7 10.5L8 10.5V9.5H9V8.5H10V7.5L11 7.5V6.5H12V5.5H13L13 13H3L3 8.5Z" />
-
  {:else if name === "checkbox-unchecked"}
-
    <path d="M2 3H3V13H2V3Z" />
-
    <path d="M13 3H14V13H13V3Z" />
-
    <path d="M3 13H13V14H3L3 13Z" />
-
    <path d="M3 2H13V3H3L3 2Z" />
-
  {:else if name === "attachment"}
-
    <path d="M4 4H12V5H4V4Z" />
-
    <path d="M4 11H11V12H4V11Z" />
-
    <path d="M13 6H14V8H13V6Z" />
-
    <path d="M2 6H3V10H2V6Z" />
-
    <path d="M12 5L13 5V6H12V5Z" />
-
    <path d="M3 5L4 5V6L3 6L3 5Z" />
-
    <path d="M12 8H13V9H12V8Z" />
-
    <path d="M3 10H4L4 11H3L3 10Z" />
-
    <path d="M5 9L12 9V10H5V9Z" />
-
    <path d="M4 8H5V9L4 9V8Z" />
-
    <path d="M5 6H11V7H5V6Z" />
-
    <path d="M4 7H5L5 8H4L4 7Z" />
+
    <path
+
      d="M3.5 8.5L3.5 7.5L10.793 7.5L8.29297 5L9 4.29297L12.707 8L9 11.707L8.29297 11L10.793 8.5L3.5 8.5Z" />
+
  {:else if name === "arrow-up"}
+
    <path
+
      d="M8.5 12.5L7.5 12.5L7.5 5.20703L5 7.70703L4.29297 7L8 3.29297L11.707 7L11 7.70703L8.5 5.20703L8.5 12.5Z" />
+
  {:else if name === "attach"}
+
    <path
+
      d="M7.89634 2.39647C9.0581 1.23471 10.9416 1.23471 12.1034 2.39647C13.2651 3.55823 13.2651 5.44174 12.1034 6.6035L7.35337 11.3535C6.60582 12.101 5.39388 12.101 4.64634 11.3535C3.89879 10.606 3.89879 9.39401 4.64634 8.64647L8.64634 4.64647L9.35337 5.3535L5.35337 9.3535C4.99634 9.71052 4.99634 10.2894 5.35337 10.6465C5.71039 11.0035 6.28931 11.0035 6.64634 10.6465L11.3963 5.89647C12.1676 5.12523 12.1676 3.87473 11.3963 3.1035C10.6251 2.33226 9.3746 2.33226 8.60337 3.1035L3.85337 7.8535C2.66792 9.03895 2.66792 10.961 3.85337 12.1465C5.03882 13.3319 6.96089 13.3319 8.14634 12.1465L12.6463 7.64647L13.3534 8.3535L8.85337 12.8535C7.27739 14.4295 4.72231 14.4295 3.14634 12.8535C1.57036 11.2775 1.57036 8.72244 3.14634 7.14647L7.89634 2.39647Z" />
+
  {:else if name === "avatar-incognito"}
+
    <path
+
      d="M13.5 8C13.5 4.96243 11.0376 2.5 8 2.5C4.96243 2.5 2.5 4.96243 2.5 8C2.5 9.74051 3.3094 11.291 4.57129 12.2988C4.61586 12.0809 4.68106 11.8667 4.7666 11.6602C4.94249 11.2357 5.2005 10.8503 5.52539 10.5254C5.85028 10.2005 6.23569 9.94249 6.66016 9.7666C7.0848 9.59071 7.54037 9.5 8 9.5C8.45963 9.5 8.9152 9.59071 9.33984 9.7666C9.76431 9.94249 10.1497 10.2005 10.4746 10.5254C10.7995 10.8503 11.0575 11.2357 11.2334 11.6602C11.3189 11.8666 11.3832 12.081 11.4277 12.2988C12.69 11.291 13.5 9.74078 13.5 8ZM8 10.5C7.6717 10.5 7.34628 10.5648 7.04297 10.6904C6.73978 10.8161 6.46449 11.0004 6.23242 11.2324C6.00036 11.4645 5.81606 11.7398 5.69043 12.043C5.57752 12.3155 5.51565 12.6061 5.50391 12.9004C6.25321 13.2828 7.10103 13.5 8 13.5C8.89866 13.5 9.74601 13.2826 10.4951 12.9004C10.4834 12.6061 10.4225 12.3155 10.3096 12.043C10.1839 11.7398 9.99964 11.4645 9.76758 11.2324C9.53551 11.0004 9.26022 10.8161 8.95703 10.6904C8.65372 10.5648 8.32831 10.5 8 10.5ZM9 6.5C9 5.94772 8.55228 5.5 8 5.5C7.44772 5.5 7 5.94772 7 6.5C7 7.05228 7.44772 7.5 8 7.5C8.55228 7.5 9 7.05228 9 6.5ZM14.5 8C14.5 10.3005 13.3033 12.3194 11.5 13.4746V13.5H11.4619C10.4593 14.1324 9.27294 14.5 8 14.5C6.72706 14.5 5.54065 14.1324 4.53809 13.5H4.5V13.4746C2.69675 12.3194 1.5 10.3005 1.5 8C1.5 4.41015 4.41015 1.5 8 1.5C11.5899 1.5 14.5 4.41015 14.5 8ZM10 6.5C10 7.60457 9.10457 8.5 8 8.5C6.89543 8.5 6 7.60457 6 6.5C6 5.39543 6.89543 4.5 8 4.5C9.10457 4.5 10 5.39543 10 6.5Z" />
+
  {:else if name === "badge"}
+
    <path
+
      d="M4.64602 8.34829L5.35313 7.64118L7.00488 9.29294L10.6613 5.63656L11.3684 6.34367L7.00488 10.7072L4.64602 8.34829Z" />
+
    <path
+
      d="M14.3125 4.06738L14.5625 4.21094V11.7891L14.3125 11.9326L8 15.5771L1.6875 11.9326L1.4375 11.7891V4.21094L1.6875 4.06738L8 0.422852L14.3125 4.06738ZM2.4375 4.78809V11.2109L8 14.4229L13.5625 11.2109V4.78809L8 1.57617L2.4375 4.78809Z" />
+
  {:else if name === "bell"}
+
    <path
+
      d="M6.5 11.5V12C6.5 12.197 6.53888 12.3922 6.61426 12.5742C6.68964 12.7561 6.80021 12.9213 6.93945 13.0605C7.07869 13.1998 7.24387 13.3104 7.42578 13.3857C7.60777 13.4611 7.80302 13.5 8 13.5C8.19698 13.5 8.39223 13.4611 8.57422 13.3857C8.75613 13.3104 8.92131 13.1998 9.06055 13.0605C9.19979 12.9213 9.31036 12.7561 9.38574 12.5742C9.46112 12.3922 9.5 12.197 9.5 12V11.5H6.5ZM5.48535 7.12109L5.42969 7.34473L5.22363 7.44727L3.5 8.30859V10.5H12.5V8.30859L10.7764 7.44727L10.5703 7.34473L10.5146 7.12109L9.60938 3.5H6.39062L5.48535 7.12109ZM10.5 12C10.5 12.3283 10.4352 12.6537 10.3096 12.957C10.1839 13.2602 9.99964 13.5355 9.76758 13.7676C9.53551 13.9996 9.26022 14.1839 8.95703 14.3096C8.65372 14.4352 8.3283 14.5 8 14.5C7.6717 14.5 7.34628 14.4352 7.04297 14.3096C6.73978 14.1839 6.46449 13.9996 6.23242 13.7676C6.00036 13.5355 5.81606 13.2602 5.69043 12.957C5.56479 12.6537 5.5 12.3283 5.5 12V11.5H2.5V7.69141L2.77637 7.55273L4.57031 6.65527L5.60938 2.5H10.3906L11.4287 6.65527L13.2236 7.55273L13.5 7.69141V11.5H10.5V12Z" />
  {:else if name === "binary"}
-
    <path d="M10 3.5H11V4.5H10V3.5Z" />
-
    <path d="M11 4.5L12 4.5V5.5H11V4.5Z" />
-
    <path d="M10 5.5L12 5.5V6.5H10V5.5Z" />
-
    <path d="M9 5.5H10V6.5H9V5.5Z" />
-
    <path d="M8 4.5H9V5.5L8 5.5V4.5Z" />
-
    <path d="M8 2.5H9L9 4.5H8L8 2.5Z" />
-
    <path d="M9 2.5L10 2.5V3.5H9V2.5Z" />
-
    <path d="M4 13.5H12V14.5H4V13.5Z" />
-
    <path d="M4 1.5H9V2.5L4 2.5V1.5Z" />
-
    <path d="M13 5.5V13.5L12 13.5L12 5.5L13 5.5Z" />
-
    <path d="M4 2.5L4 13.5H3L3 2.5L4 2.5Z" />
-
    <path d="M5 9.5H6V11.5H5V9.5Z" />
-
    <path d="M7 9.5H8V11.5H7V9.5Z" />
-
    <path d="M6 11.5H7L7 12.5H6L6 11.5Z" />
-
    <path d="M6 8.5H7V9.5L6 9.5L6 8.5Z" />
-
    <path d="M9 9.5H10V10.5H9V9.5Z" />
-
    <path d="M10 8.5H11V12.5H10V8.5Z" />
-
    <path d="M5 4.5H6V5.5H5V4.5Z" />
-
    <path d="M6 3.5H7V7.5H6V3.5Z" />
+
    <path
+
      d="M11 8.5C12.3807 8.5 13.5 9.61929 13.5 11H12.5C12.5 10.1716 11.8285 9.5 11 9.5C10.1716 9.5 9.5 10.1716 9.5 11H8.5C8.5 9.61929 9.61929 8.5 11 8.5Z" />
+
    <path
+
      d="M8.5 12V11H9.5V12C9.5 12.8285 10.1716 13.5 11 13.5C11.8285 13.5 12.5 12.8285 12.5 12V11H13.5V12C13.5 13.3807 12.3807 14.5 11 14.5C9.61929 14.5 8.5 13.3807 8.5 12Z" />
+
    <path d="M5.5 8.5V13.5H7V14.5H3V13.5H4.5V10.5H3V9.5H4.5V8.5H5.5Z" />
+
    <path d="M11.5 1.5V6.5H13V7.5H9V6.5H10.5V3.5H9V2.5H10.5V1.5H11.5Z" />
+
    <path
+
      d="M5 1.5C6.38071 1.5 7.5 2.61929 7.5 4H6.5C6.5 3.17157 5.82843 2.5 5 2.5C4.17157 2.5 3.5 3.17157 3.5 4H2.5C2.5 2.61929 3.61929 1.5 5 1.5Z" />
+
    <path
+
      d="M2.5 5V4H3.5V5C3.5 5.82843 4.17157 6.5 5 6.5C5.82843 6.5 6.5 5.82843 6.5 5V4H7.5V5C7.5 6.38071 6.38071 7.5 5 7.5C3.61929 7.5 2.5 6.38071 2.5 5Z" />
+
  {:else if name === "bookmark"}
+
    <path
+
      d="M3.5 2.5L12.5 2.5L12.5 15L8 11.626L3.5 15L3.5 2.5ZM8 10.375L11.5 13L11.5 3.5L4.5 3.5L4.5 13L8 10.375Z" />
+
  {:else if name === "bookmark-filled"}
+
    <path d="M12.5 2.5V15L8 11.625L3.5 15V2.5H12.5Z" />
  {:else if name === "branch"}
-
    <path d="M11 5L10 5V2L13 2V5L12 5V8L11 8V5ZM11 3H12V4H11V3Z" />
-
    <path
-
      d="M11 9L5 9L5 11H6L6 14H3L3 11H4L4 5H3L3 2L6 2L6 5H5L5 8H11V9ZM4 4L5 4V3H4L4 4ZM4 13V12H5L5 13H4Z" />
-
  {:else if name === "broom"}
-
    <path d="M11 13H12V14H11V13Z" />
-
    <path d="M11 13H12V14H11V13Z" />
-
    <path d="M11 12H12V13H11V12Z" />
-
    <path d="M10 12H11V14H10V12Z" />
-
    <path d="M8 12H9V14H8V12Z" />
-
    <path d="M4 12H11V14H4V12Z" />
-
    <path d="M9 12H10V13H9V12Z" />
-
    <path d="M7 12H8V13H7V12Z" />
-
    <path d="M4 10H12V11H4V10Z" />
-
    <path d="M7 3H8V10H7V3Z" />
-
    <path d="M8 3H9V10H8V3Z" />
-
    <path d="M7 2H9V3H7V2Z" />
-
  {:else if name === "broom-double"}
-
    <path d="M9 13H10V14H9V13Z" />
-
    <path d="M9 13H10V14H9V13Z" />
-
    <path d="M9 12H10V13H9V12Z" />
-
    <path d="M8 12H9V14H8V12Z" />
-
    <path d="M6 12H7V14H6V12Z" />
-
    <path d="M2 12H9V14H2V12Z" />
-
    <path d="M7 12H8V13H7V12Z" />
-
    <path d="M5 12H6V13H5V12Z" />
-
    <path d="M2 10H10V11H2V10Z" />
-
    <path d="M5 3H6V10H5V3Z" />
-
    <path d="M6 3H7V10H6V3Z" />
-
    <path d="M5 2H7V3H5V2Z" />
-
    <path d="M13 13H14V14H13V13Z" />
-
    <path d="M13 13H14V14H13V13Z" />
-
    <path d="M13 12H14V13H13V12Z" />
-
    <path d="M12 12H13V14H12V12Z" />
-
    <path d="M11 12H13V14H11V12Z" />
-
    <path d="M11 12H12V13H11V12Z" />
-
    <path d="M9 12H10V13H9V12Z" />
-
    <path d="M11 10H14V11H11V10Z" />
-
    <path d="M9 3H10V9H9V3Z" />
-
    <path d="M10 3H11V9H10V3Z" />
-
    <path d="M9 2H11V3H9V2Z" />
-
  {:else if name === "bulb"}
-
    <path d="M9 13.5H7V14.5H9V13.5Z" />
-
    <path d="M5 7.50003V9.50003H4L4 7.50003H5Z" />
-
    <path d="M4 4.50003L4 7.50003H3L3 4.50003H4Z" />
-
    <path d="M12 4.50003V7.50003H13V4.50003H12Z" />
-
    <path d="M11 7.50003V9.50003L12 9.50003V7.50003H11Z" />
-
    <path d="M5 4.50003L4 4.50003L4 3.50003L5 3.50003L5 4.50003Z" />
-
    <path d="M11 4.50003H12V3.50003H11V4.50003Z" />
-
    <path d="M6 9.50003V10.5H5L5 9.50003H6Z" />
-
    <path d="M6 2.50003L10 2.50003V1.50003L6 1.50003L6 2.50003Z" />
-
    <path d="M10 9.50003L11 9.50003V10.5H10V9.50003Z" />
-
    <path d="M9 10.5H10V13.5H9L9 10.5Z" />
-
    <path d="M6 10.5H7V13.5H6L6 10.5Z" />
-
    <path d="M7 10.5H10L10 11.5H7V10.5Z" />
-
    <path d="M10 2.50003H12V3.50003L10 3.50003L10 2.50003Z" />
-
    <path d="M4 2.50003L6 2.50003V3.50003L4 3.50003L4 2.50003Z" />
-
    <path d="M7 7.50003H9V9.50003H7V7.50003Z" />
-
    <path d="M7 12.5H8V13.5H7L7 12.5Z" />
+
    <path
+
      d="M5 4C5 3.44772 4.55228 3 4 3C3.44772 3 3 3.44772 3 4C3 4.55228 3.44772 5 4 5V6C2.89543 6 2 5.10457 2 4C2 2.89543 2.89543 2 4 2C5.10457 2 6 2.89543 6 4C6 5.10457 5.10457 6 4 6V5C4.55228 5 5 4.55228 5 4Z" />
+
    <path
+
      d="M13 4C13 3.44772 12.5523 3 12 3C11.4477 3 11 3.44772 11 4C11 4.55228 11.4477 5 12 5V6C10.8954 6 10 5.10457 10 4C10 2.89543 10.8954 2 12 2C13.1046 2 14 2.89543 14 4C14 5.10457 13.1046 6 12 6V5C12.5523 5 13 4.55228 13 4Z" />
+
    <path
+
      d="M5 12C5 11.4477 4.55228 11 4 11C3.44772 11 3 11.4477 3 12C3 12.5523 3.44772 13 4 13V14C2.89543 14 2 13.1046 2 12C2 10.8954 2.89543 10 4 10C5.10457 10 6 10.8954 6 12C6 13.1046 5.10457 14 4 14V13C4.55228 13 5 12.5523 5 12Z" />
+
    <path d="M4.5 5.5V10.5H3.5V5.5H4.5Z" />
+
    <path
+
      d="M12.5 5.00001V7.20705L10.707 9.00001H3.99995V8.00001H10.2929L11.5 6.79298V5.00001H12.5Z" />
  {:else if name === "checkmark"}
-
    <path d="M7 11V12H6V11H7Z" />
-
    <path d="M8 10V11L7 11L7 10H8Z" />
-
    <path d="M9 9V10H8V9L9 9Z" />
-
    <path d="M10 8V9L9 9V8H10Z" />
-
    <path d="M11 7V8L10 8L10 7H11Z" />
-
    <path d="M12 6V7H11V6H12Z" />
-
    <path d="M13 5V6L12 6V5L13 5Z" />
-
    <path d="M4 8V9H3L3 8H4Z" />
-
    <path d="M5 9L5 10L4 10L4 9H5Z" />
-
    <path d="M6 10L6 11H5L5 10L6 10Z" />
-
  {:else if name === "checkmark-double"}
-
    <path d="M6 10L6 11H5L5 10H6Z" />
-
    <path d="M8 10V11H7L7 10H8Z" />
-
    <path d="M7 9L7 10L6 10V9H7Z" />
-
    <path d="M9 9V10H8V9L9 9Z" />
-
    <path d="M8 8L8 9L7 9L7 8H8Z" />
-
    <path d="M10 8V9L9 9V8H10Z" />
-
    <path d="M9 7L9 8L8 8L8 7H9Z" />
-
    <path d="M11 7V8L10 8L10 7H11Z" />
-
    <path d="M10 6V7L9 7V6L10 6Z" />
-
    <path d="M12 6V7H11V6H12Z" />
-
    <path d="M11 5V6L10 6V5L11 5Z" />
-
    <path d="M13 5V6L12 6V5L13 5Z" />
-
    <path d="M4 8V9H3L3 8H4Z" />
-
    <path d="M6 8L6 9L5 9V8H6Z" />
-
    <path d="M5 9L5 10H4L4 9H5Z" />
-
    <path d="M7 9L7 10L6 10V9H7Z" />
+
    <path
+
      d="M3 7.29297L6 10.293L13 3.29297L13.707 4L6 11.707L2.29297 8L3 7.29297Z" />
  {:else if name === "checkout"}
-
    <path d="M5 5H11V6H5V5Z" />
-
    <path d="M4 6L5 6L5 11H4L4 6Z" />
-
    <path d="M11 6L12 6V11H11L11 6Z" />
-
    <path d="M3 11H4L4 12H3V11Z" />
-
    <path d="M12 11L13 11V12H12V11Z" />
-
    <path d="M3 13H13V14H3V13Z" />
-
    <path d="M4 10H12V11L4 11L4 10Z" />
-
    <path d="M13 12L14 12V13L13 13V12Z" />
-
    <path d="M2 12H3L3 13H2V12Z" />
-
    <path d="M7 2L9 2V6H7V2Z" />
-
    <path d="M7 7H9V8H7V7Z" />
-
    <path d="M6 6H10V7H6V6Z" />
-
    <path d="M5 5H11V6H5V5Z" />
+
    <path
+
      d="M9 4C9 3.44772 8.55228 3 8 3C7.44772 3 7 3.44772 7 4C7 4.55228 7.44772 5 8 5V6C6.89543 6 6 5.10457 6 4C6 2.89543 6.89543 2 8 2C9.10457 2 10 2.89543 10 4C10 5.10457 9.10457 6 8 6V5C8.55228 5 9 4.55228 9 4Z" />
+
    <path
+
      d="M7.5 5L8.5 5L8.5 9.29297L10 7.79297L10.707 8.5L8 11.207L5.29297 8.5L6 7.79297L7.5 9.29297L7.5 5Z" />
+
    <path d="M3.5 10V12.5H12.5V10H13.5V13.5H2.5V10H3.5Z" />
  {:else if name === "chevron-down"}
-
    <path d="M9 10V11H8V10H9Z" />
-
    <path d="M10 9V10L9 10V9H10Z" />
-
    <path d="M11 8V9H10V8L11 8Z" />
-
    <path d="M12 7V8L11 8V7H12Z" />
-
    <path d="M4 6V7H3L3 6L4 6Z" />
-
    <path d="M13 6V7L12 7L12 6L13 6Z" />
-
    <path d="M5 7L5 8H4L4 7L5 7Z" />
-
    <path d="M6 8V9H5L5 8L6 8Z" />
-
    <path d="M8 10V11H7L7 10H8Z" />
-
    <path d="M7 9L7 10H6L6 9L7 9Z" />
+
    <path
+
      d="M4 5.29297L8 9.29297L12 5.29297L12.707 6L8 10.707L3.29297 6L4 5.29297Z" />
+
  {:else if name === "chevron-left"}
+
    <path
+
      d="M10 3.29297L10.707 4L6.70703 8L10.707 12L10 12.707L5.29297 8L10 3.29297Z" />
+
  {:else if name === "chevron-left-right"}
+
    <path
+
      fill-rule="evenodd"
+
      d="M6.104 3.646a.5.5 0 010 .708l-3.28 3.278a.167.167 0 00.001.236l3.279 3.278a.5.5 0 01-.708.708L2.118 8.575a1.167 1.167 0 010-1.65l3.278-3.279a.5.5 0 01.708 0zm3.792 0a.5.5 0 01.707 0l3.28 3.279a1.167 1.167 0 010 1.65l-3.28 3.279a.5.5 0 01-.707-.708l3.279-3.278a.167.167 0 000-.236L9.896 4.354a.5.5 0 010-.708z"
+
      clip-rule="evenodd">
+
    </path>
  {: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" />
+
    <path
+
      d="M7 3.29297L11.707 8L7 12.707L6.29297 12L10.29297 8L6.29297 4L7 3.29297Z" />
  {:else if name === "chevron-up"}
-
    <path d="M7 6.5L7 5.5L8 5.5L8 6.5L7 6.5Z" />
-
    <path d="M6 7.5L6 6.5L7 6.5V7.5L6 7.5Z" />
-
    <path d="M5 8.5L5 7.5L6 7.5L6 8.5L5 8.5Z" />
-
    <path d="M4 9.5L4 8.5L5 8.5L5 9.5L4 9.5Z" />
-
    <path d="M3 10.5L3 9.5L4 9.5L4 10.5H3Z" />
-
    <path d="M12 10.5V9.5H13V10.5H12Z" />
-
    <path d="M11 9.5L11 8.5L12 8.5L12 9.5L11 9.5Z" />
-
    <path d="M10 8.5V7.5L11 7.5V8.5L10 8.5Z" />
-
    <path d="M8 6.5L8 5.5L9 5.5L9 6.5L8 6.5Z" />
-
    <path d="M9 7.5V6.5L10 6.5L10 7.5L9 7.5Z" />
+
    <path
+
      d="M8 5.29297L12.707 10L12 10.707L8 6.70703L4 10.707L3.29297 10L8 5.29297Z" />
+
  {:else if name === "chevron-up-down"}
+
    <path
+
      d="M12.707 10L8 14.707L3.29297 10L4 9.29297L8 13.293L12 9.29297L12.707 10Z" />
+
    <path
+
      d="M12.707 6L12 6.70703L8 2.70703L4 6.70703L3.29297 6L8 1.29297L12.707 6Z" />
+
  {:else if name === "clear-all"}
+
    <path
+
      d="M9 1.5V2.5H2.5V8.5H5.30859L5.44727 8.77637L6.30859 10.5H9.69141L10.5527 8.77637L10.6914 8.5H13.5V7H14.5V14.5H1.5V1.5H9ZM2.5 13.5H13.5V9.5H11.3086L10.4473 11.2236L10.3086 11.5H5.69141L5.55273 11.2236L4.69141 9.5H2.5V13.5Z" />
+
    <path
+
      d="M15.3535 1.35352L13.707 3L15.3535 4.64648L14.6465 5.35352L13 3.70703L11.3535 5.35352L10.6465 4.64648L12.293 3L10.6465 1.35352L11.3535 0.646484L13 2.29297L14.6465 0.646484L15.3535 1.35352Z" />
+
  {:else if name === "clipboard"}
+
    <path
+
      d="M11.5 1.5V3H13.5V14.5H2.5V3H4.5V1.5H11.5ZM3.5 13.5H12.5V4H11.5V5.5H4.5V4H3.5V13.5ZM5.5 4.5H10.5V2.5H5.5V4.5Z" />
  {:else if name === "clock"}
-
    <path d="M6 13H8V14H6V13Z" />
-
    <path d="M10 13H8V14H10V13Z" />
-
    <path d="M3 6L3 8H2L2 6H3Z" />
-
    <path d="M13 6V8H14V6H13Z" />
-
    <path d="M4 12H6V13H4V12Z" />
-
    <path d="M12 12H10V13H12V12Z" />
-
    <path d="M4 4V6H3L3 4H4Z" />
-
    <path d="M12 4V6L13 6V4L12 4Z" />
-
    <path d="M4 10L4 12H3L3 10H4Z" />
-
    <path d="M12 10V12H13V10H12Z" />
-
    <path d="M6 4L4 4L4 3L6 3V4Z" />
-
    <path d="M10 4L12 4V3L10 3V4Z" />
-
    <path d="M3 8L3 10H2L2 8H3Z" />
-
    <path d="M13 8V10H14V8H13Z" />
-
    <path d="M8 3L6 3V2L8 2V3Z" />
-
    <path d="M8 3L10 3L10 2L8 2V3Z" />
-
    <path d="M8 8H9V9H8V8Z" />
-
    <path d="M9 7H10V8H9V7Z" />
-
    <path d="M10 6H11V7L10 7V6Z" />
-
    <path d="M7 7H8V8L7 8V7Z" />
-
    <path d="M6 6H7V7L6 7V6Z" />
-
    <path d="M5 5H6V6H5V5Z" />
+
    <path
+
      d="M13.5 8C13.5 4.96243 11.0376 2.5 8 2.5C4.96243 2.5 2.5 4.96243 2.5 8C2.5 11.0376 4.96243 13.5 8 13.5C11.0376 13.5 13.5 11.0376 13.5 8ZM14.5 8C14.5 11.5899 11.5899 14.5 8 14.5C4.41015 14.5 1.5 11.5899 1.5 8C1.5 4.41015 4.41015 1.5 8 1.5C11.5899 1.5 14.5 4.41015 14.5 8Z" />
+
    <path d="M8 4V8H11V9H7V4H8Z" />
+
  {:else if name === "close"}
+
    <path
+
      d="M12.707 4L8.70605 8.00098L12.7051 12L11.998 12.707L7.99902 8.70801L4 12.707L3.29297 12L7.29199 8.00098L3.29297 4.00195L4 3.29492L7.99902 7.29395L12 3.29297L12.707 4Z" />
  {:else if name === "code"}
-
    <path d="M13 7H14V8H13V7Z" />
-
    <path d="M3 9H2L2 8H3L3 9Z" />
-
    <path d="M12 6H13V7L12 7V6Z" />
-
    <path d="M4 10H3L3 9H4L4 10Z" />
-
    <path d="M11 5L12 5V6L11 6V5Z" />
-
    <path d="M5 11H4L4 10H5V11Z" />
-
    <path d="M10 4H11V5L10 5V4Z" />
-
    <path d="M6 12H5V11H6V12Z" />
-
    <path d="M9 3H10V4L9 4V3Z" />
-
    <path d="M7 13H6L6 12H7V13Z" />
-
    <path d="M9 12L10 12V13L9 13V12Z" />
-
    <path d="M7 4L6 4L6 3L7 3V4Z" />
-
    <path d="M10 11H11V12L10 12L10 11Z" />
-
    <path d="M6 5H5V4H6L6 5Z" />
-
    <path d="M11 10H12L12 11H11L11 10Z" />
-
    <path d="M5 6H4L4 5H5V6Z" />
-
    <path d="M13 8H14V9H13V8Z" />
-
    <path d="M3 8H2L2 7H3L3 8Z" />
-
    <path d="M12 9L13 9V10L12 10V9Z" />
-
    <path d="M4 7H3L3 6H4L4 7Z" />
-
  {:else if name === "collapse"}
-
    <path d="M7 5.5L8 5.5L8 4.5L7 4.5L7 5.5Z" />
-
    <path d="M6 4.5L7 4.5L7 3.5L6 3.5L6 4.5Z" />
-
    <path d="M9 4.5L9 1.5L7 1.5L7 4.5L9 4.5Z" />
-
    <path d="M11 3.5L11 2.5L5 2.5L5 3.5L11 3.5Z" />
-
    <path d="M10 4.5L10 3.5L9 3.5L9 4.5L10 4.5Z" />
-
    <path d="M9 5.5L9 4.5L8 4.5L8 5.5L9 5.5Z" />
-
    <path d="M9 10.5L8 10.5L8 11.5L9 11.5L9 10.5Z" />
-
    <path d="M10 11.5L9 11.5L9 12.5L10 12.5L10 11.5Z" />
-
    <path d="M7 11.5L7 14.5L9 14.5L9 11.5L7 11.5Z" />
-
    <path d="M5 12.5L5 13.5L11 13.5L11 12.5L5 12.5Z" />
-
    <path d="M6 11.5L6 12.5L7 12.5L8 12.5L8 11.5L7 11.5L6 11.5Z" />
-
    <path d="M7 10.5L7 11.5L8 11.5L8 10.5L7 10.5Z" />
-
    <path d="M2 7.5H14V8.5H2V7.5Z" />
-
  {:else if name === "collapse-panel"}
-
    <path d="M2 3.00002H3V13H2V3.00002Z" />
-
    <path d="M13 3.00002H14V6.00002H13V3.00002Z" />
-
    <path d="M13 10H14V13H13V10Z" />
-
    <path d="M5 2.00002H6V14H5V2.00002Z" />
-
    <path d="M3 2.00002H13V3.00002H3L3 2.00002Z" />
-
    <path d="M3 13H13V14H3L3 13Z" />
-
    <path d="M8 9.00002L8 8.00002H9V9.00002L8 9.00002Z" />
-
    <path d="M9 10L9 9.00002H10V10H9Z" />
-
    <path d="M9 7.00002V8.00002L10 8.00002V7.00002L9 7.00002Z" />
-
    <path d="M9 7.00002L14 7.00001V9.00001L9 9.00002V7.00002Z" />
-
    <path d="M10 5.00001H11V11H10V5.00001Z" />
-
    <path d="M9 6.00002H10V7.00002L9 7.00002L9 6.00002Z" />
-
    <path d="M8 7.00001L9 7.00002V8.00002H8L8 7.00001Z" />
+
    <path
+
      d="M10 3.29297L14.707 8L10 12.707L9.29297 12L13.293 8L9.29297 4L10 3.29297Z" />
+
    <path
+
      d="M6 3.29297L6.70703 4L2.70703 8L6.70703 12L6 12.707L1.29297 8L6 3.29297Z" />
+
  {:else if name === "collapse-in"}
+
    <path
+
      d="M7.5 13.5H6.5V10.207L3 13.707L2.29297 13L5.79297 9.5H2.5V8.5H7.5V13.5Z" />
+
    <path
+
      d="M13.707 3L10.207 6.5H13.5V7.5H8.5V2.5H9.5V5.79297L13 2.29297L13.707 3Z" />
+
  {:else if name === "collapse-vertical"}
+
    <path
+
      d="M7.5 4.79297L5.5 2.79297L4.79297 3.5L8 6.70703L11.207 3.5L10.5 2.79297L8.5 4.79297V0.5H7.5V4.79297Z" />
+
    <path
+
      d="M7.5 11.207L5.5 13.207L4.79297 12.5L8 9.29297L11.207 12.5L10.5 13.207L8.5 11.207V15.5H7.5V11.207Z" />
+
    <path d="M14 7.5L14 8.5L2 8.5L2 7.5L14 7.5Z" />
  {:else if name === "comment"}
-
    <path d="M4 2H12V3H4V2Z" />
-
    <path d="M12 3L13 3V4H12V3Z" />
-
    <path d="M12 10H13V11H12V10Z" />
-
    <path d="M3 3L4 3V4H3V3Z" />
-
    <path d="M5 11H12V12H5V11Z" />
-
    <path d="M3 13H4V14H3V13Z" />
-
    <path d="M4 12H5V13H4L4 12Z" />
-
    <path d="M5 11H6V12H5V11Z" />
-
    <path d="M13 4H14V10H13V4Z" />
-
    <path d="M2 4L3 4L3 13H2V4Z" />
-
    <path d="M5 5H11V6H5V5Z" />
-
    <path d="M5 8H11V9H5V8Z" />
+
    <path
+
      d="M13.5 2.5V11.5H10.207L8 13.707L5.79297 11.5H2.5V2.5H13.5ZM3.5 10.5H6.20703L8 12.293L9.79297 10.5H12.5V3.5H3.5V10.5Z" />
+
    <path d="M11 7.5V8.5H5V7.5H11Z" />
+
    <path d="M11 5.5V6.5H5V5.5H11Z" />
  {:else if name === "comment-checkmark"}
-
    <path d="M4 2L10 2L10 3L4 3L4 2Z" />
-
    <path d="M3 3L4 3L4 4L3 4L3 3Z" />
-
    <path d="M12 2L13 2L13 3L12 3L12 2Z" />
-
    <path d="M13 2L14 2L14 3L13 3L13 2Z" />
-
    <path d="M12 10L13 10L13 11L12 11L12 10Z" />
-
    <path d="M5 11L12 11L12 12L5 12L5 11Z" />
-
    <path d="M3 13L4 13L4 14L3 14L3 13Z" />
-
    <path d="M4 12L5 12L5 13L4 13L4 12Z" />
-
    <path d="M5 11L6 11L6 12L5 12L5 11Z" />
-
    <path d="M13 5L14 5L14 10L13 10L13 5Z" />
-
    <path d="M2 8L3 8L3 13L2 13L2 8Z" />
-
    <path d="M5 6L6 6L6 7L5 7L5 6Z" />
-
    <path d="M4 5L5 5L5 6L4 6L4 5Z" />
-
    <path d="M6 7L7 7L7 8L6 8L6 7Z" />
-
    <path d="M7 8L8 8L8 9L7 9L7 8Z" />
-
    <path d="M8 7L9 7L9 8L8 8L8 7Z" />
-
    <path d="M9 6L10 6L10 7L9 7L9 6Z" />
-
    <path d="M10 5L11 5L11 6L10 6L10 5Z" />
-
    <path d="M11 4L12 4L12 5L11 5L11 4Z" />
-
    <path d="M12 3L13 3L13 4L12 4L12 3Z" />
-
    <path d="M11 3L12 3L12 4L11 4L11 3Z" />
-
    <path d="M10 4L11 4L11 5L10 5L10 4Z" />
-
    <path d="M9 5L10 5L10 6L9 6L9 5Z" />
-
    <path d="M8 6L9 6L9 7L8 7L8 6Z" />
-
    <path d="M7 7L8 7L8 8L7 8L7 7Z" />
-
    <path d="M5 5L6 5L6 6L5 6L5 5Z" />
-
    <path d="M6 6L7 6L7 7L6 7L6 6Z" />
-
    <path d="M2 4L3 4L3 8L2 8L2 4Z" />
+
    <path
+
      d="M13.5 2.5V11.5H10.207L8 13.707L5.79297 11.5H2.5V2.5H13.5ZM3.5 10.5H6.20703L8 12.293L9.79297 10.5H12.5V3.5H3.5V10.5Z" />
+
    <path
+
      d="M11.3682 5.35352L7.00491 9.7168L4.64651 7.3584L5.35355 6.65137L7.00491 8.30273L10.6612 4.64648L11.3682 5.35352Z" />
  {:else if name === "comment-cross"}
-
    <path d="M3 3L4 3L4 4L3 4L3 3Z" />
-
    <path d="M12 2L13 2L13 3L12 3L12 2Z" />
-
    <path d="M13 2L14 2L14 3L13 3L13 2Z" />
-
    <path d="M5 11L12 11L12 12L5 12L5 11Z" />
-
    <path d="M3 13L4 13L4 14L3 14L3 13Z" />
-
    <path d="M4 12L5 12L5 13L4 13L4 12Z" />
-
    <path d="M5 11L6 11L6 12L5 12L5 11Z" />
-
    <path d="M2 8L3 8L3 13L2 13L2 8Z" />
-
    <path d="M7 3L8 3L8 4L7 4L7 3Z" />
-
    <path d="M6 2L7 2L7 3L6 3L6 2Z" />
-
    <path d="M8 4L9 4L9 5L8 5L8 4Z" />
-
    <path d="M10 4L11 4L11 5L10 5L10 4Z" />
-
    <path d="M10 6L11 6L11 7L10 7L10 6Z" />
-
    <path d="M11 6L12 6L12 7L11 7L11 6Z" />
-
    <path d="M11 7L12 7L12 8L11 8L11 7Z" />
-
    <path d="M12 7L13 7L13 8L12 8L12 7Z" />
-
    <path d="M12 8L13 8L13 9L12 9L12 8Z" />
-
    <path d="M13 8L14 8L14 9L13 9L13 8Z" />
-
    <path d="M7 8L8 8L8 9L7 9L7 8Z" />
-
    <path d="M6 8L7 8L7 9L6 9L6 8Z" />
-
    <path d="M8 7L9 7L9 8L8 8L8 7Z" />
-
    <path d="M10 6L11 6L11 7L10 7L10 6Z" />
-
    <path d="M9 5L11 5L11 7L9 7L9 5Z" />
-
    <path d="M11 4L12 4L12 5L11 5L11 4Z" />
-
    <path d="M12 3L13 3L13 4L12 4L12 3Z" />
-
    <path d="M11 3L12 3L12 4L11 4L11 3Z" />
-
    <path d="M9 4L10 4L10 5L9 5L9 4Z" />
-
    <path d="M8 6L12 6L12 7L8 7L8 6Z" />
-
    <path d="M8 6L9 6L9 7L8 7L8 6Z" />
-
    <path d="M7 7L8 7L8 8L7 8L7 7Z" />
-
    <path d="M7 2L8 2L8 3L7 3L7 2Z" />
-
    <path d="M8 3L9 3L9 4L8 4L8 3Z" />
-
    <path d="M2 4L3 4L3 8L2 8L2 4Z" />
-
    <path d="M12 10L13 10L13 11L12 11L12 10Z" />
-
    <path d="M6 2L7 2L7 3L6 3L6 2Z" />
-
    <path d="M4 2L5 2L5 3L4 3L4 2Z" />
+
    <path
+
      d="M13.5 2.5V11.5H10.207L8 13.707L5.79297 11.5H2.5V2.5H13.5ZM3.5 10.5H6.20703L8 12.293L9.79297 10.5H12.5V3.5H3.5V10.5Z" />
+
    <path
+
      d="M10.3535 5.35352L8.70605 7.00098L10.3516 8.64648L9.64453 9.35352L7.99902 7.70801L6.35352 9.35352L5.64648 8.64648L7.29199 7.00098L5.64648 5.35547L6.35352 4.64844L7.99902 6.29395L9.64648 4.64648L10.3535 5.35352Z" />
+
  {:else if name === "commit"}
+
    <path
+
      d="M8 4.5C9.7632 4.5 11.2212 5.8039 11.4639 7.5H15V8.5H11.4639C11.2212 10.1961 9.7632 11.5 8 11.5C6.2368 11.5 4.77879 10.1961 4.53613 8.5H1V7.5H4.53613C4.77879 5.8039 6.2368 4.5 8 4.5ZM8 5.5C6.61929 5.5 5.5 6.61929 5.5 8C5.5 9.38071 6.61929 10.5 8 10.5C9.38071 10.5 10.5 9.38071 10.5 8C10.5 6.61929 9.38071 5.5 8 5.5Z" />
  {:else if name === "copy"}
-
    <path d="M6.5 2H13.5V3H6.5V2Z" />
-
    <path d="M3.5 5H4.5V6H3.5V5Z" />
-
    <path d="M7.5 5H10.5V6H7.5V5Z" />
-
    <path d="M6.5 10H13.5V11H6.5V10Z" />
-
    <path d="M3.5 13H10.5V14H3.5V13Z" />
-
    <path d="M13.5 3L14.5 3V10L13.5 10L13.5 3Z" />
-
    <path d="M5.5 3L6.5 3L6.5 10L5.5 10V3Z" />
-
    <path d="M2.5 6H3.5L3.5 13H2.5V6Z" />
-
    <path d="M10.5 12H11.5V13H10.5V12Z" />
-
    <path d="M10.5 6L11.5 6V9H10.5L10.5 6Z" />
-
  {:else if name === "cross"}
-
    <path d="M5.00003 11V12H4.00003L4.00003 11H5.00003Z" />
-
    <path d="M6.00003 10L6.00003 11H5.00003L5.00003 10H6.00003Z" />
-
    <path d="M7.00003 9V10H6.00003L6.00003 9L7.00003 9Z" />
-
    <path d="M8.00003 7V8H7.00003V7H8.00003Z" />
-
    <path d="M10 6V7H9.00003V6H10Z" />
-
    <path d="M11 5V6L10 6V5H11Z" />
-
    <path d="M12 4V5L11 5V4L12 4Z" />
-
    <path d="M10 10V11H11V10H10Z" />
-
    <path d="M11 11V12H12V11H11Z" />
-
    <path d="M9.00003 9V10H10V9H9.00003Z" />
-
    <path d="M8.00003 8L8.00003 9H9.00003V8L8.00003 8Z" />
-
    <path d="M8.00003 7V8L9.00003 8V7L8.00003 7Z" />
-
    <path d="M7.00003 7L7.00003 9H8.00003L8.00003 7H7.00003Z" />
-
    <path d="M6.00003 6L6.00003 7H7.00003L7.00003 6H6.00003Z" />
-
    <path d="M5.00003 5V6L6.00003 6V5L5.00003 5Z" />
-
    <path d="M4.00003 4L4.00003 5L5.00003 5L5.00003 4L4.00003 4Z" />
-
  {:else if name === "cross-double"}
-
    <path d="M4 11V12H3L3 11H4Z" />
-
    <path d="M5 10L5 11H4L4 10H5Z" />
-
    <path d="M6 9V10H5L5 9L6 9Z" />
-
    <path d="M7 7V8H6V7H7Z" />
-
    <path d="M10 5V6L9 6V5H10Z" />
-
    <path d="M11 4V5L10 5V4L11 4Z" />
-
    <path d="M9 10V11H10V10H9Z" />
-
    <path d="M10 11V12H11V11H10Z" />
-
    <path d="M8 9V10H9V9H8Z" />
-
    <path d="M7 8L7 9H8V8L7 8Z" />
-
    <path d="M5 6L5 7H6L6 6H5Z" />
-
    <path d="M4 5V6L5 6V5L4 5Z" />
-
    <path d="M3 4L3 5L4 5L4 4L3 4Z" />
-
    <path d="M6 11V12H5L5 11H6Z" />
-
    <path d="M7 10L7 11H6L6 10H7Z" />
-
    <path d="M9 7V8H8V7H9Z" />
-
    <path d="M11 6V7H10V6H11Z" />
-
    <path d="M12 5V6L11 6V5H12Z" />
-
    <path d="M13 4V5L12 5V4L13 4Z" />
-
    <path d="M11 10V11H12V10H11Z" />
-
    <path d="M12 11V12H13V11H12Z" />
-
    <path d="M10 9V10H11V9H10Z" />
-
    <path d="M9 8L9 9H10V8L9 8Z" />
-
    <path d="M7 6L7 7H8L8 6H7Z" />
-
    <path d="M6 5V6L7 6V5L6 5Z" />
-
    <path d="M5 4L5 5L6 5L6 4L5 4Z" />
+
    <path d="M10.5 5.5V14.5H1.5V5.5H10.5ZM2.5 13.5H9.5V6.5H2.5V13.5Z" />
+
    <path d="M13.5 2.5V11.5H10V10.5H12.5V3.5H5.5V6H4.5V2.5H13.5Z" />
+
  {:else if name === "cursor"}
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M4.62672 2.32908C5.22095 2.00257 5.94947 2.15788 6.60157 2.60621C6.61264 2.61382 6.62339 2.62186 6.6338 2.63034L12.6308 7.50811C13.4117 8.14331 13.0756 9.40302 12.082 9.56471L10.5513 9.8138L11.7818 12.1598C12.0808 12.7299 11.8615 13.4344 11.2919 13.7341C11.2915 13.7344 11.291 13.7346 11.2905 13.7349L10.3375 14.2411L10.3352 14.2424C9.76458 14.5417 9.05939 14.3217 8.7601 13.7511L7.53168 11.4091L6.48794 12.5074C5.79339 13.2383 4.56132 12.8035 4.47941 11.7986L3.85193 4.09987C3.85193 4.09991 3.85192 4.09982 3.85193 4.09987C3.79086 3.35282 4.02701 2.6586 4.62672 2.32908ZM5.10827 3.2055C4.98313 3.27426 4.8056 3.49265 4.8486 4.01834L5.47611 11.7173C5.48781 11.8609 5.66382 11.923 5.76304 11.8186L7.28772 10.2141C7.39881 10.0972 7.55945 10.0411 7.71916 10.0633C7.87888 10.0856 8.01806 10.1835 8.09296 10.3263L9.64568 13.2866C9.68829 13.3679 9.78851 13.3993 9.86988 13.3572C9.8696 13.3574 9.87015 13.3571 9.86988 13.3572L10.8237 12.8505L10.826 12.8493C10.9075 12.8065 10.939 12.7058 10.8962 12.6243L9.34349 9.66396C9.26883 9.52163 9.26715 9.35209 9.33896 9.2083C9.41078 9.06452 9.54733 8.96402 9.70597 8.9382L11.9214 8.57769C12.0633 8.55459 12.1114 8.37463 11.9998 8.28389L6.01914 3.41941C5.5398 3.09522 5.2375 3.13449 5.10827 3.2055Z" />
  {: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" />
+
    <path d="M6.5 2.5V7.5H1.5V2.5H6.5ZM2.5 6.5H5.5V3.5H2.5V6.5Z" />
+
    <path d="M14.5 2.5V13.5H7.5V2.5H14.5ZM8.5 12.5H13.5V3.5H8.5V12.5Z" />
+
    <path d="M6.5 8.5V13.5H1.5V8.5H6.5ZM2.5 12.5H5.5V9.5H2.5V12.5Z" />
+
  {:else if name === "device"}
+
    <path
+
      d="M13.5 10.5L2.5 10.5L2.5 2.5L13.5 2.5L13.5 10.5ZM3.5 8.5L3.5 9.5L12.5 9.5L12.5 8.5L3.5 8.5ZM3.5 3.5L3.5 7.5L12.5 7.5L12.5 3.5L3.5 3.5Z" />
+
    <path d="M7.5 9.5H8.5V12.5H10.5V13.5H5.5V12.5H7.5V9.5Z" />
  {: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" />
+
    <path
+
      d="M13.5 1.5V14.5H2.5V1.5H13.5ZM3.5 13.5H12.5V2.5H3.5V13.5ZM10 10.5V11.5H6V10.5H10ZM8.5 4V5.5H10V6.5H8.5V8H7.5V6.5H6V5.5H7.5V4H8.5Z" />
+
  {:else if name === "disconnect"}
+
    <path
+
      d="M2.5 2.5L9.5 2.5L9.5 5.5L8.5 5.5L8.5 3.5L3.5 3.5L3.5 12.5L8.5 12.5L8.5 10.5L9.5 10.5L9.5 13.5L2.5 13.5L2.5 2.5Z" />
+
    <path
+
      d="M6 7.5L12.793 7.5L10.6465 5.35352L11.3535 4.64648L14.707 8L11.3535 11.3535L10.6465 10.6465L12.793 8.5L6 8.5L6 7.5Z" />
+
  {:else if name === "document"}
+
    <path
+
      d="M13.5 14.5L2.5 14.5L2.5 1.5L9.20703 1.5L13.5 5.79297L13.5 14.5ZM8.5 6.5L8.5 2.5L3.5 2.5L3.5 13.5L12.5 13.5L12.5 6.5L8.5 6.5ZM9.5 3.20703L9.5 5.5L11.793 5.5L9.5 3.20703Z" />
+
  {:else if name === "download"}
+
    <path
+
      d="M7.5 2.5H8.5V9.79297L11 7.29297L11.707 8L8 11.707L4.29297 8L5 7.29297L7.5 9.79297V2.5Z" />
+
    <path
+
      d="M3.5 11L3.5 13.5L12.5 13.5L12.5 11L13.5 11L13.5 14.5L2.5 14.5L2.5 11L3.5 11Z" />
+
  {:else if name === "edit"}
+
    <path
+
      d="M14.707 5L6.20703 13.5H2.5V9.79297L11 1.29297L14.707 5ZM3.5 10.207V12.5H5.79297L11.293 7L9 4.70703L3.5 10.207ZM9.70703 4L12 6.29297L13.293 5L11 2.70703L9.70703 4Z" />
  {:else if name === "ellipsis"}
-
    <path d="M2 7H4V9H2V7Z" />
-
    <path d="M7 7H9V9H7V7Z" />
-
    <path d="M12 7H14V9H12V7Z" />
-
  {:else if name === "expand"}
-
    <path d="M9 1.5H8V2.5H9V1.5Z" />
-
    <path d="M10 2.5H9V3.5H10V2.5Z" />
-
    <path d="M7 2.5V5.5H9V2.5H7Z" />
-
    <path d="M5 3.5V4.5H11V3.5H5Z" />
-
    <path d="M6 2.5V3.5H7V2.5H6Z" />
-
    <path d="M7 1.5V2.5H8V1.5H7Z" />
-
    <path d="M7 14.5H8V13.5H7V14.5Z" />
-
    <path d="M6 13.5H7V12.5H6V13.5Z" />
-
    <path d="M9 13.5V10.5H7V13.5H9Z" />
-
    <path d="M11 12.5V11.5H5V12.5H11Z" />
-
    <path d="M10 13.5V12.5H9H8V13.5H9H10Z" />
-
    <path d="M9 14.5V13.5H8V14.5H9Z" />
-
    <path d="M2 7.5H14V8.5H2V7.5Z" />
-
  {:else if name === "expand-panel"}
-
    <path d="M2 3.00002H3V13H2V3.00002Z" />
-
    <path d="M13 3.00002H14V5.00002H13V3.00002Z" />
-
    <path d="M13 11H14V13H13V11Z" />
-
    <path d="M5 2.00002H6V14H5V2.00002Z" />
-
    <path d="M3 2.00002H13V3.00002H3L3 2.00002Z" />
-
    <path d="M3 13H13V14H3L3 13Z" />
-
    <path d="M14 9.00002V8.00002H13V9.00002H14Z" />
-
    <path d="M13 10V9.00002H12V10H13Z" />
-
    <path d="M13 7.00002V8.00002H12V7.00002H13Z" />
-
    <path d="M13 7.00002L8 7.00002V9.00002L13 9.00002V7.00002Z" />
-
    <path d="M12 5.00002H11V11H12V5.00002Z" />
-
    <path d="M13 6.00002H12V7.00002H13V6.00002Z" />
-
    <path d="M14 7.00002H13V8.00002H14V7.00002Z" />
+
    <path
+
      d="M9 8C9 8.55228 8.55228 9 8 9C7.44772 9 7 8.55228 7 8C7 7.44772 7.44772 7 8 7C8.55228 7 9 7.44772 9 8Z" />
+
    <path
+
      d="M13 8C13 8.55228 12.5523 9 12 9C11.4477 9 11 8.55228 11 8C11 7.44772 11.4477 7 12 7C12.5523 7 13 7.44772 13 8Z" />
+
    <path
+
      d="M5 8C5 8.55228 4.55228 9 4 9C3.44772 9 3 8.55228 3 8C3 7.44772 3.44772 7 4 7C4.55228 7 5 7.44772 5 8Z" />
+
  {:else if name === "ellipsis-vertical"}
+
    <path
+
      d="M9 8C9 8.55228 8.55228 9 8 9C7.44772 9 7 8.55228 7 8C7 7.44772 7.44772 7 8 7C8.55228 7 9 7.44772 9 8Z" />
+
    <path
+
      d="M9 12C9 12.5523 8.55228 13 8 13C7.44772 13 7 12.5523 7 12C7 11.4477 7.44772 11 8 11C8.55228 11 9 11.4477 9 12Z" />
+
    <path
+
      d="M9 4C9 4.55228 8.55228 5 8 5C7.44772 5 7 4.55228 7 4C7 3.44772 7.44772 3 8 3C8.55228 3 9 3.44772 9 4Z" />
+
  {:else if name === "emoji"}
+
    <path
+
      d="M13.5 8C13.5 4.96243 11.0376 2.5 8 2.5C4.96243 2.5 2.5 4.96243 2.5 8C2.5 11.0376 4.96243 13.5 8 13.5C11.0376 13.5 13.5 11.0376 13.5 8ZM14.5 8C14.5 11.5899 11.5899 14.5 8 14.5C4.41015 14.5 1.5 11.5899 1.5 8C1.5 4.41015 4.41015 1.5 8 1.5C11.5899 1.5 14.5 4.41015 14.5 8Z" />
+
    <path
+
      d="M11.2995 9.16699C11.1272 9.65421 10.8477 10.1021 10.4753 10.4746C9.81891 11.131 8.92795 11.5 7.9997 11.5C7.07155 11.4999 6.18139 11.1309 5.52509 10.4746C5.15257 10.102 4.87225 9.65437 4.69989 9.16699L5.64325 8.83301C5.76637 9.18123 5.96603 9.50144 6.23212 9.76758C6.70089 10.2363 7.33676 10.4999 7.9997 10.5C8.66274 10.5 9.29941 10.2364 9.76825 9.76758C10.0343 9.50148 10.234 9.18115 10.3571 8.83301L11.2995 9.16699Z" />
+
    <path
+
      d="M6.75 6.5C6.75 6.91421 6.41421 7.25 6 7.25C5.58579 7.25 5.25 6.91421 5.25 6.5C5.25 6.08579 5.58579 5.75 6 5.75C6.41421 5.75 6.75 6.08579 6.75 6.5Z" />
+
    <path
+
      d="M10.75 6.5C10.75 6.91421 10.4142 7.25 10 7.25C9.58579 7.25 9.25 6.91421 9.25 6.5C9.25 6.08579 9.58579 5.75 10 5.75C10.4142 5.75 10.75 6.08579 10.75 6.5Z" />
+
  {:else if name === "expand-out"}
+
    <path
+
      d="M8.70703 9L4.20703 12.5H7.5V13.5H2.5V8.5H3.5V11.793L7 8.29297L7.70703 9ZM13.5 7.5H12.5V4.20703L9 7.70703L8.29297 7L11.793 3.5H8.5V2.5H13.5V7.5Z" />
+
  {:else if name === "expand-vertical"}
+
    <path
+
      d="M7.5 2.20703L5.5 4.20703L4.79297 3.5L8 0.292969L11.207 3.5L10.5 4.20703L8.5 2.20703V6.5H7.5V2.20703Z" />
+
    <path
+
      d="M7.5 13.793L5.5 11.793L4.79297 12.5L8 15.707L11.207 12.5L10.5 11.793L8.5 13.793V9.5H7.5V13.793Z" />
+
    <path d="M14 7.5L14 8.5L2 8.5L2 7.5L14 7.5Z" />
+
  {:else if name === "explore"}
+
    <path
+
      d="M6.5 11.5C6.5 10.3954 5.60457 9.5 4.5 9.5C3.39543 9.5 2.5 10.3954 2.5 11.5C2.5 12.6046 3.39543 13.5 4.5 13.5C5.60457 13.5 6.5 12.6046 6.5 11.5ZM13.5 11.5C13.5 10.3954 12.6046 9.5 11.5 9.5C10.3954 9.5 9.5 10.3954 9.5 11.5C9.5 12.6046 10.3954 13.5 11.5 13.5C12.6046 13.5 13.5 12.6046 13.5 11.5ZM7.5 7H8.5V6H7.5V7ZM6.5 4.5C6.5 3.39543 5.60457 2.5 4.5 2.5C3.39543 2.5 2.5 3.39543 2.5 4.5V9.26758C3.03098 8.79156 3.73076 8.5 4.5 8.5C5.26924 8.5 5.96902 8.79156 6.5 9.26758V4.5ZM13.5 4.5C13.5 3.39543 12.6046 2.5 11.5 2.5C10.3954 2.5 9.5 3.39543 9.5 4.5V9.26758C10.031 8.79156 10.7308 8.5 11.5 8.5C12.2692 8.5 12.969 8.79156 13.5 9.26758V4.5ZM14.5 11.5C14.5 13.1569 13.1569 14.5 11.5 14.5C9.84315 14.5 8.5 13.1569 8.5 11.5V8H7.5V11.5C7.5 13.1569 6.15685 14.5 4.5 14.5C2.84315 14.5 1.5 13.1569 1.5 11.5V4.5C1.5 2.84315 2.84315 1.5 4.5 1.5C6.15685 1.5 7.5 2.84315 7.5 4.5V5H8.5V4.5C8.5 2.84315 9.84316 1.5 11.5 1.5C13.1568 1.5 14.5 2.84315 14.5 4.5V11.5Z" />
  {:else if name === "eye"}
-
    <path d="M10 5L8.00002 5V4L10 4V5Z" />
-
    <path d="M6.00002 11L8.00002 11L8.00002 12H6.00002L6.00002 11Z" />
-
    <path d="M7.00002 7L9.00002 7V6L7.00002 6V7Z" />
-
    <path d="M9.00001 9L7.00001 9V10L9.00001 10V9Z" />
-
    <path d="M4.00002 6V7H3.00002L3.00002 6H4.00002Z" />
-
    <path d="M12 10V9H13V10H12Z" />
-
    <path d="M12 6L10 6V5L12 5V6Z" />
-
    <path d="M4.00002 10L6.00002 10V11H4.00002L4.00002 10Z" />
-
    <path d="M6.00002 6L4.00002 6L4.00002 5L6.00002 5V6Z" />
-
    <path d="M10 10H12V11L10 11L10 10Z" />
-
    <path d="M12 7V6H13V7H12Z" />
-
    <path d="M4.00002 9V10H3.00002L3.00002 9H4.00002Z" />
-
    <path d="M8.00002 5L6.00002 5V4L8.00002 4V5Z" />
-
    <path d="M8.00002 11L10 11V12H8.00002L8.00002 11Z" />
-
    <path d="M6.00002 8V7L7.00002 7V8L6.00002 8Z" />
-
    <path d="M10 8V9H9.00001L9.00002 8H10Z" />
-
    <path d="M9.00002 8L9.00002 7L10 7V8H9.00002Z" />
-
    <path d="M7.00002 8L7.00001 9L6.00002 9V8L7.00002 8Z" />
-
    <path d="M14 7V8H13V7L14 7Z" />
-
    <path d="M2.00002 9L2.00002 8H3.00002V9L2.00002 9Z" />
-
    <path d="M3.00002 7L3.00002 8H2.00002L2.00002 7H3.00002Z" />
-
    <path d="M13 9V8H14V9L13 9Z" />
-
  {:else if name === "eye-closed"}
-
    <path d="M10 5L8 5V4L10 4V5Z" />
-
    <path d="M6 11H8V12H6V11Z" />
-
    <path d="M7 7H9L9 6L7 6V7Z" />
-
    <path d="M9 9L8 9V10H9V9Z" />
-
    <path d="M4 6V7H3L3 6H4Z" />
-
    <path d="M12 10L12 9L13 9L13 10L12 10Z" />
-
    <path d="M11 6H10L10 5L11 5V6Z" />
-
    <path d="M4 10H6L6 11L4 11L4 10Z" />
-
    <path d="M6 6H4V5L6 5L6 6Z" />
-
    <path d="M10 10L12 10V11H10L10 10Z" />
-
    <path d="M12 7V6L13 6L13 7L12 7Z" />
-
    <path d="M4 9V10L3 10L3 9L4 9Z" />
-
    <path d="M8 5L6 5L6 4L8 4V5Z" />
-
    <path d="M8 11L10 11V12L8 12V11Z" />
-
    <path d="M6 8V7H7V8L6 8Z" />
-
    <path d="M10 8V9H9V8L10 8Z" />
-
    <path d="M7 8L7 9H6V8L7 8Z" />
-
    <path d="M14 7V8H13V7L14 7Z" />
-
    <path d="M2 9L2 8H3L3 9H2Z" />
-
    <path d="M3 7L3 8H2L2 7H3Z" />
-
    <path d="M13 9V8H14L14 9H13Z" />
-
    <path d="M13 2L14 2V3H13V2Z" />
-
    <path d="M12 3L13 3V4H12V3Z" />
-
    <path d="M11 4L12 4L12 5L11 5L11 4Z" />
-
    <path d="M10 5L11 5V6H10L10 5Z" />
-
    <path d="M9 6L10 6V7H9L9 6Z" />
-
    <path d="M8 7L9 7V8L8 8V7Z" />
-
    <path d="M7 8H8V9L7 9L7 8Z" />
-
    <path d="M6 9H7L7 10L6 10L6 9Z" />
-
    <path d="M4 11L5 11L5 12H4L4 11Z" />
-
    <path d="M3 12H4V13H3V12Z" />
-
    <path d="M2 13H3L3 14H2V13Z" />
-
  {:else if name === "face"}
-
    <path d="M6 13H8V14H6V13Z" />
-
    <path d="M10 13L8 13V14L10 14V13Z" />
-
    <path d="M3 6L3 8H2L2 6H3Z" />
-
    <path d="M13 6V8H14V6H13Z" />
-
    <path d="M4 12H6V13L4 13V12Z" />
-
    <path d="M12 12H10L10 13H12V12Z" />
-
    <path d="M4 4V6H3L3 4H4Z" />
-
    <path d="M12 4V6L13 6V4H12Z" />
-
    <path d="M4 10L4 12H3L3 10H4Z" />
-
    <path d="M12 10V12L13 12V10H12Z" />
-
    <path d="M6 4L4 4L4 3L6 3V4Z" />
-
    <path d="M10 4L12 4V3L10 3V4Z" />
-
    <path d="M3 8L3 10H2L2 8H3Z" />
-
    <path d="M13 8V10L14 10V8H13Z" />
-
    <path d="M8 3L6 3V2L8 2V3Z" />
-
    <path d="M8 3L10 3L10 2L8 2V3Z" />
-
    <path d="M9 6H11V7H9V6Z" />
-
    <path d="M5 9H6V10H5V9Z" />
-
    <path d="M10 9H11V10H10V9Z" />
-
    <path d="M6 10H7V11H6L6 10Z" />
-
    <path d="M7 10L10 10L10 11H7V10Z" />
-
    <path d="M5 6H7V7H5V6Z" />
-
  {: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" />
+
    <path
+
      d="M8 3C10.2021 3 11.8273 4.24044 12.874 5.41797C13.4003 6.01012 13.793 6.60046 14.0547 7.04199C14.1857 7.26315 14.2848 7.44857 14.3516 7.58008C14.385 7.64588 14.4112 7.69851 14.4287 7.73535C14.4373 7.75347 14.4435 7.76802 14.4482 7.77832C14.4506 7.78349 14.4527 7.78795 14.4541 7.79102C14.4548 7.79255 14.4556 7.79393 14.4561 7.79492V7.7959L14.457 7.79688L14.5469 8L14.457 8.20312L14.4561 8.20508C14.4556 8.20607 14.4548 8.20745 14.4541 8.20898C14.4527 8.21205 14.4506 8.21651 14.4482 8.22168C14.4435 8.23198 14.4373 8.24653 14.4287 8.26465C14.4112 8.30149 14.385 8.35412 14.3516 8.41992C14.2848 8.55143 14.1857 8.73685 14.0547 8.95801C13.793 9.39954 13.4003 9.98988 12.874 10.582C11.8273 11.7596 10.2021 13 8 13C5.7979 13 4.17267 11.7596 3.12598 10.582C2.59966 9.98988 2.20697 9.39954 1.94531 8.95801C1.81425 8.73685 1.71524 8.55143 1.64844 8.41992C1.61502 8.35412 1.58875 8.30149 1.57129 8.26465C1.5627 8.24653 1.55649 8.23198 1.55176 8.22168C1.54939 8.21651 1.54728 8.21205 1.5459 8.20898C1.54521 8.20745 1.54439 8.20607 1.54395 8.20508V8.2041L1.54297 8.20312L1.45312 8L1.54297 7.79688L1.54395 7.7959V7.79492C1.54439 7.79393 1.54521 7.79255 1.5459 7.79102C1.54728 7.78795 1.54939 7.78349 1.55176 7.77832C1.55649 7.76802 1.5627 7.75347 1.57129 7.73535C1.58875 7.69851 1.61502 7.64588 1.64844 7.58008C1.71524 7.44857 1.81425 7.26315 1.94531 7.04199C2.20697 6.60046 2.59966 6.01012 3.12598 5.41797C4.17267 4.24044 5.7979 3 8 3ZM8 4C6.20223 4 4.82733 5.00965 3.87402 6.08203C3.40052 6.61472 3.04299 7.14962 2.80469 7.55176C2.69678 7.73386 2.61356 7.88751 2.55566 8C2.61356 8.11249 2.69678 8.26614 2.80469 8.44824C3.04299 8.85038 3.40052 9.38528 3.87402 9.91797C4.82733 10.9904 6.20223 12 8 12C9.79777 12 11.1727 10.9904 12.126 9.91797C12.5995 9.38528 12.957 8.85038 13.1953 8.44824C13.3031 8.26635 13.3855 8.11246 13.4434 8C13.3855 7.88754 13.3031 7.73365 13.1953 7.55176C12.957 7.14962 12.5995 6.61472 12.126 6.08203C11.1727 5.00965 9.79777 4 8 4ZM9.5 8C9.5 7.17157 8.82843 6.5 8 6.5C7.17157 6.5 6.5 7.17157 6.5 8C6.5 8.82843 7.17157 9.5 8 9.5C8.82843 9.5 9.5 8.82843 9.5 8ZM10.5 8C10.5 9.38071 9.38071 10.5 8 10.5C6.61929 10.5 5.5 9.38071 5.5 8C5.5 6.61929 6.61929 5.5 8 5.5C9.38071 5.5 10.5 6.61929 10.5 8Z" />
+
  {:else if name === "eye-slash"}
+
    <path
+
      d="M13.3535 3.35352L3.35352 13.3535L2.64648 12.6465L12.6465 2.64648L13.3535 3.35352Z" />
+
    <path
+
      d="M12.9629 5.41797C13.4988 6.01003 13.8986 6.60048 14.165 7.04199C14.2985 7.26315 14.3997 7.44857 14.4678 7.58008C14.5018 7.64578 14.5281 7.69852 14.5459 7.73535C14.5546 7.75347 14.5616 7.76802 14.5664 7.77832C14.5688 7.78346 14.5709 7.78796 14.5723 7.79102C14.573 7.79254 14.5738 7.79394 14.5742 7.79492V7.7959L14.5752 7.79688L14.667 8L14.5752 8.20312L14.5723 8.20898C14.5709 8.21204 14.5688 8.21654 14.5664 8.22168C14.5616 8.23198 14.5546 8.24653 14.5459 8.26465C14.5281 8.30148 14.5018 8.35422 14.4678 8.41992C14.3997 8.55143 14.2985 8.73685 14.165 8.95801C13.8986 9.39952 13.4988 9.98997 12.9629 10.582C11.897 11.7595 10.2423 13 8 13C7.1153 13 6.32281 12.8051 5.62109 12.499L6.39355 11.7266C6.88615 11.8984 7.42143 12 8 12C9.83058 12 11.2304 10.9903 12.2012 9.91797C12.6833 9.38535 13.0474 8.85037 13.29 8.44824C13.3998 8.26635 13.484 8.11246 13.543 8C13.484 7.88754 13.3998 7.73365 13.29 7.55176C13.0474 7.14963 12.6833 6.61465 12.2012 6.08203C12.1755 6.05364 12.1484 6.02633 12.1221 5.99805L12.8369 5.2832C12.8795 5.32844 12.922 5.37285 12.9629 5.41797ZM8 3C8.8842 3 9.6765 3.19417 10.3779 3.5L9.60547 4.27246C9.1132 4.10091 8.57811 4 8 4C6.16942 4 4.76957 5.0097 3.79883 6.08203C3.31672 6.61465 2.95263 7.14963 2.70996 7.55176C2.60008 7.73386 2.51501 7.88751 2.45605 8C2.51501 8.11249 2.60008 8.26614 2.70996 8.44824C2.95263 8.85037 3.31672 9.38535 3.79883 9.91797C3.82422 9.94602 3.85097 9.97303 3.87695 10.001L3.16211 10.7158C3.11986 10.6709 3.07765 10.6268 3.03711 10.582C2.50124 9.98997 2.10141 9.39952 1.83496 8.95801C1.70151 8.73685 1.60025 8.55143 1.53223 8.41992C1.49825 8.35422 1.47188 8.30148 1.4541 8.26465C1.44536 8.24653 1.43841 8.23198 1.43359 8.22168C1.43119 8.21654 1.42914 8.21204 1.42773 8.20898C1.42703 8.20746 1.42623 8.20606 1.42578 8.20508V8.2041L1.4248 8.20312L1.33301 8L1.4248 7.79688L1.42578 7.7959V7.79492C1.42623 7.79394 1.42703 7.79254 1.42773 7.79102C1.42914 7.78796 1.43119 7.78346 1.43359 7.77832C1.43841 7.76802 1.44536 7.75347 1.4541 7.73535C1.47188 7.69852 1.49825 7.64578 1.53223 7.58008C1.60025 7.44857 1.70151 7.26315 1.83496 7.04199C2.10141 6.60048 2.50124 6.01003 3.03711 5.41797C4.10295 4.24049 5.75771 3 8 3Z" />
+
    <path
+
      d="M10.4717 7.64844C10.4879 7.76347 10.5 7.88049 10.5 8C10.5 9.38071 9.38071 10.5 8 10.5C7.88049 10.5 7.76347 10.4879 7.64844 10.4717L10.4717 7.64844ZM8 5.5C8.11913 5.5 8.2359 5.51123 8.35059 5.52734L5.52734 8.35059C5.51123 8.2359 5.5 8.11913 5.5 8C5.5 6.61929 6.61929 5.5 8 5.5Z" />
  {:else if name === "filter"}
-
    <path d="M3 2L13 2V3L3 3V2Z" />
-
    <path d="M13 3L14 3V4L13 4V3Z" />
-
    <path d="M12 4L13 4V5H12V4Z" />
-
    <path d="M11 5L12 5V7H11V5Z" />
-
    <path d="M5 5H4V7H5V5Z" />
-
    <path d="M10 7L11 7V9H10V7Z" />
-
    <path d="M6 7L5 7L5 9H6V7Z" />
-
    <path d="M9 9L10 9L10 13L9 13L9 9Z" />
-
    <path d="M7 9H6V12H7V9Z" />
-
    <path d="M2 3L3 3L3 4H2L2 3Z" />
-
    <path d="M3 4H4L4 5L3 5L3 4Z" />
-
    <path d="M8 13H9L9 14H8V13Z" />
-
    <path d="M6 4L12 4V5L6 5V4Z" />
-
    <path d="M7 12H8L8 13H7L7 12Z" />
-
  {:else if name === "folder-closed"}
-
    <path d="M8 4L13 4V5L8 5V4Z" />
-
    <path d="M3 6L13 6V7L3 7L3 6Z" />
-
    <path d="M7 3H8V4L7 4V3Z" />
-
    <path d="M3 13L13 13V14L3 14V13Z" />
-
    <path d="M3 2L7 2V3L3 3V2Z" />
-
    <path d="M14 5L14 13H13L13 5L14 5Z" />
-
    <path d="M3 3L3 13H2L2 3L3 3Z" />
+
    <path d="M14 4.5V5.5H2V4.5H14Z" />
+
    <path d="M12 7.5V8.5H4V7.5H12Z" />
+
    <path d="M10 10.5V11.5H6V10.5H10Z" />
+
  {:else if name === "folder"}
+
    <path
+
      d="M7.20703 2.5L9.20703 4.5H14.5V14.5H1.5V2.5H7.20703ZM2.5 13.5H13.5V5.5H2.5V13.5ZM2.5 4.5H7.79297L6.79297 3.5H2.5V4.5Z" />
  {:else if name === "folder-open"}
-
    <path d="M7.5 4L12.5 4V5L7.5 5V4Z" />
-
    <path d="M4.5 7L12.5 7V8L4.5 8L4.5 7Z" />
-
    <path d="M6.5 3H7.5V4L6.5 4V3Z" />
-
    <path d="M2.5 13L11.5 13V14L2.5 14L2.5 13Z" />
-
    <path d="M2.5 2L6.5 2L6.5 3L2.5 3V2Z" />
-
    <path d="M13.5 10V12H12.5L12.5 10H13.5Z" />
-
    <path d="M13.5 5V8H12.5L12.5 5L13.5 5Z" />
-
    <path d="M2.5 11L2.5 13H1.5L1.5 11H2.5Z" />
-
    <path d="M2.5 3L2.5 12H1.5L1.5 3L2.5 3Z" />
-
    <path d="M3.5 10L3.5 12H2.5L2.5 10H3.5Z" />
-
    <path d="M4.5 8V10H3.5L3.5 8H4.5Z" />
-
    <path d="M14.5 8L14.5 10H13.5L13.5 8H14.5Z" />
-
    <path d="M11.5 12H12.5V13H11.5V12Z" />
+
    <path
+
      d="M7.20703 2.5L9.20703 4.5H14.5V6.5H15.6631L13.377 14.5H1.5V2.5H7.20703ZM2.70898 13.5H12.623L14.3369 7.5H4.85254L2.70898 13.5ZM2.5 11.1133L4.0293 6.83203L4.14746 6.5H13.5V5.5H8.79297L6.79297 3.5H2.5V11.1133Z" />
+
  {:else if name === "fullscreen"}
+
    <path d="M6 1.5V2.5H2.5V6H1.5V1.5H6Z" />
+
    <path d="M10 14.5L10 13.5L13.5 13.5L13.5 10L14.5 10L14.5 14.5L10 14.5Z" />
+
    <path d="M1.5 10L2.5 10L2.5 13.5L6 13.5L6 14.5L1.5 14.5L1.5 10Z" />
+
    <path d="M14.5 6L13.5 6L13.5 2.5L10 2.5L10 1.5L14.5 1.5L14.5 6Z" />
+
  {:else if name === "git"}
+
    <path
+
      d="M9 7C9 6.44772 8.55228 6 8 6C7.44772 6 7 6.44772 7 7C7 7.55228 7.44772 8 8 8C8.55228 8 9 7.55228 9 7ZM10 7C10 8.10457 9.10457 9 8 9C6.89543 9 6 8.10457 6 7C6 5.89543 6.89543 5 8 5C9.10457 5 10 5.89543 10 7Z" />
+
    <path
+
      d="M15.7783 8.00023L8 15.7785L0.22168 8.00023L8 0.221909L15.7783 8.00023ZM5.6709 3.9641L7.35352 5.64671L6.64648 6.35374L4.96387 4.67113L1.63574 8.00023L8 14.3645L14.3643 8.00023L8 1.63597L5.6709 3.9641ZM8.5 8.50023V12.0002H7.5V8.50023H8.5ZM11.3535 9.64671L10.6465 10.3537L8.64648 8.35374L9.35352 7.64671L11.3535 9.64671Z" />
+
  {:else if name === "guide"}
+
    <path
+
      d="M13.5 8C13.5 4.96243 11.0376 2.5 8 2.5C4.96243 2.5 2.5 4.96243 2.5 8C2.5 11.0376 4.96243 13.5 8 13.5C11.0376 13.5 13.5 11.0376 13.5 8ZM14.5 8C14.5 11.5899 11.5899 14.5 8 14.5C4.41015 14.5 1.5 11.5899 1.5 8C1.5 4.41015 4.41015 1.5 8 1.5C11.5899 1.5 14.5 4.41015 14.5 8Z" />
+
    <path d="M8.5 6.25V10.5H9.5V11.5H6.5V10.5H7.5V7.25H6.75V6.25H8.5Z" />
+
    <path
+
      d="M8.625 4.625C8.625 4.97018 8.34518 5.25 8 5.25C7.65482 5.25 7.375 4.97018 7.375 4.625C7.375 4.27982 7.65482 4 8 4C8.34518 4 8.625 4.27982 8.625 4.625Z" />
  {:else if name === "help"}
-
    <path d="M9 12H8L8 14H9L9 12Z" />
-
    <path d="M8 12H7V14H8L8 12Z" />
-
    <path d="M11 5L11 7H12V5H11Z" />
-
    <path d="M11 5L11 7H12V5H11Z" />
-
    <path d="M10 5L10 7L11 7L11 5L10 5Z" />
-
    <path d="M5 4L5 6H4L4 4H5Z" />
-
    <path d="M6 4V6H5L5 4H6Z" />
-
    <path d="M11 4V5H12V4H11Z" />
-
    <path d="M10 4V5L11 5V4L10 4Z" />
-
    <path d="M9 8V9H10V8H9Z" />
-
    <path d="M8 9V10H9V9L8 9Z" />
-
    <path d="M8 10V11H9V10H8Z" />
-
    <path d="M6 4H5L5 3L6 3L6 4Z" />
-
    <path d="M9 4L11 4V3L9 3V4Z" />
-
    <path d="M11 6V7H12V6H11Z" />
-
    <path d="M10 6V7L11 7V6L10 6Z" />
-
    <path d="M9 7L9 8H10L10 7H9Z" />
-
    <path d="M8 8V9L9 9V8H8Z" />
-
    <path d="M7 9V10L8 10V9L7 9Z" />
-
    <path d="M7 10L7 11L8 11V10L7 10Z" />
-
    <path d="M10 7L10 8H11V7L10 7Z" />
-
    <path d="M7 3L6 3L6 2L7 2V3Z" />
-
    <path d="M7 4H6L6 3L7 3V4Z" />
-
    <path d="M7 3L9 3V2L7 2V3Z" />
-
    <path d="M7 4L9 4V3L7 3V4Z" />
+
    <path
+
      d="M13.5 8C13.5 4.96243 11.0376 2.5 8 2.5C4.96243 2.5 2.5 4.96243 2.5 8C2.5 11.0376 4.96243 13.5 8 13.5C11.0376 13.5 13.5 11.0376 13.5 8ZM14.5 8C14.5 11.5899 11.5899 14.5 8 14.5C4.41015 14.5 1.5 11.5899 1.5 8C1.5 4.41015 4.41015 1.5 8 1.5C11.5899 1.5 14.5 4.41015 14.5 8Z" />
+
    <path
+
      d="M8.625 11.625C8.625 11.9702 8.34518 12.25 8 12.25C7.65482 12.25 7.375 11.9702 7.375 11.625C7.375 11.2798 7.65482 11 8 11C8.34518 11 8.625 11.2798 8.625 11.625Z" />
+
    <path
+
      d="M9.63379 6.5C9.63379 5.89312 9.41336 5.5453 9.13867 5.33496C8.84286 5.10866 8.42725 5 8 5C7.57445 5 7.20236 5.10724 6.94531 5.32129C6.7043 5.52213 6.5 5.87024 6.5 6.5H5.5C5.5 5.62992 5.79588 4.97787 6.30469 4.55371C6.79763 4.14292 7.42571 4 8 4C8.57275 4 9.22492 4.14134 9.74609 4.54004C10.2882 4.95474 10.6338 5.60725 10.6338 6.5C10.6338 7.32703 10.1246 7.91638 9.6123 8.29102C9.24736 8.55787 8.84058 8.74663 8.5 8.86816V10H7.5V8.10938L7.87891 8.01465C8.16659 7.9427 8.63623 7.76595 9.02148 7.48438C9.40947 7.20067 9.63379 6.87287 9.63379 6.5Z" />
  {:else if name === "home"}
-
    <path d="M7 1.50003H9V2.50003H7V1.50003Z" />
-
    <path d="M6 2.50003L7 2.50003V3.50003H6V2.50003Z" />
-
    <path d="M7 3.50003H8V4.50003H7V3.50003Z" />
-
    <path d="M9 2.50003L10 2.50003V3.50003H9V2.50003Z" />
-
    <path d="M8 3.50003L9 3.50003V4.50003H8V3.50003Z" />
-
    <path d="M10 3.50003L11 3.50003V4.50003H10L10 3.50003Z" />
-
    <path d="M9 4.50003L10 4.50003V5.50003H9L9 4.50003Z" />
-
    <path d="M11 4.50003L12 4.50003V5.50003H11V4.50003Z" />
-
    <path d="M10 5.50003L11 5.50003V6.50003H10V5.50003Z" />
-
    <path d="M12 5.50003H13V6.50003H12V5.50003Z" />
-
    <path d="M11 6.50003L12 6.50003V7.50003H11V6.50003Z" />
-
    <path d="M13 6.50003L14 6.50003V7.50003H13V6.50003Z" />
-
    <path d="M12 6.50003H13V7.50003H12V6.50003Z" />
-
    <path d="M12 7.50003H13V13.5H12V7.50003Z" />
-
    <path d="M3 7.50003H4V13.5H3V7.50003Z" />
-
    <path d="M5 3.50003L6 3.50003V4.50003H5V3.50003Z" />
-
    <path d="M6 4.50003L7 4.50003L7 5.50003H6V4.50003Z" />
-
    <path d="M4 4.50003H5V5.50003H4V4.50003Z" />
-
    <path d="M5 5.50003L6 5.50003L6 6.50003H5L5 5.50003Z" />
-
    <path d="M3 5.50003H4V6.50003H3V5.50003Z" />
-
    <path d="M3 6.50003L5 6.50003V7.50003L3 7.50003L3 6.50003Z" />
-
    <path d="M2 6.50003H3L3 7.50003L2 7.50003V6.50003Z" />
-
    <path d="M3 7.50003H4V8.50003H3V7.50003Z" />
-
    <path d="M4 13.5H7V14.5H4L4 13.5Z" />
-
    <path d="M9 13.5H12V14.5H9V13.5Z" />
-
    <path d="M6 10.5H7V13.5H6V10.5Z" />
-
    <path d="M7 9.50003H9V10.5L7 10.5L7 9.50003Z" />
-
    <path d="M9 10.5L10 10.5V13.5H9L9 10.5Z" />
+
    <path
+
      d="M13.3125 5.60938L13.5 5.75977V13.5H2.5V5.75977L2.6875 5.60938L8 1.35938L13.3125 5.60938ZM9.5 9C9.5 8.17157 8.82843 7.5 8 7.5C7.17157 7.5 6.5 8.17157 6.5 9V12.5H9.5V9ZM10.5 12.5H12.5V6.24023L8 2.64062L3.5 6.24023V12.5H5.5V9C5.5 7.61929 6.61929 6.5 8 6.5C9.38071 6.5 10.5 7.61929 10.5 9V12.5Z" />
  {:else if name === "hourglass"}
-
    <path d="M13 14H3V13H13V14Z" />
-
    <path d="M3 13H2V12H3V13Z" />
-
    <path d="M14 13H13V12H14V13Z" />
-
    <path d="M4 12H3V11H4V12Z" />
-
    <path d="M11 12H5V11H6V10H10V11H11V12Z" />
-
    <path d="M13 12H12V11H13V12Z" />
-
    <path d="M5 11H4V10H5V11Z" />
-
    <path d="M12 11H11V10H12V11Z" />
-
    <path d="M6 10H5V9H6V10Z" />
-
    <path d="M11 10H10V9H11V10Z" />
-
    <path d="M7 9H6V7H7V9Z" />
-
    <path d="M10 9H9V7H10V9Z" />
-
    <path d="M6 7H5V6H6V7Z" />
-
    <path d="M10 6H9V7H7V6H6V5H10V6Z" />
-
    <path d="M11 7H10V6H11V7Z" />
-
    <path d="M5 6H4V5H5V6Z" />
-
    <path d="M12 6H11V5H12V6Z" />
-
    <path d="M4 5H3V4H4V5Z" />
-
    <path d="M13 5H12V4H13V5Z" />
-
    <path d="M3 4H2V3H3V4Z" />
-
    <path d="M14 4H13V3H14V4Z" />
-
    <path d="M13 3H3V2H13V3Z" />
+
    <path
+
      d="M13.5 1.5V2.5H12.5V5.25L8.83301 8L12.5 10.75V13.5H13.5V14.5H2.5V13.5H3.5V10.75L7.16602 8L3.5 5.25V2.5H2.5V1.5H13.5ZM4.5 11.25V13.5H11.5V11.25L8 8.625L4.5 11.25ZM4.5 4.74902L8 7.37402L11.5 4.74902V2.5H4.5V4.74902Z" />
  {: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 === "info"}
-
    <path d="M10 13V14L6 14V13L10 13Z" />
-
    <path d="M4 12H6L6 13L4 13L4 12Z" />
-
    <path d="M3 10H4L4 12L3 12L3 10Z" />
-
    <path d="M3 6L3 10H2L2 6H3Z" />
-
    <path d="M4 4L4 6H3L3 4L4 4Z" />
-
    <path d="M6 3L6 4L4 4L4 3L6 3Z" />
-
    <path d="M10 3L6 3V2L10 2V3Z" />
-
    <path d="M12 4L10 4V3L12 3V4Z" />
-
    <path d="M13 6L12 6V4H13V6Z" />
-
    <path d="M13 10L13 6H14L14 10H13Z" />
-
    <path d="M12 12V10L13 10V12L12 12Z" />
-
    <path d="M12 12H10V13H12V12Z" />
-
    <path d="M9 7V10L10 10V11L6 11L6 10H7V8H6V7L9 7Z" />
-
    <path d="M9 4L7 4L7 6L9 6V4Z" />
+
    <path
+
      d="M14.5 1.5V14.5H1.5V1.5H14.5ZM2.5 13.5H13.5V9.5H11.3086L10.4473 11.2236L10.3086 11.5H5.69141L5.55273 11.2236L4.69141 9.5H2.5V13.5ZM2.5 8.5H5.30859L5.44727 8.77637L6.30859 10.5H9.69141L10.5527 8.77637L10.6914 8.5H13.5V2.5H2.5V8.5Z" />
  {:else if name === "issue"}
-
    <path d="M6 13H8V14H6V13Z" />
-
    <path d="M10 13L8 13V14L10 14V13Z" />
-
    <path d="M3 5.99999L3 7.99999H2L2 5.99999H3Z" />
-
    <path d="M13 5.99999V7.99999H14V5.99999H13Z" />
-
    <path d="M4 12H6V13L4 13V12Z" />
-
    <path d="M12 12H10V13L12 13V12Z" />
-
    <path d="M4 3.99999V5.99999H3L3 3.99999H4Z" />
-
    <path d="M12 3.99999V5.99999L13 5.99999V3.99999H12Z" />
-
    <path d="M4 9.99999L4 12H3L3 9.99999H4Z" />
-
    <path d="M12 9.99998V12H13V9.99998H12Z" />
-
    <path d="M6 3.99998L4 3.99999L4 2.99998L6 2.99999V3.99998Z" />
-
    <path d="M10 3.99998L12 3.99999V2.99998L10 2.99998V3.99998Z" />
-
    <path d="M3 7.99999L3 9.99999H2L2 7.99999H3Z" />
-
    <path d="M13 7.99999V9.99998L14 9.99999V7.99999H13Z" />
-
    <path d="M8 2.99998L6 2.99999V1.99998L8 1.99998V2.99998Z" />
-
    <path d="M8 2.99998L10 2.99998L10 1.99998L8 1.99998V2.99998Z" />
-
    <path d="M7 5.99999H9V6.99999H7V5.99999Z" />
-
    <path d="M7 8.99999H9V9.99998H7V8.99999Z" />
-
    <path d="M10 6.99998V8.99998L9 8.99999L9 6.99999L10 6.99998Z" />
-
    <path d="M7 6.99999L7 8.99999L6 8.99998V6.99998L7 6.99999Z" />
+
    <path
+
      d="M9.5 8C9.5 7.17157 8.82843 6.5 8 6.5C7.17157 6.5 6.5 7.17157 6.5 8C6.5 8.82843 7.17157 9.5 8 9.5C8.82843 9.5 9.5 8.82843 9.5 8ZM10.5 8C10.5 9.38071 9.38071 10.5 8 10.5C6.61929 10.5 5.5 9.38071 5.5 8C5.5 6.61929 6.61929 5.5 8 5.5C9.38071 5.5 10.5 6.61929 10.5 8Z" />
+
    <path
+
      d="M13.5 8C13.5 4.96243 11.0376 2.5 8 2.5C4.96243 2.5 2.5 4.96243 2.5 8C2.5 11.0376 4.96243 13.5 8 13.5C11.0376 13.5 13.5 11.0376 13.5 8ZM14.5 8C14.5 11.5899 11.5899 14.5 8 14.5C4.41015 14.5 1.5 11.5899 1.5 8C1.5 4.41015 4.41015 1.5 8 1.5C11.5899 1.5 14.5 4.41015 14.5 8Z" />
  {:else if name === "issue-closed"}
-
    <path d="M6 13H8V14H6V13Z" />
-
    <path d="M10 13H8V14H10V13Z" />
-
    <path d="M3 6L3 8H2L2 6H3Z" />
-
    <path d="M13 6V8H14V6H13Z" />
-
    <path d="M4 12H6V13H4V12Z" />
-
    <path d="M12 12H10V13H12V12Z" />
-
    <path d="M4 4V6H3L3 4H4Z" />
-
    <path d="M12 4V6L13 6V4L12 4Z" />
-
    <path d="M4 10L4 12H3L3 10H4Z" />
-
    <path d="M12 10V12H13V10H12Z" />
-
    <path d="M6 4L4 4L4 3L6 3V4Z" />
-
    <path d="M10 4L12 4V3L10 3V4Z" />
-
    <path d="M3 8L3 10H2L2 8H3Z" />
-
    <path d="M13 8V10H14V8H13Z" />
-
    <path d="M8 3L6 3V2L8 2V3Z" />
-
    <path d="M8 3L10 3L10 2L8 2V3Z" />
-
    <path d="M7 7H8V8H7V7Z" />
-
    <path d="M8 8L9 8V9H8V8Z" />
-
    <path d="M9 9L10 9V10H9V9Z" />
-
    <path d="M9 6H10V7H9V6Z" />
-
    <path d="M6 9H7V10H6V9Z" />
-
    <path d="M7 8H8V9L7 9L7 8Z" />
-
    <path d="M8 7H9V8L8 8V7Z" />
-
    <path d="M6 6H7V7L6 7V6Z" />
+
    <path
+
      d="M13.5 8C13.5 4.96243 11.0376 2.5 8 2.5C4.96243 2.5 2.5 4.96243 2.5 8C2.5 11.0376 4.96243 13.5 8 13.5C11.0376 13.5 13.5 11.0376 13.5 8ZM14.5 8C14.5 11.5899 11.5899 14.5 8 14.5C4.41015 14.5 1.5 11.5899 1.5 8C1.5 4.41015 4.41015 1.5 8 1.5C11.5899 1.5 14.5 4.41015 14.5 8Z" />
+
    <path
+
      d="M10.707 6L8.70605 8.00098L10.7051 10L9.99805 10.707L7.99902 8.70801L6 10.707L5.29297 10L7.29199 8.00098L5.29297 6.00195L6 5.29492L7.99902 7.29395L10 5.29297L10.707 6Z" />
+
  {:else if name === "key"}
+
    <path
+
      d="M7.5 10C7.5 8.61929 6.38071 7.5 5 7.5C3.61929 7.5 2.5 8.61929 2.5 10C2.5 11.3807 3.61929 12.5 5 12.5C6.38071 12.5 7.5 11.3807 7.5 10ZM8.5 10C8.5 11.933 6.933 13.5 5 13.5C3.067 13.5 1.5 11.933 1.5 10C1.5 8.067 3.067 6.5 5 6.5C6.933 6.5 8.5 8.067 8.5 10Z" />
+
    <path
+
      d="M5 10.5C5 10.7761 4.77614 11 4.5 11C4.22386 11 4 10.7761 4 10.5C4 10.2239 4.22386 10 4.5 10C4.77614 10 5 10.2239 5 10.5Z" />
+
    <path
+
      d="M4.75 10.5C4.75 10.3619 4.63807 10.25 4.5 10.25C4.36193 10.25 4.25 10.3619 4.25 10.5C4.25 10.6381 4.36193 10.75 4.5 10.75C4.63807 10.75 4.75 10.6381 4.75 10.5ZM5.25 10.5C5.25 10.9142 4.91421 11.25 4.5 11.25C4.08579 11.25 3.75 10.9142 3.75 10.5C3.75 10.0858 4.08579 9.75 4.5 9.75C4.91421 9.75 5.25 10.0858 5.25 10.5Z" />
+
    <path
+
      d="M14.3535 4.64648L13.6465 5.35352L12 3.70703L10.707 5L12.3535 6.64648L11.6465 7.35352L10 5.70703L7.35352 8.35352L6.64648 7.64648L12 2.29297L14.3535 4.64648Z" />
  {:else if name === "label"}
-
    <path d="M8.5 2.50003H11.5V3.50003H8.5V2.50003Z" />
-
    <path d="M12.5 4.50003H13.5V7.50003H12.5V4.50003Z" />
-
    <path d="M11.5 7.50003H12.5L12.5 8.50003H11.5V7.50003Z" />
-
    <path d="M9.5 9.50003H10.5V10.5H9.5V9.50003Z" />
-
    <path d="M7.5 11.5H8.5V12.5H7.5V11.5Z" />
-
    <path d="M8.5 10.5H9.5V11.5L8.5 11.5V10.5Z" />
-
    <path d="M6.5 12.5H7.5V13.5H6.5V12.5Z" />
-
    <path d="M5.5 11.5H6.5V12.5H5.5V11.5Z" />
-
    <path d="M4.5 10.5H5.5V11.5H4.5V10.5Z" />
-
    <path d="M3.5 9.50003H4.5L4.5 10.5H3.5V9.50003Z" />
-
    <path d="M2.5 8.50003H3.5L3.5 9.50003L2.5 9.50003V8.50003Z" />
-
    <path d="M3.5 7.50003H4.5V8.50003L3.5 8.50003L3.5 7.50003Z" />
-
    <path d="M4.5 6.50003H5.5V7.50003H4.5V6.50003Z" />
-
    <path d="M5.5 5.50003H6.5V6.50003H5.5V5.50003Z" />
-
    <path d="M6.5 4.50003H7.5V5.50003H6.5V4.50003Z" />
-
    <path d="M7.5 3.50003L8.5 3.50003V4.50003L7.5 4.50003V3.50003Z" />
-
    <path d="M9.5 4.50003H10.5V5.50003H9.5V4.50003Z" />
-
    <path d="M9.5 6.50003H10.5V7.50003H9.5V6.50003Z" />
-
    <path d="M8.5 5.50003H9.5L9.5 6.50003H8.5V5.50003Z" />
-
    <path d="M10.5 5.50003L11.5 5.50003V6.50003L10.5 6.50003L10.5 5.50003Z" />
-
    <path d="M11.5 3.50003L12.5 3.50003V4.50003H11.5L11.5 3.50003Z" />
-
    <path d="M12.5 8.50003H13.5V9.50003H12.5V8.50003Z" />
-
    <path d="M9.5 8.50003H10.5L10.5 10.5H9.5L9.5 8.50003Z" />
-
    <path d="M9.5 6.50003H10.5L10.5 8.50003H9.5L9.5 6.50003Z" />
-
    <path d="M12.5 9.50003H13.5V10.5H12.5V9.50003Z" />
-
    <path d="M10.5 10.5H11.5V11.5H10.5L10.5 10.5Z" />
-
    <path d="M12.5 10.5H13.5V11.5H12.5V10.5Z" />
-
    <path d="M10.5 5.50003L11.5 5.50003V6.50003L10.5 6.50003L10.5 5.50003Z" />
-
    <path d="M11.5 11.5H12.5L12.5 12.5H11.5L11.5 11.5Z" />
+
    <path
+
      d="M4.75 12.5L1.375 8L4.75 3.5L14.5 3.5L14.5 12.5L4.75 12.5ZM13.5 4.5L5.25 4.5L2.62598 8L5.25 11.5L13.5 11.5L13.5 4.5Z" />
+
    <path
+
      d="M8 8C8 8.55228 7.55228 9 7 9C6.44772 9 6 8.55228 6 8C6 7.44772 6.44772 7 7 7C7.55228 7 8 7.44772 8 8Z" />
+
  {:else if name === "lightbulb"}
+
    <path
+
      d="M10.3535 6.35352L8.5 8.20703V11.5H10.5V9.79297L12.5 7.79297V6C12.5 4.80654 12.0256 3.66227 11.1816 2.81836C10.3378 1.97446 9.19348 1.5 8 1.5C6.80653 1.5 5.66227 1.97444 4.81836 2.81836C3.97444 3.66227 3.5 4.80653 3.5 6V7.79297L5.5 9.79297V11.5H7.5V8.20703L5.64648 6.35352L6.35352 5.64648L8 7.29297L9.64648 5.64648L10.3535 6.35352ZM13.5 8.20703L11.5 10.207V12.5H4.5V10.207L2.5 8.20703V6C2.5 4.54131 3.07987 3.14278 4.11133 2.11133C5.14278 1.07987 6.54131 0.5 8 0.5C9.45868 0.5 10.8573 1.07985 11.8887 2.11133C12.9201 3.14277 13.5 4.54132 13.5 6V8.20703Z" />
+
    <path d="M10 13.5V14.5H6V13.5H10Z" />
  {:else if name === "link"}
-
    <path d="M8 2L11 2V3H8V2Z" />
-
    <path d="M7 9H9V10H7V9Z" />
-
    <path d="M13 4V6H12V4L13 4Z" />
-
    <path d="M5 6L5 8L4 8L4 6L5 6Z" />
-
    <path d="M11 3L12 3V4L11 4L11 3Z" />
-
    <path d="M5 8H6V9H5L5 8Z" />
-
    <path d="M7 3L8 3L8 4L7 4V3Z" />
-
    <path d="M6 4L7 4L7 5H6V4Z" />
-
    <path d="M5 5H6L6 6H5V5Z" />
-
    <path d="M11 6H12V7H11V6Z" />
-
    <path d="M10 7L11 7L11 8H10V7Z" />
-
    <path d="M9 8H10L10 9H9L9 8Z" />
-
    <path d="M7 6H9V7H7V6Z" />
-
    <path d="M5 13H8V14H5V13Z" />
-
    <path d="M12 8L12 10H11L11 8L12 8Z" />
-
    <path d="M4 10L4 12H3L3 10H4Z" />
-
    <path d="M10 7L11 7L11 8H10V7Z" />
-
    <path d="M4 12H5V13H4L4 12Z" />
-
    <path d="M6 7L7 7L7 8H6L6 7Z" />
-
    <path d="M4 9H5L5 10H4L4 9Z" />
-
    <path d="M10 10L11 10V11L10 11V10Z" />
-
    <path d="M9 11L10 11V12H9V11Z" />
-
    <path d="M8 12L9 12L9 13H8L8 12Z" />
+
    <path
+
      d="M10.3535 6.35352L6.35352 10.3535L5.64648 9.64648L9.64648 5.64648L10.3535 6.35352Z" />
+
    <path
+
      d="M8.64648 2.6465C9.94632 1.34667 12.0537 1.34667 13.3535 2.6465C14.6533 3.94634 14.6533 6.0537 13.3535 7.35354L11 9.70705L10.293 9.00002L12.6465 6.6465C13.5558 5.7372 13.5558 4.26284 12.6465 3.35354C11.7372 2.44423 10.2628 2.44423 9.35352 3.35354L7 5.70705L6.29297 5.00002L8.64648 2.6465Z" />
+
    <path
+
      d="M5.70681 7L3.85329 8.85352C2.94398 9.76282 2.94398 11.2372 3.85329 12.1465C4.7626 13.0558 6.23695 13.0558 7.14626 12.1465L8.99978 10.293L9.70681 11L7.85329 12.8535C6.55346 14.1533 4.44609 14.1533 3.14626 12.8535C1.84643 11.5537 1.84643 9.44632 3.14626 8.14648L4.99978 6.29297L5.70681 7Z" />
  {:else if name === "lock"}
-
    <path d="M6 2H10V3H6V2Z" />
-
    <path d="M10 3L11 3V4H10V3Z" />
-
    <path d="M6 3H5V4H6L6 3Z" />
-
    <path d="M11 4L12 4V7H11V4Z" />
-
    <path d="M5 4H4V7H5V4Z" />
-
    <path d="M2 7H3V13H2V7Z" />
-
    <path d="M3 13H13V14H3L3 13Z" />
-
    <path d="M13 7H14V13H13V7Z" />
-
    <path d="M3 6H13V7H3L3 6Z" />
-
    <path d="M7 8H9V11H7V8Z" />
+
    <path
+
      d="M10.5 5C10.5 3.61929 9.38071 2.5 8 2.5C6.61929 2.5 5.5 3.61929 5.5 5V6H4.5V5C4.5 3.067 6.067 1.5 8 1.5C9.933 1.5 11.5 3.067 11.5 5V6H10.5V5Z" />
+
    <path d="M12.5 6.5H3.5V13.5H12.5V6.5ZM13.5 14.5H2.5V5.5H13.5V14.5Z" />
+
    <path
+
      d="M9 9C9 9.55228 8.55228 10 8 10C7.44772 10 7 9.55228 7 9C7 8.44772 7.44772 8 8 8C8.55228 8 9 8.44772 9 9Z" />
+
    <path d="M8.5 9V12H7.5V9H8.5Z" />
+
  {:else if name === "logo"}
+
    <path
+
      d="M16 12H8V4H16V12ZM12 5.36035C10.542 5.36037 9.36035 6.54296 9.36035 8.00098C9.36051 9.45886 10.5421 10.6406 12 10.6406C13.4579 10.6406 14.6395 9.45887 14.6396 8.00098C14.6396 6.54294 13.458 5.36035 12 5.36035Z" />
+
    <path
+
      d="M1.87988 8.00012C1.87988 6.82928 2.82903 5.88013 3.99988 5.88013C5.17072 5.88013 6.11987 6.82928 6.11987 8.00012C6.11987 9.17097 5.17072 10.1201 3.99988 10.1201C2.82903 10.1201 1.87988 9.17097 1.87988 8.00012Z" />
+
  {:else if name === "mark-read"}
+
    <path
+
      d="M9 1.5V2.5H2.5V8.5H5.30859L5.44727 8.77637L6.30859 10.5H9.69141L10.5527 8.77637L10.6914 8.5H13.5V7H14.5V14.5H1.5V1.5H9ZM2.5 13.5H13.5V9.5H11.3086L10.4473 11.2236L10.3086 11.5H5.69141L5.55273 11.2236L4.69141 9.5H2.5V13.5Z" />
+
    <path
+
      d="M15 3C15 4.10457 14.1046 5 13 5C11.8954 5 11 4.10457 11 3C11 1.89543 11.8954 1 13 1C14.1046 1 15 1.89543 15 3Z" />
  {:else if name === "markdown"}
-
    <path d="M2 4.00003H3V12H2V4.00003Z" />
-
    <path d="M3 7.00003H4V12H3V7.00003Z" />
-
    <path d="M9 4.00003H8V8.00003H9V4.00003Z" />
-
    <path d="M8 7.00003H7V9.00003H8V7.00003Z" />
-
    <path d="M11 11H12V12H11V11Z" />
-
    <path d="M9 10H13V11H9V10Z" />
-
    <path d="M8 9.00003H14V10H8V9.00003Z" />
-
    <path d="M10 4.00003H12V10H10V4.00003Z" />
-
    <path d="M4 5.00003H5V7.00003H4V5.00003Z" />
-
    <path d="M5 7.00003H6V8.00003H5V7.00003Z" />
-
    <path d="M5 6.00003H6V7.00003H5V6.00003Z" />
-
    <path d="M6 5.00003H7V7.00003H6V5.00003Z" />
-
    <path d="M7 4.00003H8V6.00003H7V4.00003Z" />
-
    <path d="M8 11H9V12H8V11Z" />
-
    <path d="M7 10H8V12H7V10Z" />
-
    <path d="M3 4.00003H4V6.00003H3V4.00003Z" />
+
    <path
+
      d="M10.5 5.5H12.793L10.5 3.20703V5.5ZM14.5 14.5H12.5V13.5H13.5V6.5H9.5V2.5H4.5V8.5H3.5V1.5H10.207L14.5 5.79297V14.5Z" />
+
    <path
+
      d="M5.5 10.833L4 12.833L2.5 10.833V14.5H1.5V9.5H2.75L4 11.166L5.25 9.5H6.5V14.5H5.5V10.833Z" />
+
    <path
+
      d="M10.5 12C10.5 11.363 10.2459 11.013 9.95801 10.8057C9.6446 10.58 9.25332 10.5 9 10.5H8.5V13.5H9C9.25332 13.5 9.6446 13.42 9.95801 13.1943C10.2459 12.987 10.5 12.637 10.5 12ZM11.5 12C11.5 12.963 11.0874 13.6132 10.542 14.0059C10.0221 14.3801 9.41331 14.5 9 14.5H7.5V9.5H9C9.41331 9.5 10.0221 9.61988 10.542 9.99414C11.0874 10.3868 11.5 11.037 11.5 12Z" />
+
  {:else if name === "menu"}
+
    <path d="M2 7.5L14 7.5L14 8.5L2 8.5L2 7.5Z" />
+
    <path d="M2 3.5L14 3.5L14 4.5L2 4.5L2 3.5Z" />
+
    <path d="M2 11.5L14 11.5L14 12.5L2 12.5L2 11.5Z" />
  {:else if name === "minus"}
-
    <path d="M14 7V9L2 9L2 7L14 7Z" />
+
    <path d="M13 7.505V8.505H3V7.505H13Z" />
  {: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 === "more-vertical"}
-
    <path d="M9 2V4L7 4L7 2L9 2Z" />
-
    <path d="M9 7L9 9H7L7 7H9Z" />
-
    <path d="M9 12L9 14H7L7 12H9Z" />
+
    <path
+
      d="M7.33545 2.38928C6.07966 4.14677 6.24028 6.60466 7.81787 8.18225C9.39586 9.75989 11.8542 9.91984 13.6118 8.66272L14.395 9.15784C14.1681 10.4168 13.5693 11.6232 12.5962 12.5963C10.0579 15.1346 5.94221 15.1344 3.40381 12.5963C0.865398 10.0579 0.865398 5.94234 3.40381 3.40393C4.37633 2.43146 5.58144 1.83329 6.83935 1.60608L7.33545 2.38928ZM5.91064 2.91272C5.25599 3.18051 4.64259 3.57924 4.11084 4.11096C1.96295 6.25885 1.96295 9.7414 4.11084 11.8893C6.25877 14.0369 9.7414 14.037 11.8892 11.8893C12.4218 11.3566 12.8204 10.7416 13.0884 10.0856C11.0983 10.9027 8.72776 10.5059 7.11084 8.88928C5.49473 7.27318 5.09511 4.90264 5.91064 2.91272Z" />
  {:else if name === "none"}
-
    <path d="M6 13H8V14H6V13Z" />
-
    <path d="M10 13H7V14H10V13Z" />
-
    <path d="M3 5.00003L3 8.00003H2L2 5.00003L3 5.00003Z" />
-
    <path d="M13 6.00003V8.00003H14V6.00003H13Z" />
-
    <path d="M4 12H6V13H4V12Z" />
-
    <path d="M12 12H10V13H12V12Z" />
-
    <path d="M4 4.00003V6.00003H3L3 4.00003H4Z" />
-
    <path d="M12 4.00003V6.00003L13 6.00003V4.00003L12 4.00003Z" />
-
    <path d="M4 10L4 12H3L3 10H4Z" />
-
    <path d="M12 10V12H13V10H12Z" />
-
    <path d="M6 4.00003L4 4.00003L4 3.00003L6 3.00003V4.00003Z" />
-
    <path d="M10 4.00003L12 4.00003V3.00003L10 3.00003V4.00003Z" />
-
    <path d="M3 8.00003L3 10H2L2 8.00003H3Z" />
-
    <path d="M13 7.00003V10H14V7.00003H13Z" />
-
    <path d="M9 3.00003L6 3.00003V2.00003L9 2.00003V3.00003Z" />
-
    <path d="M8 3.00003L10 3.00003L10 2.00003L8 2.00003L8 3.00003Z" />
-
    <path d="M11 4.00003H12V5.00003H11V4.00003Z" />
-
    <path d="M10 5.00003H11V6.00003H10V5.00003Z" />
-
    <path d="M11 5.00003H12V6.00003L11 6.00003V5.00003Z" />
-
    <path d="M10 6.00003H11V7.00003H10V6.00003Z" />
-
    <path d="M9 7.00003L10 7.00003V8.00003H9V7.00003Z" />
-
    <path d="M8 8.00003H9V9.00003H8V8.00003Z" />
-
    <path d="M7 9.00003L8 9.00003L8 10H7V9.00003Z" />
-
    <path d="M6 10H7V11H6V10Z" />
-
    <path d="M5 11H6L6 12H5V11Z" />
-
    <path d="M9 6.00003H10V7.00003L9 7.00003V6.00003Z" />
-
    <path d="M8 7.00003H9V8.00003H8V7.00003Z" />
-
    <path d="M7 8.00003L8 8.00003V9.00003L7 9.00003V8.00003Z" />
-
    <path d="M6 9.00003H7V10H6V9.00003Z" />
-
    <path d="M5 10H6V11H5L5 10Z" />
-
    <path d="M4 11H5V12H4V11Z" />
+
    <path
+
      d="M3.40431 3.40431C5.9424 0.866216 10.0563 0.865874 12.5947 3.40333H12.5957C15.1342 5.94174 15.1342 10.0573 12.5957 12.5957C10.0573 15.1342 5.94174 15.1342 3.40333 12.5957V12.5947C0.865874 10.0563 0.866216 5.9424 3.40431 3.40431ZM4.48048 12.2256C6.64131 14.0296 9.85986 13.9176 11.8887 11.8887C13.9176 9.85986 14.0296 6.64131 12.2256 4.48048L4.48048 12.2256ZM11.5186 3.77345C9.35778 1.97034 6.13988 2.08279 4.11134 4.11134C2.08279 6.13988 1.97034 9.35778 3.77345 11.5186L11.5186 3.77345Z" />
  {:else if name === "offline"}
-
    <path d="M3 6L3 8H2L2 6H3Z" />
-
    <path d="M13 10V8H14V10H13Z" />
-
    <path d="M5 9V7H4V9H5Z" />
-
    <path d="M11 7V9H12V7H11Z" />
-
    <path d="M4 4L4 6H3L3 4H4Z" />
-
    <path d="M12 12V10H13V12H12Z" />
-
    <path d="M4 10V12H3L3 10H4Z" />
-
    <path d="M12 6V5H13V6H12Z" />
-
    <path d="M5 4H4V3L5 3V4Z" />
-
    <path d="M11 12H12V13H11V12Z" />
-
    <path d="M3 8L3 10H2L2 8H3Z" />
-
    <path d="M13 8V6H14V8H13Z" />
-
    <path d="M10 5L11 5V6L10 6V5Z" />
-
    <path d="M6 7H5L5 5H6V7Z" />
-
    <path d="M10 9L11 9V11H10V9Z" />
-
    <path d="M7 7H9V8H7V7Z" />
-
    <path d="M6 11H7V12H6V11Z" />
-
    <path d="M10 5H9V4H10V5Z" />
-
    <path d="M6 4H7V5H6L6 4Z" />
-
    <path d="M10 12H9V11H10V12Z" />
-
    <path d="M5 2H6V3L5 3L5 2Z" />
-
    <path d="M11 14H10V13H11V14Z" />
-
    <path d="M5 13H6V14H5V13Z" />
-
    <path d="M11 3L10 3V2L11 2V3Z" />
-
    <path d="M12 4H11V3L12 3V4Z" />
-
    <path d="M13 2.00001H14V3.00001H13V2.00001Z" />
-
    <path d="M12 3L13 3.00001V4.00001L12 4V3Z" />
-
    <path d="M11 4H12V5L11 5V4Z" />
-
    <path d="M10 5L11 5V6L10 6V5Z" />
-
    <path d="M9 6.00001L10 6L10 7.00001L9 7L9 6.00001Z" />
-
    <path d="M8 7.00001L9 7V8L8 8.00001V7.00001Z" />
-
    <path d="M7 8L8 8.00001V9.00001H7L7 8Z" />
-
    <path d="M6 9L7 9.00001V10H6V9Z" />
-
    <path d="M5 10H6V11L5 11V10Z" />
-
    <path d="M4 11L5 11V12L4 12L4 11Z" />
-
    <path d="M3 12H4L4 13H3L3 12Z" />
-
    <path d="M2 13L3 13L3 14H2V13Z" />
+
    <path
+
      d="M10.207 10.5L8.70703 12L10.207 13.5L9.5 14.207L8 12.707L6.5 14.207L5.79297 13.5L7.29297 12L5.79297 10.5L6.5 9.79297L8 11.293L9.5 9.79297L10.207 10.5Z" />
+
    <path
+
      d="M8.00049 2.5C9.1165 2.50005 10.2214 2.72041 11.2524 3.14746C12.2836 3.57457 13.221 4.2001 14.0103 4.98926L13.3032 5.69629C12.607 5.00011 11.7802 4.44814 10.8706 4.07129C9.96078 3.69443 8.98528 3.50005 8.00049 3.5C7.01564 3.5 6.04026 3.69443 5.13037 4.07129C4.22051 4.44817 3.39318 4.99994 2.69678 5.69629L1.98975 4.98926C2.77901 4.20005 3.71637 3.57459 4.74756 3.14746C5.77881 2.72031 6.88427 2.5 8.00049 2.5Z" />
+
    <path
+
      d="M7.99951 4.5C8.8531 4.5 9.69918 4.66846 10.4878 4.99512C11.2761 5.32173 11.9928 5.79996 12.5962 6.40332L12.2427 6.75781L11.8892 7.11133C11.3785 6.60069 10.7721 6.19534 10.105 5.91895C9.43769 5.64254 8.72178 5.5 7.99951 5.5C7.2774 5.50006 6.56217 5.6426 5.89502 5.91895C5.22783 6.19534 4.6215 6.60067 4.11084 7.11133L3.75732 6.75781L3.40381 6.40332C4.00724 5.79996 4.72386 5.32172 5.51221 4.99512C6.30068 4.66852 7.14607 4.50006 7.99951 4.5Z" />
+
    <path
+
      d="M7.99951 6.5C8.59046 6.5 9.1762 6.61663 9.72217 6.84277C10.268 7.06892 10.7643 7.40058 11.1821 7.81836L10.4751 8.52539C10.1501 8.20043 9.76392 7.94249 9.33936 7.7666C8.91472 7.59071 8.45914 7.5 7.99951 7.5C7.54003 7.50004 7.08516 7.59078 6.66064 7.7666C6.236 7.94249 5.84991 8.20039 5.5249 8.52539L4.81787 7.81836C5.23573 7.4005 5.73187 7.06892 6.27783 6.84277C6.8237 6.61667 7.40867 6.50004 7.99951 6.5Z" />
  {:else if name === "online"}
-
    <path d="M3 5.99999L3 7.99999H2L2 5.99999H3Z" />
-
    <path d="M13 9.99998V7.99998H14V9.99998H13Z" />
-
    <path d="M5 8.99998L5 6.99998H4L4 8.99998H5Z" />
-
    <path d="M11 6.99999V8.99999H12V6.99999H11Z" />
-
    <path d="M4 12H5V13H4V12Z" />
-
    <path d="M12 3.99998L11 3.99998V2.99998L12 2.99998V3.99998Z" />
-
    <path d="M4 3.99999V5.99999L3 5.99999L3 3.99999H4Z" />
-
    <path d="M12 12V9.99998H13V12H12Z" />
-
    <path d="M4 9.99998L4 12H3L3 9.99998H4Z" />
-
    <path d="M12 5.99999V3.99998L13 3.99999V5.99999H12Z" />
-
    <path d="M5 3.99999H4L4 2.99999L5 2.99999V3.99999Z" />
-
    <path d="M11 12H12V13H11V12Z" />
-
    <path d="M3 7.99999L3 9.99998H2L2 7.99999H3Z" />
-
    <path d="M13 7.99998V5.99999L14 5.99998V7.99998H13Z" />
-
    <path d="M6 11H5L5 8.99998H6L6 11Z" />
-
    <path d="M10 4.99998H11L11 6.99999L10 6.99998L10 4.99998Z" />
-
    <path d="M6 6.99998H5L5 4.99998L6 4.99998L6 6.99998Z" />
-
    <path d="M10 8.99998L11 8.99999L11 11H10L10 8.99998Z" />
-
    <path d="M7 6.99998H9V8.99998H7V6.99998Z" />
-
    <path d="M6 11H7V12H6V11Z" />
-
    <path d="M10 4.99998L9 4.99998V3.99998L10 3.99998V4.99998Z" />
-
    <path d="M6 3.99999H7V4.99999L6 4.99998L6 3.99999Z" />
-
    <path d="M10 12H9V11H10L10 12Z" />
-
    <path d="M5 1.99999H6V2.99999L5 2.99999L5 1.99999Z" />
-
    <path d="M11 14H10V13H11V14Z" />
-
    <path d="M5 13H6V14H5V13Z" />
-
    <path d="M11 2.99998L10 2.99998V1.99998L11 1.99998V2.99998Z" />
+
    <path
+
      d="M8.00049 2.5C9.11659 2.50005 10.2223 2.72035 11.2534 3.14746C12.2843 3.57457 13.2211 4.20026 14.0103 4.98926L13.3032 5.69629C12.6069 5.00011 11.7802 4.44814 10.8706 4.07129C9.96078 3.69443 8.98527 3.50005 8.00049 3.5C7.01564 3.5 6.04026 3.69444 5.13037 4.07129C4.22051 4.44817 3.39318 4.99994 2.69678 5.69629L1.98975 4.98926C2.779 4.20005 3.71637 3.57459 4.74756 3.14746C5.7788 2.72032 6.88427 2.5 8.00049 2.5Z" />
+
    <path
+
      d="M7.99951 4.5C8.8531 4.5 9.69919 4.66846 10.4878 4.99512C11.2762 5.32173 11.9927 5.79999 12.5962 6.40332L12.2427 6.75781L11.8892 7.11133C11.3785 6.60067 10.7722 6.19534 10.105 5.91895C9.43772 5.64255 8.72178 5.5 7.99951 5.5C7.27739 5.50006 6.56216 5.6426 5.89502 5.91895C5.22783 6.19534 4.6215 6.60067 4.11084 7.11133L3.75732 6.75781L3.40381 6.40332C4.00723 5.79997 4.72385 5.32172 5.51221 4.99512C6.30068 4.66852 7.14608 4.50006 7.99951 4.5Z" />
+
    <path
+
      d="M7.99951 6.5C8.59046 6.5 9.1762 6.61662 9.72217 6.84277C10.268 7.06891 10.7644 7.40056 11.1821 7.81836L10.4751 8.52539C10.1502 8.20046 9.7639 7.9425 9.33936 7.7666C8.91472 7.59071 8.45914 7.5 7.99951 7.5C7.54003 7.50004 7.08516 7.59078 6.66064 7.7666C6.23601 7.94249 5.8499 8.20039 5.5249 8.52539L4.81787 7.81836C5.23573 7.4005 5.73187 7.06892 6.27783 6.84277C6.82371 6.61667 7.40867 6.50004 7.99951 6.5Z" />
+
    <path
+
      d="M9 12C9 11.4477 8.55228 11 8 11C7.44772 11 7 11.4477 7 12C7 12.5523 7.44772 13 8 13C8.55228 13 9 12.5523 9 12ZM10 12C10 13.1046 9.10457 14 8 14C6.89543 14 6 13.1046 6 12C6 10.8954 6.89543 10 8 10C9.10457 10 10 10.8954 10 12Z" />
  {:else if name === "open-external"}
-
    <path d="M3 2L6 2V3L3 3V2Z" />
-
    <path d="M3 13L13 13V14L3 14V13Z" />
-
    <path d="M2 3L3 3L3 13H2L2 3Z" />
-
    <path d="M13 10H14V13H13V10Z" />
-
    <path d="M13 2H14V3L13 3V2Z" />
-
    <path d="M12 3L13 3V4H12V3Z" />
-
    <path d="M12 4H13V5H12V4Z" />
-
    <path d="M11 3L12 3V4L11 4V3Z" />
-
    <path d="M11 4L12 4V5L11 5V4Z" />
-
    <path d="M10 5L11 5V6H10V5Z" />
-
    <path d="M9 6H10V7H9V6Z" />
-
    <path d="M8 7L9 7V8H8V7Z" />
-
    <path d="M7 8H8V9H7V8Z" />
-
    <path d="M12 2L13 2V3L12 3V2Z" />
-
    <path d="M11 2L12 2V3L11 3V2Z" />
-
    <path d="M9 2H10V3H9V2Z" />
-
    <path d="M8 2L9 2V3L8 3V2Z" />
-
    <path d="M10 2L11 2V3L10 3V2Z" />
-
    <path d="M13 3L14 3V4H13V3Z" />
-
    <path d="M13 4H14V5H13V4Z" />
-
    <path d="M13 5H14V6H13V5Z" />
-
    <path d="M13 6H14V7H13V6Z" />
-
    <path d="M13 7H14V8H13V7Z" />
+
    <path d="M6 2.5V3.5H3.5V12.5H12.5V10H13.5V13.5H2.5V2.5H6Z" />
+
    <path
+
      d="M6.29297 9L11.793 3.5L8.5 3.5L8.5 2.5L13.5 2.5L13.5 7.5L12.5 7.5L12.5 4.20703L7 9.70703L6.29297 9Z" />
  {:else if name === "patch"}
-
    <path d="M13 11H14V14H11L11 11H12V7H13V11ZM12 13L12 12H13L13 13H12Z" />
-
    <path d="M12 7L9 7L9 9H8L8 8L7 8V7H6V6L7 6V5H8L8 4L9 4V6L12 6L12 7Z" />
    <path
-
      d="M3 5H2L2 2L5 2L5 5H4L4 11H5L5 14H2L2 11H3L3 5ZM4 4V3H3L3 4L4 4ZM3 12H4L4 13H3V12Z" />
+
      d="M5.5 12C5.5 11.1716 4.82843 10.5 4 10.5C3.17157 10.5 2.5 11.1716 2.5 12C2.5 12.8284 3.17157 13.5 4 13.5C4.82843 13.5 5.5 12.8284 5.5 12ZM13.5 12C13.5 11.1716 12.8284 10.5 12 10.5C11.1716 10.5 10.5 11.1716 10.5 12C10.5 12.8284 11.1716 13.5 12 13.5C12.8284 13.5 13.5 12.8284 13.5 12ZM7.5 2.5H11.5V3.5H9.20703L12.5 6.79297V9.5498C13.6411 9.78142 14.5 10.7905 14.5 12C14.5 13.3807 13.3807 14.5 12 14.5C10.6193 14.5 9.5 13.3807 9.5 12C9.5 10.7905 10.3589 9.78142 11.5 9.5498V7.20703L8.5 4.20703V6.5H7.5V2.5ZM5.5 4C5.5 3.17157 4.82843 2.5 4 2.5C3.17157 2.5 2.5 3.17157 2.5 4C2.5 4.82843 3.17157 5.5 4 5.5C4.82843 5.5 5.5 4.82843 5.5 4ZM6.5 4C6.5 5.20943 5.64105 6.21753 4.5 6.44922V9.5498C5.64114 9.78142 6.5 10.7905 6.5 12C6.5 13.3807 5.38071 14.5 4 14.5C2.61929 14.5 1.5 13.3807 1.5 12C1.5 10.7905 2.35886 9.78142 3.5 9.5498V6.44922C2.35895 6.21753 1.5 5.20943 1.5 4C1.5 2.61929 2.61929 1.5 4 1.5C5.38071 1.5 6.5 2.61929 6.5 4Z" />
  {:else if name === "patch-archived"}
-
    <path d="M9 6H12V7H9V6Z" />
-
    <path d="M12 7L13 7V12H12V7Z" />
-
    <path d="M3 5H4V12H3V5Z" />
-
    <path d="M4 12H3L3 13H4L4 12ZM2 11V14H5V11H2Z" />
-
    <path d="M4 3H3V4H4V3ZM2 2V5H5V2H2Z" />
-
    <path d="M9 14H8L8 13H9L9 14Z" />
-
    <path d="M10 13H9V12H10V13Z" />
-
    <path d="M7 13H8V12H7V13Z" />
-
    <path d="M6 12V11H11V12H6Z" />
-
    <path d="M8 7H9V13H8V7Z" />
-
    <path d="M11 12H12V11H14V14H11V12ZM12 12H13V13H12V12Z" />
+
    <path
+
      d="M13.5 2.5V13.5H2.5V2.5H13.5ZM3.5 12.5H12.5V8.5H10.4502C10.2186 9.64114 9.20949 10.5 8 10.5C6.79051 10.5 5.78142 9.64114 5.5498 8.5H3.5V12.5ZM8 6.5C7.17157 6.5 6.5 7.17157 6.5 8C6.5 8.82843 7.17157 9.5 8 9.5C8.82843 9.5 9.5 8.82843 9.5 8C9.5 7.17157 8.82843 6.5 8 6.5ZM3.5 7.5H5.5498C5.78142 6.35886 6.79051 5.5 8 5.5C9.20949 5.5 10.2186 6.35886 10.4502 7.5H12.5V3.5H3.5V7.5Z" />
  {:else if name === "patch-draft"}
-
    <path d="M11 9H12V10H11V9Z" />
-
    <path d="M11 7H12V8H11V7Z" />
-
    <path d="M11 5H12V6H11V5Z" />
-
    <path d="M11 3H12V4H11V3Z" />
-
    <path d="M4 5H5V11H4V5Z" />
-
    <path d="M12 12H11V13H12V12ZM10 11V14H13V11H10Z" />
-
    <path d="M5 12H4V13H5V12ZM3 11V14H6V11H3Z" />
-
    <path d="M5 3H4V4H5V3ZM3 2V5H6V2H3Z" />
+
    <path
+
      d="M5 4C5 3.44772 4.55228 3 4 3C3.44772 3 3 3.44772 3 4C3 4.55228 3.44772 5 4 5V6C2.89543 6 2 5.10457 2 4C2 2.89543 2.89543 2 4 2C5.10457 2 6 2.89543 6 4C6 5.10457 5.10457 6 4 6V5C4.55228 5 5 4.55228 5 4Z" />
+
    <path
+
      d="M4.5 12V13H3.5V12H4.5ZM4.5 10V11H3.5V10H4.5ZM4.5 8V9H3.5V8H4.5ZM4.5 5V7H3.5V5H4.5Z" />
+
    <path
+
      d="M13 4C13 3.44772 12.5523 3 12 3C11.4477 3 11 3.44772 11 4C11 4.55228 11.4477 5 12 5V6C10.8954 6 10 5.10457 10 4C10 2.89543 10.8954 2 12 2C13.1046 2 14 2.89543 14 4C14 5.10457 13.1046 6 12 6V5C12.5523 5 13 4.55228 13 4Z" />
+
    <path
+
      d="M13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12C11 12.5523 11.4477 13 12 13V14C10.8954 14 10 13.1046 10 12C10 10.8954 10.8954 10 12 10C13.1046 10 14 10.8954 14 12C14 13.1046 13.1046 14 12 14V13C12.5523 13 13 12.5523 13 12Z" />
+
    <path d="M12.5 5.5L12.5 10.5H11.5L11.5 5.5H12.5Z" />
  {:else if name === "patch-merged"}
-
    <path d="M5 3.00003H11V4.00003H5V3.00003Z" />
-
    <path d="M11 4.00003L12 4.00003V11H11V4.00003Z" />
-
    <path d="M4 5.00003H5V11H4V5.00003Z" />
-
    <path d="M12 12H11V13H12V12ZM10 11V14H13V11H10Z" />
-
    <path d="M5 12H4V13H5V12ZM3 11V14H6V11H3Z" />
-
    <path
-
      d="M5 3.00003L4 3.00003V4.00003L5 4.00003V3.00003ZM3 2.00003V5.00003H6V2.00003H3Z" />
-
  {:else if name === "pen"}
-
    <path d="M13 4.99998H14V5.99999H13V4.99998Z" />
-
    <path d="M2 13H3V14H2V13Z" />
-
    <path d="M10 1.99998H11V2.99998H10V1.99998Z" />
-
    <path d="M11 2.99998L12 2.99998V3.99998H11V2.99998Z" />
-
    <path d="M12 3.99998L13 3.99998V4.99998H12V3.99998Z" />
-
    <path d="M6 5.99998H7V6.99998H6V5.99998Z" />
-
    <path d="M7 4.99998H8V5.99998H7V4.99998Z" />
-
    <path d="M8 3.99998H9V4.99998H8V3.99998Z" />
-
    <path d="M9 4.99998L10 4.99998V5.99998H9V4.99998Z" />
-
    <path d="M10 5.99998H11V6.99998H10V5.99998Z" />
-
    <path d="M9 2.99998L10 2.99998V3.99998H9V2.99998Z" />
-
    <path d="M12 5.99998L13 5.99999V6.99998H12V5.99998Z" />
-
    <path d="M5 6.99999L6 6.99998L6 7.99999H5V6.99999Z" />
-
    <path d="M9 8.99998H10V9.99998H9V8.99998Z" />
-
    <path d="M10 7.99998H11V8.99998H10V7.99998Z" />
-
    <path d="M11 6.99998H12V7.99998H11V6.99998Z" />
-
    <path d="M4 7.99998L5 7.99999V8.99998H4V7.99998Z" />
-
    <path d="M8 9.99998H9L9 11H8V9.99998Z" />
-
    <path d="M3 8.99998H4V9.99998H3V8.99998Z" />
-
    <path d="M7 11H8V12H7V11Z" />
-
    <path d="M2 9.99998H3V11H2V9.99998Z" />
-
    <path d="M6 12H7V13H6V12Z" />
-
    <path d="M4 12H5V13H4V12Z" />
-
    <path d="M4 11H5V12H4V11Z" />
-
    <path d="M3 11H4V12H3V11Z" />
-
    <path d="M3 9.99998H4V11H3V9.99998Z" />
-
    <path d="M5 12H6V13H5V12Z" />
-
    <path d="M4 13H5V14H4V13Z" />
-
    <path d="M5 13H6L6 14H5V13Z" />
-
    <path d="M2 12H3V13H2V12Z" />
-
    <path d="M2 11H3V12H2V11Z" />
-
    <path d="M3 12H4V13H3V12Z" />
-
    <path d="M3 13H4V14H3V13Z" />
-
  {:else if name === "pin"}
-
    <path d="M3.5 2H12.5V3H3.5V2Z" />
-
    <path d="M3.5 9H12.5V10H3.5V9Z" />
-
    <path d="M7.5 10H8.5V14H7.5V10Z" />
-
    <path d="M5.5 4H6.5V8H5.5V4Z" />
-
    <path d="M10.5 4H9.5V8H10.5V4Z" />
-
    <path d="M4.5 3H5.5V4H4.5V3Z" />
-
    <path d="M11.5 3H10.5V4H11.5V3Z" />
-
    <path d="M4.5 8H5.5V9H4.5V8Z" />
-
    <path d="M11.5 8H10.5V9H11.5V8Z" />
-
    <path d="M5.5 3H8.5V8H5.5V3Z" />
-
    <path d="M5.5 8H10.5V9H5.5V8Z" />
+
    <path
+
      d="M5 4C5 3.44772 4.55228 3 4 3C3.44772 3 3 3.44772 3 4C3 4.55228 3.44772 5 4 5V6C2.89543 6 2 5.10457 2 4C2 2.89543 2.89543 2 4 2C5.10457 2 6 2.89543 6 4C6 5.10457 5.10457 6 4 6V5C4.55228 5 5 4.55228 5 4Z" />
+
    <path
+
      d="M13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12C11 12.5523 11.4477 13 12 13V14C10.8954 14 10 13.1046 10 12C10 10.8954 10.8954 10 12 10C13.1046 10 14 10.8954 14 12C14 13.1046 13.1046 14 12 14V13C12.5523 13 13 12.5523 13 12Z" />
+
    <path
+
      d="M5 12C5 11.4477 4.55228 11 4 11C3.44772 11 3 11.4477 3 12C3 12.5523 3.44772 13 4 13V14C2.89543 14 2 13.1046 2 12C2 10.8954 2.89543 10 4 10C5.10457 10 6 10.8954 6 12C6 13.1046 5.10457 14 4 14V13C4.55228 13 5 12.5523 5 12Z" />
+
    <path d="M4.5 5.5V10.5H3.5V5.5H4.5Z" />
+
    <path
+
      d="M9.20703 3.5L12.5 6.79297V10.5H11.5V7.20703L8.79297 4.5H5.5V3.5H9.20703Z" />
+
  {:else if name === "pin-filled"}
+
    <path d="M5 6L6 2H10L11 6L13 7V10H8H3V7L5 6Z" />
+
    <path
+
      d="M10.3906 1.5L11.4287 5.65527L13.2236 6.55273L13.5 6.69141V10.5H8.5V15H7.5V10.5H2.5V6.69141L2.77637 6.55273L4.57031 5.65527L5.60938 1.5H10.3906ZM5.48535 6.12109L5.42969 6.34473L5.22363 6.44727L3.5 7.30859V9.5H12.5V7.30859L10.7764 6.44727L10.5703 6.34473L10.5146 6.12109L9.60938 2.5H6.39062L5.48535 6.12109Z" />
  {:else if name === "pin-hollow"}
-
    <path d="M3.5 2H12.5V3H3.5V2Z" />
-
    <path d="M3.5 9H12.5V10H3.5V9Z" />
-
    <path d="M7.5 10H8.5V14H7.5V10Z" />
-
    <path d="M5.5 4H6.5V8H5.5V4Z" />
-
    <path d="M10.5 4H9.5V8H10.5V4Z" />
-
    <path d="M4.5 3H5.5V4H4.5V3Z" />
-
    <path d="M11.5 3H10.5V4H11.5V3Z" />
-
    <path d="M4.5 8H5.5V9H4.5V8Z" />
-
    <path d="M11.5 8H10.5V9H11.5V8Z" />
-
    <path d="M8.5 4H9.5V5H8.5V4Z" />
+
    <path
+
      d="M10.3906 1.5L11.4287 5.65527L13.2236 6.55273L13.5 6.69141V10.5H8.5V15H7.5V10.5H2.5V6.69141L2.77637 6.55273L4.57031 5.65527L5.60938 1.5H10.3906ZM5.48535 6.12109L5.42969 6.34473L5.22363 6.44727L3.5 7.30859V9.5H12.5V7.30859L10.7764 6.44727L10.5703 6.34473L10.5146 6.12109L9.60938 2.5H6.39062L5.48535 6.12109Z" />
+
  {:else if name === "placeholder"}
+
    <path
+
      d="M13 13H3V3H13V13ZM4.70703 12H12V4.70703L4.70703 12ZM4 11.293L11.293 4H4V11.293Z" />
+
  {:else if name === "play"}
+
    <path d="M12 8L4 13V3L12 8Z" />
+
    <path
+
      d="M12.9434 8L3.5 13.9023V2.09766L12.9434 8ZM4.5 12.0977L11.0566 8L4.5 3.90137V12.0977Z" />
  {:else if name === "plus"}
-
    <path d="M9 14H7L7 2L9 2L9 14Z" />
-
    <path d="M14 7V9L2 9L2 7L14 7Z" />
+
    <path
+
      d="M8.5 3V7.50488H13.0049V8.50488H8.5V13H7.5V8.50488H3.00488V7.50488H7.5V3H8.5Z" />
+
  {:else if name === "question-mark"}
+
    <path
+
      d="M8.625 11.625C8.625 11.9702 8.34518 12.25 8 12.25C7.65482 12.25 7.375 11.9702 7.375 11.625C7.375 11.2798 7.65482 11 8 11C8.34518 11 8.625 11.2798 8.625 11.625Z" />
+
    <path
+
      d="M9.63379 6.5C9.63379 5.89312 9.41336 5.5453 9.13867 5.33496C8.84286 5.10866 8.42725 5 8 5C7.57445 5 7.20236 5.10724 6.94531 5.32129C6.7043 5.52213 6.5 5.87024 6.5 6.5H5.5C5.5 5.62992 5.79588 4.97787 6.30469 4.55371C6.79763 4.14292 7.42571 4 8 4C8.57275 4 9.22492 4.14134 9.74609 4.54004C10.2882 4.95474 10.6338 5.60725 10.6338 6.5C10.6338 7.32703 10.1246 7.91638 9.6123 8.29102C9.24736 8.55787 8.84058 8.74663 8.5 8.86816V10H7.5V8.10938L7.87891 8.01465C8.16659 7.9427 8.63623 7.76595 9.02148 7.48438C9.40947 7.20067 9.63379 6.87287 9.63379 6.5Z" />
  {:else if name === "reply"}
-
    <path d="M2.5 9V8H3.5V9H2.5Z" />
-
    <path d="M3.5 10L3.5 9L4.5 9V10L3.5 10Z" />
-
    <path d="M3.5 7V8L4.5 8V7L3.5 7Z" />
-
    <path d="M3.5 8L13.5 8V9L3.5 9V8Z" />
-
    <path d="M4.5 6H5.5V11H4.5V6Z" />
-
    <path d="M13.5 4H12.5V8L13.5 8L13.5 4Z" />
-
    <path d="M5.5 5H6.5V12H5.5V5Z" />
-
  {: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 === "review"}
-
    <path d="M11 5L8 5V4L11 4V5Z" />
-
    <path d="M9 7L10 7V6L9 6V7Z" />
-
    <path d="M10 9H9L9 10H10V9Z" />
-
    <path d="M7 6V7H6L6 6L7 6Z" />
-
    <path d="M4 6L4 7H3L3 6H4Z" />
-
    <path d="M12 10V9L13 9V10L12 10Z" />
-
    <path d="M12 6L11 6V5L12 5V6Z" />
-
    <path d="M7 10L8 10L8 11H7L7 10Z" />
-
    <path d="M4 10H5L5 11L4 11L4 10Z" />
-
    <path d="M5 11L7 11L7 12L5 12L5 11Z" />
-
    <path d="M8 6L7 6V5L8 5V6Z" />
-
    <path d="M5 6H4L4 5L5 5V6Z" />
-
    <path d="M7 5L5 5L5 4L7 4V5Z" />
-
    <path d="M11 10H12V11L11 11V10Z" />
-
    <path d="M12 7V6L13 6V7L12 7Z" />
-
    <path d="M7 9L7 10H6L6 9H7Z" />
-
    <path d="M4 9L4 10H3L3 9H4Z" />
-
    <path d="M8 11H11L11 12H8L8 11Z" />
-
    <path d="M8 8L8 7H9L9 8H8Z" />
-
    <path d="M11 8L11 9L10 9L10 8L11 8Z" />
-
    <path d="M10 8V7L11 7V8L10 8Z" />
-
    <path d="M9 8L9 9H8V8H9Z" />
-
    <path d="M14 7V8H13V7L14 7Z" />
-
    <path d="M5 9V8H6V9L5 9Z" />
-
    <path d="M2 9L2 8H3L3 9L2 9Z" />
-
    <path d="M6 7L6 8H5L5 7H6Z" />
-
    <path d="M3 7L3 8H2L2 7H3Z" />
-
    <path d="M13 9V8H14V9H13Z" />
+
    <path
+
      d="M5.35352 2.35352L3.20703 4.5H14.5V13.5H10V12.5H13.5V5.5H3.20703L5.35352 7.64648L4.64648 8.35352L1.29297 5L4.64648 1.64648L5.35352 2.35352Z" />
+
  {:else if name === "repository"}
+
    <path
+
      d="M8.22363 9.44727L8 9.55859L7.77637 9.44727L2.5 6.80859V9.69043L8 12.4404L13.5 9.69043V6.80859L8.22363 9.44727ZM3.11816 6L8 8.44043L12.8818 6L8 3.55859L3.11816 6ZM14.5 10.3086L14.2236 10.4473L8.22363 13.4473L8 13.5586L7.77637 13.4473L1.77637 10.4473L1.5 10.3086V5.69141L1.77637 5.55273L7.77637 2.55273L8 2.44141L8.22363 2.55273L14.2236 5.55273L14.5 5.69141V10.3086Z" />
  {:else if name === "revision"}
-
    <path d="M6 13H8V14H6V13Z" />
-
    <path d="M8 13H9V14H8V13Z" />
-
    <path d="M3 6L3 8H2L2 6H3Z" />
-
    <path d="M4 12H6L6 13H4V12Z" />
-
    <path d="M12 12H10V13H12V12Z" />
-
    <path d="M4 4V6H3L3 4H4Z" />
-
    <path d="M4 10L4 12H3L3 10H4Z" />
-
    <path d="M12 10V11H13V10H12Z" />
-
    <path d="M13 7V8H14V7H13Z" />
-
    <path d="M6 4L4 4L4 3L6 3V4Z" />
-
    <path d="M3 8L3 10H2L2 8H3Z" />
-
    <path d="M8 3L6 3L6 2L8 2V3Z" />
-
    <path d="M8 3H9V2L8 2V3Z" />
-
    <path d="M11 4H12V5H11V4Z" />
-
    <path d="M12 3H13V4L12 4V3Z" />
-
    <path d="M11 6H12V7H11V6Z" />
-
    <path d="M12 5L13 5V6H12V5Z" />
-
    <path d="M13 4H14V5H13V4Z" />
-
    <path d="M10 7L11 7V8H10V7Z" />
-
    <path d="M7 6H8V7H7V6Z" />
-
    <path d="M8 5H9V6H8V5Z" />
-
    <path d="M9 4H10V5L9 5V4Z" />
-
    <path d="M10 3H11V4L10 4L10 3Z" />
-
    <path d="M11 2H12V3L11 3V2Z" />
-
    <path d="M9 8L10 8L10 9H9V8Z" />
-
    <path d="M7 9H8V10H7V9Z" />
-
    <path d="M7 8H8V9H7V8Z" />
-
    <path d="M6 8H7V9H6V8Z" />
-
    <path d="M6 7H7V8H6V7Z" />
-
    <path d="M8 9H9V10L8 10V9Z" />
-
    <path d="M6 9H7V10H6V9Z" />
-
  {: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 === "seedling-filled"}
-
    <path d="M10 6H9V5L10 5V6Z" />
-
    <path d="M11 5L10 5V4L11 4V5Z" />
-
    <path d="M12 4H11L11 3L12 3L12 4Z" />
-
    <path d="M13 4L12 4L12 3L13 3L13 4Z" />
-
    <path d="M14 5L13 5V4L14 4V5Z" />
-
    <path d="M13 6H12V5H13V6Z" />
-
    <path d="M5 7L5 6L4 6L4 7L5 7Z" />
-
    <path d="M4 8V7L3 7L3 8L4 8Z" />
-
    <path d="M3 9L3 8L2 8L2 9H3Z" />
-
    <path d="M3 10L3 9H2L2 10H3Z" />
-
    <path d="M4 11L4 10H3L3 11L4 11Z" />
-
    <path d="M10 7H9L9 6H10L10 7Z" />
-
    <path d="M9 8H8V7L9 7L9 8Z" />
-
    <path d="M8 8H7V7H8V8Z" />
-
    <path d="M11 8L10 8L10 7L11 7V8Z" />
-
    <path d="M12 8L10 8L10 7L12 7V8Z" />
-
    <path d="M13 7H12V6H13V7Z" />
-
    <path d="M5 10L5 9H4L4 10L5 10Z" />
-
    <path d="M9 8H8V11H9V8Z" />
-
    <path d="M8 11L7 11V14H8V11Z" />
-
    <path d="M6 7L6 6H5L5 7H6Z" />
-
    <path d="M7 8V7L6 7L6 8H7Z" />
-
    <path d="M7 9L7 7L6 7L6 9H7Z" />
-
    <path d="M6 10V9H5L5 10H6Z" />
-
    <path d="M4 7L6 7L6 9H4L4 7Z" />
-
    <path d="M3 8L4 8L4 10H3L3 8Z" />
-
    <path d="M11 4L13 4V5L11 5V4Z" />
-
    <path d="M10 5L12 5V7L10 7L10 5Z" />
+
    <path
+
      d="M8 2V3H3V13H13V9.20703L11.8535 10.3535L11.1465 9.64648L13.5 7.29297L15.8535 9.64648L15.1465 10.3535L14 9.20703V14H2V2H8Z" />
+
    <path
+
      d="M13.4141 5L7.41406 11H5V8.58594L11 2.58594L13.4141 5ZM6 10H7L12 5L11 4L6 9V10Z" />
+
    <path
+
      d="M11.3535 5.64648L10.6465 6.35352L9.64648 5.35352L10.3535 4.64648L11.3535 5.64648Z" />
+
  {:else if name === "sad-emoji"}
+
    <path
+
      d="M13.5 8C13.5 4.96243 11.0376 2.5 8 2.5C4.96243 2.5 2.5 4.96243 2.5 8C2.5 11.0376 4.96243 13.5 8 13.5C11.0376 13.5 13.5 11.0376 13.5 8ZM14.5 8C14.5 11.5899 11.5899 14.5 8 14.5C4.41015 14.5 1.5 11.5899 1.5 8C1.5 4.41015 4.41015 1.5 8 1.5C11.5899 1.5 14.5 4.41015 14.5 8Z" />
+
    <path
+
      d="M7.99951 8.5C8.92777 8.5 9.81874 8.86901 10.4751 9.52539C10.8474 9.89783 11.127 10.3457 11.2993 10.833L10.3569 11.167C10.2339 10.8188 10.0333 10.4986 9.76709 10.2324C9.29828 9.76367 8.66249 9.5 7.99951 9.5C7.33658 9.50008 6.7007 9.76365 6.23193 10.2324C5.96583 10.4986 5.76619 10.8188 5.64307 11.167L4.69971 10.833C4.87207 10.3456 5.15238 9.89796 5.5249 9.52539C6.18121 8.86908 7.07136 8.50008 7.99951 8.5Z" />
+
    <path
+
      d="M6 7C6.27614 7 6.5 6.77614 6.5 6.5C6.5 6.22386 6.27614 6 6 6C5.72386 6 5.5 6.22386 5.5 6.5C5.5 6.77614 5.72386 7 6 7Z" />
+
    <path
+
      d="M10 7C10.2761 7 10.5 6.77614 10.5 6.5C10.5 6.22386 10.2761 6 10 6C9.72386 6 9.5 6.22386 9.5 6.5C9.5 6.77614 9.72386 7 10 7Z" />
+
    <path
+
      d="M6.25 6.5C6.25 6.36193 6.13807 6.25 6 6.25C5.86193 6.25 5.75 6.36193 5.75 6.5C5.75 6.63807 5.86193 6.75 6 6.75C6.13807 6.75 6.25 6.63807 6.25 6.5ZM10.25 6.5C10.25 6.36193 10.1381 6.25 10 6.25C9.86193 6.25 9.75 6.36193 9.75 6.5C9.75 6.63807 9.86193 6.75 10 6.75C10.1381 6.75 10.25 6.63807 10.25 6.5ZM6.75 6.5C6.75 6.91421 6.41421 7.25 6 7.25C5.58579 7.25 5.25 6.91421 5.25 6.5C5.25 6.08579 5.58579 5.75 6 5.75C6.41421 5.75 6.75 6.08579 6.75 6.5ZM10.75 6.5C10.75 6.91421 10.4142 7.25 10 7.25C9.58579 7.25 9.25 6.91421 9.25 6.5C9.25 6.08579 9.58579 5.75 10 5.75C10.4142 5.75 10.75 6.08579 10.75 6.5Z" />
+
  {:else if name === "search"}
+
    <path
+
      d="M12 7.5C12 5.567 10.433 4 8.5 4C6.567 4 5 5.567 5 7.5C5 9.433 6.567 11 8.5 11V12C6.01472 12 4 9.98528 4 7.5C4 5.01472 6.01472 3 8.5 3C10.9853 3 13 5.01472 13 7.5C13 9.98528 10.9853 12 8.5 12V11C10.433 11 12 9.433 12 7.5Z" />
+
    <path
+
      d="M5.85352 10.8535L3.35352 13.3535L2.64648 12.6465L5.14648 10.1465L5.85352 10.8535Z" />
+
  {:else if name === "seed"}
+
    <path
+
      d="M12.3535 6.35352L7.5 11.207V14H6.5V11.207L4.64648 9.35352L5.35352 8.64648L7 10.293L11.6465 5.64648L12.3535 6.35352Z" />
+
    <path
+
      d="M14 6.5V4H11.5C10.1193 4 9 5.11929 9 6.5C9 7.88071 10.1193 9 11.5 9V10C9.567 10 8 8.433 8 6.5C8 4.567 9.567 3 11.5 3H15V6.5C15 8.433 13.433 10 11.5 10V9C12.8807 9 14 7.88071 14 6.5Z" />
+
    <path
+
      d="M5 7.5C5 6.67157 4.32843 6 3.5 6C2.67157 6 2 6.67157 2 7.5C2 8.32843 2.67157 9 3.5 9V10C2.11929 10 1 8.88071 1 7.5C1 6.11929 2.11929 5 3.5 5C4.88071 5 6 6.11929 6 7.5C6 8.88071 4.88071 10 3.5 10V9C4.32843 9 5 8.32843 5 7.5Z" />
+
  {:else if name === "seed-filled"}
+
    <path
+
      d="M12.3535 6.35352L7.5 11.207V14H6.5V11.207L4.64648 9.35352L5.35352 8.64648L7 10.293L11.6465 5.64648L12.3535 6.35352Z" />
+
    <path
+
      d="M15 6.5C15 8.433 13.433 10 11.5 10C9.567 10 8 8.433 8 6.5C8 4.567 9.567 3 11.5 3H15V6.5Z" />
+
    <path
+
      d="M14 6.5V4H11.5C10.1193 4 9 5.11929 9 6.5C9 7.88071 10.1193 9 11.5 9V10C9.567 10 8 8.433 8 6.5C8 4.567 9.567 3 11.5 3H15V6.5C15 8.433 13.433 10 11.5 10V9C12.8807 9 14 7.88071 14 6.5Z" />
+
    <path
+
      d="M3.5 5C4.88071 5 6 6.11929 6 7.5C6 8.88071 4.88071 10 3.5 10C2.11929 10 1 8.88071 1 7.5C1 6.11929 2.11929 5 3.5 5Z" />
+
    <path
+
      d="M5 7.5C5 6.67157 4.32843 6 3.5 6C2.67157 6 2 6.67157 2 7.5C2 8.32843 2.67157 9 3.5 9V10C2.11929 10 1 8.88071 1 7.5C1 6.11929 2.11929 5 3.5 5C4.88071 5 6 6.11929 6 7.5C6 8.88071 4.88071 10 3.5 10V9C4.32843 9 5 8.32843 5 7.5Z" />
  {: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" />
+
    <path
+
      d="M5.5 10V14H4.5V10H5.5ZM3.5 11.5V12.5H2V11.5H3.5ZM14 11.5V12.5H6.5V11.5H14ZM11.5 6V10H10.5V6H11.5ZM9.5 7.5V8.5H2V7.5H9.5ZM14 7.5V8.5H12.5V7.5H14ZM5.5 2V6H4.5V2H5.5ZM3.5 3.5V4.5H2V3.5H3.5ZM14 3.5V4.5H6.5V3.5H14Z" />
+
  {:else if name === "share"}
+
    <path
+
      d="M7.5 4.20703L5 6.70703L4.29297 6L8 2.29297L11.707 6L11 6.70703L8.5 4.20703V11.5H7.5V4.20703Z" />
+
    <path d="M3.5 11V13.5H12.5V11H13.5V14.5H2.5V11H3.5Z" />
+
  {:else if name === "sidebar-left"}
+
    <path
+
      d="M14.5 2.5V13.5H1.5V2.5H14.5ZM6.5 12.5H13.5V3.5H6.5V12.5ZM2.5 12.5H5.5V3.5H2.5V12.5Z" />
+
  {:else if name === "sidebar-left-filled"}
+
    <path
+
      d="M14.5 2.5V13.5H1.5V2.5H14.5ZM6.5 12.5H13.5V3.5H6.5V12.5ZM2.5 12.5H5.5V3.5H2.5V12.5Z" />
+
    <path d="M6 3H2V13H6V3Z" />
+
  {:else if name === "sidebar-right"}
+
    <path
+
      d="M14.5 2.5V13.5H1.5V2.5H14.5ZM10.5 12.5H13.5V3.5H10.5V12.5ZM2.5 12.5H9.5V3.5H2.5V12.5Z" />
+
  {:else if name === "sidebar-right-filled"}
+
    <path
+
      d="M14.5 2.5V13.5H1.5V2.5H14.5ZM10.5 12.5H13.5V3.5H10.5V12.5ZM2.5 12.5H9.5V3.5H2.5V12.5Z" />
+
    <path d="M14 3H10V13H14V3Z" />
  {:else if name === "stop"}
-
    <path d="M3.5 5L3.5 13H2.5L2.5 5H3.5Z" />
-
    <path d="M5.5 4L5.5 8H4.5L4.5 4H5.5Z" />
-
    <path d="M7.5 3L7.5 8H6.5L6.5 3L7.5 3Z" />
-
    <path d="M3.5 13L12.5 13V14L3.5 14L3.5 13Z" />
-
    <path d="M3.5 4H5.5V5H3.5L3.5 4Z" />
-
    <path d="M12.5 8L13.5 8L13.5 12H12.5L12.5 8Z" />
-
    <path d="M11.5 7H12.5V8H11.5V7Z" />
-
    <path d="M8.5 3H9.5V8H8.5V3Z" />
-
    <path d="M10.5 4H11.5L11.5 10H10.5L10.5 4Z" />
-
    <path d="M5.5 3L6.5 3L6.5 4H5.5L5.5 3Z" />
-
    <path d="M7.5 2H8.5V3L7.5 3V2Z" />
-
    <path d="M9.5 3L10.5 3V4L9.5 4V3Z" />
-
    <path d="M11.5 12L12.5 12V13H11.5V12Z" />
+
    <path
+
      d="M10.707 1.49994L14.5 5.29291V10.707L10.707 14.4999H5.29297L1.5 10.707V5.29291L5.29297 1.49994H10.707ZM2.5 5.70697V10.2929L5.70703 13.4999H10.293L13.5 10.2929V5.70697L10.293 2.49994H5.70703L2.5 5.70697Z" />
+
    <path
+
      d="M9 11C9 11.5523 8.55228 12 8 12C7.44772 12 7 11.5523 7 11C7 10.4477 7.44772 10 8 10C8.55228 10 9 10.4477 9 11Z" />
+
    <path d="M8.75 4H7.25L7.5 9H8.5L8.75 4Z" />
  {: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 === "thumb-up"}
-
    <path d="M4 13L11 13V14L4 14V13Z" />
-
    <path d="M3 11H7V12H3L3 11Z" />
-
    <path d="M3 9H7V10H3V9Z" />
-
    <path d="M12 12L12 5L13 5L13 12H12Z" />
-
    <path d="M11 5L11 4H12V5L11 5Z" />
-
    <path d="M10 4V2L11 2V4L10 4Z" />
-
    <path d="M9 5V3H10V5L9 5Z" />
-
    <path d="M9 8L9 4L10 4V8H9Z" />
-
    <path d="M3 8L3 7L7 7V8H3Z" />
-
    <path d="M4 6V5L9 5V6L4 6Z" />
-
    <path d="M3 12L3 11H4L4 12H3Z" />
-
    <path d="M3 13L3 6H4L4 13L3 13Z" />
-
    <path d="M9 2L10 2V3H9V2Z" />
-
    <path d="M11 12H12L12 13L11 13L11 12Z" />
-
    <path d="M7 8H8V9H7V8Z" />
-
    <path d="M7 6H8V7H7L7 6Z" />
-
    <path d="M7 10L8 10V11L7 11L7 10Z" />
-
    <path d="M7 12L8 12L8 13H7L7 12Z" />
+
    <path
+
      d="M10.5 8C10.5 6.61929 9.38071 5.5 8 5.5C6.61929 5.5 5.5 6.61929 5.5 8C5.5 9.38071 6.61929 10.5 8 10.5C9.38071 10.5 10.5 9.38071 10.5 8ZM11.5 8C11.5 9.933 9.933 11.5 8 11.5C6.067 11.5 4.5 9.933 4.5 8C4.5 6.067 6.067 4.5 8 4.5C9.933 4.5 11.5 6.067 11.5 8Z" />
+
    <path d="M8.5 1V3.5H7.5V1H8.5Z" />
+
    <path
+
      d="M3.40535 2.70024L5.17312 4.46801L4.46601 5.17512L2.69824 3.40735L3.40535 2.70024Z" />
+
    <path
+
      d="M1.00488 7.505L3.50488 7.505L3.50488 8.505L1.00488 8.505L1.00488 7.505Z" />
+
    <path
+
      d="M2.705 12.5997L4.47277 10.832L5.17988 11.5391L3.41211 13.3069L2.705 12.5997Z" />
+
    <path d="M8.5 12.5V15H7.5V12.5H8.5Z" />
+
    <path
+
      d="M11.5372 10.832L13.305 12.5997L12.5978 13.3068L10.8301 11.5391L11.5372 10.832Z" />
+
    <path
+
      d="M12.5049 7.505L15.0049 7.505L15.0049 8.505L12.5049 8.505L12.5049 7.505Z" />
+
    <path
+
      d="M10.8368 4.468L12.6046 2.70024L13.3117 3.40734L11.5439 5.17511L10.8368 4.468Z" />
+
  {:else if name === "thumbs-up"}
+
    <path
+
      d="M14.5 7.5H8.5V2.5H7.36035L5.5 8.08203V13.5H12.6914L14.5 9.88184V7.5ZM2.5 13.5H4.5V8.5H2.5V13.5ZM15.5 10.1182L13.4473 14.2236L13.3086 14.5H1.5V7.5H4.63965L6.63965 1.5H9.5V6.5H15.5V10.1182Z" />
  {:else if name === "trash"}
-
    <path d="M6.5 2L9.5 2V3L6.5 3V2Z" />
-
    <path d="M3.5 5H4.5L4.5 13H3.5L3.5 5Z" />
-
    <path d="M4.5 13L11.5 13V14L4.5 14V13Z" />
-
    <path d="M11.5 5L12.5 5L12.5 13H11.5L11.5 5Z" />
-
    <path d="M2.5 3L13.5 3V4L2.5 4L2.5 3Z" />
-
    <path d="M9.5 5H10.5L10.5 12L9.5 12V5Z" />
-
    <path d="M7.5 5L8.5 5V12H7.5L7.5 5Z" />
-
    <path d="M5.5 5H6.5L6.5 12H5.5L5.5 5Z" />
-
  {:else if name === "unresolve"}
-
    <path d="M7 11.5V12.5H6V11.5H7Z" />
-
    <path d="M8 10.5V11.5L7 11.5L7 10.5H8Z" />
-
    <path d="M10 8.5V9.5H9V8.5H10Z" />
-
    <path d="M11 7.5V8.5L10 8.5L10 7.5H11Z" />
-
    <path d="M10 7.5L10 8.5H9L9 7.5L10 7.5Z" />
-
    <path d="M12 6.5V7.5H11V6.5H12Z" />
-
    <path d="M13 5.5V6.5L12 6.5V5.5L13 5.5Z" />
-
    <path d="M4 8.5V9.5H3L3 8.5H4Z" />
-
    <path d="M5 8.5L5 9.5H4V8.5H5Z" />
-
    <path d="M6 9.5L6 10.5H5V9.5H6Z" />
-
    <path d="M7 10.5L7 11.5H6L6 10.5H7Z" />
-
    <path d="M8 9.5V10.5H7V9.5H8Z" />
-
    <path d="M11 6.5V7.5H10V6.5H11Z" />
-
    <path d="M12 5.5V6.5H11V5.5H12Z" />
-
    <path d="M5 9.5V10.5H4L4 9.5H5Z" />
-
    <path d="M6 10.5L6 11.5H5L5 10.5H6Z" />
-
    <path d="M12 11.5H13V12.5H12V11.5Z" />
-
    <path d="M11 10.5H12L12 11.5H11V10.5Z" />
-
    <path d="M10 9.5L11 9.5V10.5H10V9.5Z" />
-
    <path d="M9 8.5H10V9.5H9V8.5Z" />
-
    <path d="M8 7.5L9 7.5L9 8.5H8V7.5Z" />
-
    <path d="M7 6.5H8V7.5H7V6.5Z" />
-
    <path d="M6 5.5H7V6.5H6V5.5Z" />
-
    <path d="M5 4.5H6V5.5L5 5.5V4.5Z" />
-
    <path d="M4 3.5L5 3.5V4.5L4 4.5V3.5Z" />
-
  {:else if name === "user"}
-
    <path d="M5 3H6V4H5V3Z" />
-
    <path d="M5 6L5 8H4V6H5Z" />
-
    <path d="M12 4V8H11V4H12Z" />
-
    <path d="M4 4L5 4L5 6H4L4 4Z" />
-
    <path d="M10 3H11V4L10 4V3Z" />
-
    <path d="M6 2H10V3L6 3V2Z" />
-
    <path d="M5 8H6V9H5L5 8Z" />
-
    <path d="M6 9H10V10H6V9Z" />
-
    <path d="M10 8L11 8V9L10 9L10 8Z" />
-
    <path d="M9 9H10V10H9V9Z" />
-
    <path d="M10 10H12V11H10L10 10Z" />
-
    <path d="M4 10H6L6 11H4V10Z" />
-
    <path d="M12 11H13V12H12V11Z" />
-
    <path d="M3 11H4L4 12H3V11Z" />
-
    <path d="M13 12H14V13H13V12Z" />
-
    <path d="M2 12H3L3 13H2V12Z" />
-
    <path d="M2 13H14V14H2L2 13Z" />
-
    <path d="M5 3L11 3V5H5V3Z" />
-
    <path d="M9 6H10V7H9V6Z" />
-
    <path d="M6 6H7V7H6V6Z" />
+
    <path
+
      d="M13.5 3.5V4.5H12.5V13.5H3.5V4.5H2.5V3.5H13.5ZM4.5 12.5H11.5V4.5H4.5V12.5Z" />
+
    <path d="M10.5 1.5V3.5H9.5V2.5H6.5V3.5H5.5V1.5H10.5Z" />
+
    <path d="M6 6H7V11H6V6Z" />
+
    <path d="M9 6H10V11H9V6Z" />
  {: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" />
+
    <path
+
      d="M8 12.5C8.41421 12.5 8.75 12.1642 8.75 11.75C8.75 11.3358 8.41421 11 8 11C7.58579 11 7.25 11.3358 7.25 11.75C7.25 12.1642 7.58579 12.5 8 12.5Z" />
+
    <path d="M8.5 6H7.5V10H8.5V6Z" />
+
    <path
+
      d="M8.43311 1.75L15.3608 13.75L14.9282 14.5H1.07178L0.63916 13.75L7.56689 1.75H8.43311ZM1.93701 13.5H14.063L7.99951 3L1.93701 13.5Z" />
+
  {:else if name === "webhooks"}
+
    <path
+
      d="M12 9.50024C12.5522 9.50024 13 9.948 13 10.5002C12.9999 11.0524 12.5522 11.5002 12 11.5002C11.6303 11.5002 11.3088 11.2986 11.1357 11.0002H6.95508C6.7103 12.3955 5.46657 13.5002 4 13.5002C2.36685 13.5002 1.00013 12.1171 1 10.5002C1.00001 9.85741 1.21153 9.2181 1.59961 8.70044L2.40039 9.30005C2.14234 9.64412 2.00001 10.0729 2 10.5002C2.00013 11.5682 2.92246 12.5002 4 12.5002C5.08253 12.5002 5.99986 11.5733 6 10.5002V10.0002H11.1348C11.3077 9.70148 11.63 9.50024 12 9.50024ZM13.9971 10.5002C13.9971 9.43213 13.0763 8.50138 11.999 8.50024C11.6313 8.49995 11.2638 8.60265 10.9492 8.79614L10.5234 9.05884L10.2617 8.63208L8.02637 4.99829C8.01759 4.99852 8.00883 5.00024 8 5.00024C7.4478 5.00024 7.00013 4.55241 7 4.00024C7 3.44796 7.44772 3.00024 8 3.00024C8.55228 3.00024 9 3.44796 9 4.00024C8.99996 4.17255 8.95568 4.33432 8.87891 4.47583L10.874 7.71997C11.2315 7.57582 11.6153 7.49984 12.001 7.50024C13.6331 7.50222 14.9971 8.88357 14.9971 10.5002C14.9969 12.1381 13.6232 13.4983 12.001 13.5002L11.999 12.5002C13.0728 12.4991 13.9969 11.5832 13.9971 10.5002ZM8 1.0022C9.18186 1.00184 10.2762 1.71917 10.749 2.79907L10.291 3.00024L9.83301 3.20044C9.51933 2.48379 8.78412 2.00191 8 2.0022C6.93263 2.00269 6.00106 2.92457 6 4.00024C5.99941 4.67586 6.37247 5.34764 6.9502 5.70435L7.375 5.96606L4.87891 10.0237C4.95585 10.1654 5 10.3277 5 10.5002C4.99987 11.0523 4.55223 11.5002 4 11.5002C3.44777 11.5002 3.00013 11.0523 3 10.5002C3 9.94803 3.44769 9.50024 4 9.50024C4.00882 9.50024 4.0176 9.50099 4.02637 9.50122L6.02344 6.25415C5.38644 5.68843 4.99925 4.85435 5 3.99927C5.0016 2.36825 6.38432 1.00279 8 1.0022Z" />
  {:else}
    {unreachable(name)}
  {/if}
modified src/components/Id.svelte
@@ -1,9 +1,18 @@
<script lang="ts">
+
  import type { Placement } from "@floating-ui/dom";
  import type { ComponentProps } from "svelte";

+
  import {
+
    autoUpdate,
+
    computePosition,
+
    flip,
+
    offset,
+
    shift,
+
  } from "@floating-ui/dom";
  import debounce from "lodash/debounce";

  import { writeToClipboard } from "@app/lib/invoke";
+
  import { portal } from "@app/lib/portal";
  import { formatOid } from "@app/lib/utils";

  import Icon from "@app/components/Icon.svelte";
@@ -25,54 +34,82 @@
  }

  let visible: boolean = $state(false);
+
  let anchorEl: HTMLDivElement | undefined = $state();
+
  let floatingEl: HTMLDivElement | undefined = $state();

  interface Props {
    ariaLabel?: string;
    clipboard: string;
    id: string;
+
    placement?: Placement;
    shorten?: boolean;
-
    variant: "oid" | "commit" | "none";
  }

-
  const { ariaLabel, clipboard, id, shorten = true, variant }: Props = $props();
+
  const {
+
    ariaLabel,
+
    clipboard,
+
    id,
+
    placement = "top-start",
+
    shorten = true,
+
  }: Props = $props();

  const setVisible = debounce((value: boolean) => {
    visible = value;
  }, 50);
+

+
  $effect(() => {
+
    // Re-run when tooltip text changes so the wider "Copied to clipboard"
+
    // text is repositioned and doesn't overflow the viewport edge.
+
    void tooltip;
+
    if (floatingEl && anchorEl) {
+
      const cleanup = autoUpdate(anchorEl, floatingEl, () => {
+
        void computePosition(anchorEl!, floatingEl!, {
+
          placement,
+
          middleware: [offset(6), flip(), shift({ padding: 8 })],
+
        }).then(({ x, y }) => {
+
          if (floatingEl) {
+
            floatingEl.style.left = `${x}px`;
+
            floatingEl.style.top = `${y}px`;
+
            floatingEl.style.visibility = "visible";
+
          }
+
        });
+
      });
+
      return cleanup;
+
    }
+
  });
</script>

<style>
  .container {
-
    position: relative;
    display: inline-block;
  }
  .popover {
-
    position: absolute;
+
    position: fixed;
+
    top: 0;
+
    left: 0;
+
    visibility: hidden;
    display: flex;
    align-items: center;
    flex-direction: row;
    gap: 0.5rem;
    justify-content: center;
    z-index: 20;
-
    background: var(--color-fill-counter);
-
    color: var(--color-foreground-contrast);
+
    background: var(--color-surface-subtle);
+
    color: var(--color-text-primary);
+
    border: 1px solid var(--color-border-subtle);
+
    border-radius: var(--border-radius-md);
    box-shadow: var(--elevation-low);
-
    font-family: var(--font-family-sans-serif);
-
    font-size: var(--font-size-small);
-
    font-weight: var(--font-weight-regular);
+
    font: var(--txt-body-m-regular);
    white-space: nowrap;
-
    padding: 0.125rem 0.5rem;
-
    clip-path: var(--1px-corner-fill);
-
  }
-
  .target-commit:hover {
-
    color: var(--color-foreground-contrast);
+
    width: max-content;
+
    padding: 0.25rem 0.5rem;
  }
-
  .target-oid:hover {
-
    color: var(--color-foreground-emphasized-hover);
+
  .txt-id:hover {
+
    color: var(--color-text-primary);
  }
</style>

-
<div class="container">
+
<div class="container" bind:this={anchorEl}>
  <!-- svelte-ignore a11y_click_events_have_key_events -->
  <div
    onmouseenter={() => {
@@ -81,7 +118,7 @@
    onmouseleave={() => {
      setVisible(false);
    }}
-
    class="target-{variant} global-{variant}"
+
    class="txt-id"
    style:cursor="pointer"
    aria-label={ariaLabel}
    onclick={async event => {
@@ -99,11 +136,9 @@
  </div>

  {#if visible}
-
    <div style:position="absolute">
-
      <div class="popover" style:bottom="1.5rem" style:left="1rem">
-
        <Icon name={icon} />
-
        {tooltip}
-
      </div>
+
    <div use:portal bind:this={floatingEl} class="popover">
+
      <Icon name={icon} />
+
      {tooltip}
    </div>
  {/if}
</div>
added src/components/IdentityButton.svelte
@@ -0,0 +1,73 @@
+
<script lang="ts">
+
  import type { Config } from "@bindings/config/Config";
+

+
  import debounce from "lodash/debounce";
+

+
  import { writeToClipboard } from "@app/lib/invoke";
+
  import { didFromPublicKey, explorerUrl, truncateDid } from "@app/lib/utils";
+

+
  import Button from "@app/components/Button.svelte";
+
  import DropdownListItem from "@app/components/DropdownListItem.svelte";
+
  import Icon from "@app/components/Icon.svelte";
+
  import { closeFocused } from "@app/components/Popover.svelte";
+
  import Popover from "@app/components/Popover.svelte";
+
  import UserAvatar from "@app/components/UserAvatar.svelte";
+

+
  interface Props {
+
    config: Config;
+
  }
+

+
  const { config }: Props = $props();
+

+
  let popoverExpanded: boolean = $state(false);
+
  let copyIcon: "copy" | "checkmark" = $state("copy");
+
  const restoreCopyIcon = debounce(() => {
+
    copyIcon = "copy";
+
  }, 1000);
+
</script>
+

+
<Popover placement="bottom-start" bind:expanded={popoverExpanded}>
+
  {#snippet toggle(onclick)}
+
    <Button variant="naked" active={popoverExpanded} {onclick}>
+
      <UserAvatar nodeId={config.publicKey} styleWidth="1rem" />
+
      {config.alias}
+
      <span style:color="var(--color-text-tertiary)">
+
        <Icon name={popoverExpanded ? "chevron-up" : "chevron-down"} />
+
      </span>
+
    </Button>
+
  {/snippet}
+
  {#snippet popover()}
+
    <div
+
      style:border="1px solid var(--color-border-subtle)"
+
      style:border-radius="var(--border-radius-md)"
+
      style:background-color="var(--color-surface-canvas)"
+
      style:padding="0.25rem">
+
      <DropdownListItem
+
        styleGap="0.5rem"
+
        styleWidth="100%"
+
        selected={false}
+
        onclick={async () => {
+
          await writeToClipboard(didFromPublicKey(config.publicKey));
+
          copyIcon = "checkmark";
+
          restoreCopyIcon();
+
          closeFocused();
+
        }}>
+
        <Icon name="avatar-incognito" />
+
        {truncateDid(config.publicKey)}
+
        <span style:margin-left="auto"><Icon name={copyIcon} /></span>
+
      </DropdownListItem>
+
      <a
+
        style:text-decoration="none"
+
        style:width="100%"
+
        onclick={closeFocused}
+
        href={explorerUrl(`users/${didFromPublicKey(config.publicKey)}`)}
+
        target="_blank">
+
        <DropdownListItem styleGap="0.5rem" styleWidth="100%" selected={false}>
+
          <Icon name="seed" />
+
          view on seed.radicle.garden
+
          <span style:margin-left="auto"><Icon name="open-external" /></span>
+
        </DropdownListItem>
+
      </a>
+
    </div>
+
  {/snippet}
+
</Popover>
deleted src/components/Inbox.svelte
@@ -1,187 +0,0 @@
-
<script lang="ts">
-
  import type { NotificationsByRepo } from "@bindings/cob/inbox/NotificationsByRepo";
-

-
  import ConfirmClear from "@app/components/ConfirmClear.svelte";
-
  import Icon from "@app/components/Icon.svelte";
-
  import NakedButton from "@app/components/NakedButton.svelte";
-
  import NotificationsByRepoComponent from "@app/components/NotificationsByRepo.svelte";
-

-
  interface Props {
-
    clearAll: () => Promise<void>;
-
    clearByIds: (ids: string[]) => Promise<void>;
-
    clearByRepo: (rid: string) => Promise<void>;
-
    loadNew: () => Promise<void>;
-
    notificationCount: number | undefined;
-
    notificationsByRepo: NotificationsByRepo[];
-
    showAll: (rid: string) => Promise<void>;
-
  }
-

-
  const {
-
    clearAll,
-
    clearByIds,
-
    clearByRepo,
-
    loadNew,
-
    notificationCount,
-
    notificationsByRepo,
-
    showAll,
-
  }: Props = $props();
-

-
  let pinnedRepos: string[] = $state(loadPinnedRepos());
-
  let hiddenRepos: string[] = $state(loadHiddenRepos());
-

-
  function loadPinnedRepos(): string[] {
-
    const storedPinnedRepos = localStorage
-
      ? localStorage.getItem("pinnedInboxRepos")
-
      : null;
-

-
    if (storedPinnedRepos === null) {
-
      return [];
-
    } else {
-
      return JSON.parse(storedPinnedRepos);
-
    }
-
  }
-

-
  function updatePinnedRepos(newRepos: string[]) {
-
    pinnedRepos = newRepos;
-
    localStorage.setItem("pinnedInboxRepos", JSON.stringify(newRepos));
-
  }
-

-
  function togglePin(rid: string) {
-
    const repos = loadPinnedRepos();
-
    if (repos.includes(rid)) {
-
      updatePinnedRepos(repos.filter(r => r !== rid));
-
    } else {
-
      updatePinnedRepos([rid, ...repos]);
-
    }
-
  }
-

-
  function loadHiddenRepos(): string[] {
-
    const storedHiddenRepos = localStorage
-
      ? localStorage.getItem("hiddenInboxRepos")
-
      : null;
-

-
    if (storedHiddenRepos === null) {
-
      return [];
-
    } else {
-
      return JSON.parse(storedHiddenRepos);
-
    }
-
  }
-

-
  function updateHiddenRepos(newRepos: string[]) {
-
    hiddenRepos = newRepos;
-
    localStorage.setItem("hiddenInboxRepos", JSON.stringify(newRepos));
-
  }
-

-
  function toggleHide(rid: string) {
-
    const repos = loadHiddenRepos();
-
    if (repos.includes(rid)) {
-
      updateHiddenRepos(repos.filter(r => r !== rid));
-
    } else {
-
      updateHiddenRepos([rid, ...repos]);
-
    }
-
  }
-

-
  function sortedRepos(
-
    allRepos: NotificationsByRepo[],
-
    pinned: string[],
-
    hidden: string[],
-
  ) {
-
    // Preserve pinning order.
-
    const pinnedRepos = pinned
-
      .map(p => allRepos.find(r => r.rid === p))
-
      .filter((repo): repo is NotificationsByRepo => repo !== undefined);
-

-
    const sortedRepos = allRepos
-
      .filter(r => !pinned.includes(r.rid) && !hidden.includes(r.rid))
-
      .sort((a, b) => a.name.localeCompare(b.name));
-
    const hiddenRepos = allRepos
-
      .filter(r => hidden.includes(r.rid))
-
      .sort((a, b) => a.name.localeCompare(b.name));
-

-
    return [...pinnedRepos, ...sortedRepos, ...hiddenRepos];
-
  }
-

-
  function loadedNotificationCount() {
-
    return notificationsByRepo.reduce((acc, repo) => {
-
      return acc + repo.count;
-
    }, 0);
-
  }
-
</script>
-

-
<style>
-
  .container {
-
    width: 100%;
-
  }
-
  .header {
-
    font-weight: var(--font-weight-medium);
-
    font-size: var(--font-size-medium);
-
    display: flex;
-
    align-items: center;
-
    min-height: 2rem;
-
  }
-
  .clear-inbox {
-
    margin-left: auto;
-
    margin-right: 1rem;
-
    display: none;
-
  }
-
  .header:hover .clear-inbox {
-
    display: flex;
-
  }
-
  .repo-list {
-
    display: flex;
-
    flex-direction: column;
-
    gap: 1rem;
-
    margin-top: 1rem;
-
  }
-
</style>
-

-
<div class="container">
-
  <div class="header">
-
    <div>
-
      Inbox
-
      {#if notificationCount !== undefined && notificationCount > 0}
-
        {notificationCount}
-
      {/if}
-
    </div>
-
    {#if notificationCount === undefined || notificationCount === 0}
-
      <div
-
        class="txt-missing txt-small global-flex"
-
        style:gap="0.25rem"
-
        style:margin-left="auto">
-
        <Icon name="thumb-up" />
-
        Yay, inbox zero!
-
      </div>
-
    {/if}
-
    {#if notificationCount !== undefined && notificationCount > loadedNotificationCount()}
-
      <div class="txt-missing txt-small global-flex" style:margin-left="1rem">
-
        <NakedButton variant="ghost" onclick={loadNew}>
-
          See {notificationCount - loadedNotificationCount()} new
-
        </NakedButton>
-
      </div>
-
    {/if}
-
    {#if notificationCount && notificationCount > 0}
-
      <div class="clear-inbox">
-
        <ConfirmClear count={notificationCount} clear={clearAll} />
-
      </div>
-
    {/if}
-
  </div>
-

-
  {#if notificationCount !== undefined && notificationCount > 0}
-
    <div class="repo-list">
-
      {#each sortedRepos(notificationsByRepo, pinnedRepos, hiddenRepos) as repo}
-
        <NotificationsByRepoComponent
-
          count={repo.count}
-
          groupedNotifications={repo.notifications}
-
          hidden={hiddenRepos.includes(repo.rid)}
-
          name={repo.name}
-
          pinned={pinnedRepos.includes(repo.rid)}
-
          rid={repo.rid}
-
          {clearByIds}
-
          {clearByRepo}
-
          {showAll}
-
          {toggleHide}
-
          {togglePin} />
-
      {/each}
-
    </div>
-
  {/if}
-
</div>
deleted src/components/InboxButton.svelte
@@ -1,162 +0,0 @@
-
<script lang="ts">
-
  import type { NotificationsByRepo } from "@bindings/cob/inbox/NotificationsByRepo";
-

-
  import { getCurrentWindow } from "@tauri-apps/api/window";
-
  import { useOverlayScrollbars } from "overlayscrollbars-svelte";
-
  import { onMount } from "svelte";
-

-
  import { dynamicInterval } from "@app/lib/interval";
-
  import { invoke } from "@app/lib/invoke";
-

-
  import Border from "@app/components/Border.svelte";
-
  import Icon from "@app/components/Icon.svelte";
-
  import Inbox from "@app/components/Inbox.svelte";
-
  import OutlineButton from "@app/components/OutlineButton.svelte";
-
  import Popover from "@app/components/Popover.svelte";
-

-
  interface Props {
-
    notificationCount: number;
-
  }
-

-
  let { notificationCount }: Props = $props();
-

-
  let borderComponent: HTMLElement | undefined = $state();
-
  let notificationPopoverExpaneded: boolean = $state(false);
-
  let buttonActive: boolean = $state(false);
-

-
  $effect(() => {
-
    if (notificationPopoverExpaneded === false) {
-
      buttonActive = false;
-
    }
-
  });
-

-
  $effect(() => {
-
    if (borderComponent) {
-
      const [initialize] = useOverlayScrollbars({
-
        options: () => ({
-
          scrollbars: { theme: "global-os-theme-radicle", autoHide: "scroll" },
-
        }),
-
        defer: true,
-
      });
-

-
      initialize({ target: borderComponent });
-
    }
-
  });
-

-
  onMount(async () => {
-
    await loadCounter();
-
  });
-

-
  dynamicInterval("auth", loadCounter, 3_000);
-

-
  async function loadCounter() {
-
    notificationCount = await invoke<number>("notification_count");
-
    if (window.__TAURI_INTERNALS__) {
-
      await getCurrentWindow().setBadgeCount(
-
        notificationCount === 0 ? undefined : notificationCount,
-
      );
-
    }
-
  }
-

-
  let notificationsByRepo: NotificationsByRepo[] = $state([]);
-

-
  async function loadNotifications() {
-
    notificationsByRepo = await invoke<NotificationsByRepo[]>(
-
      "list_notifications",
-
      { params: { take: 100 } },
-
    );
-
  }
-

-
  async function clearAll() {
-
    try {
-
      await invoke("clear_notifications", {
-
        params: { type: "all" },
-
      });
-
    } catch (error) {
-
      console.error("Clearing notifications failed", error);
-
    } finally {
-
      await loadCounter();
-
      await loadNotifications();
-
    }
-
  }
-

-
  async function clearByRepo(rid: string) {
-
    try {
-
      await invoke("clear_notifications", {
-
        params: { type: "repo", content: rid },
-
      });
-
    } catch (error) {
-
      console.error("Clearing notifications failed", error);
-
    } finally {
-
      await loadCounter();
-
      await loadNotifications();
-
    }
-
  }
-

-
  async function clearByIds(ids: string[]) {
-
    try {
-
      await invoke("clear_notifications", {
-
        params: { type: "ids", content: ids },
-
      });
-
    } catch (error) {
-
      console.error("Clearing notifications failed", error);
-
    } finally {
-
      await loadCounter();
-
      await loadNotifications();
-
    }
-
  }
-

-
  async function showAll(rid: string) {
-
    const allNotificationsForRepo = await invoke<NotificationsByRepo[]>(
-
      "list_notifications",
-
      { params: { repos: [rid] } },
-
    );
-
    notificationsByRepo = [
-
      ...notificationsByRepo.filter(r => r.rid !== rid),
-
      ...allNotificationsForRepo,
-
    ];
-
  }
-
</script>
-

-
<Popover
-
  popoverPositionRight="0"
-
  popoverPositionTop="3rem"
-
  bind:expanded={notificationPopoverExpaneded}>
-
  {#snippet toggle(onclick)}
-
    <OutlineButton
-
      onclick={async () => {
-
        buttonActive = true;
-
        await loadNotifications();
-
        onclick();
-
      }}
-
      variant={notificationCount && notificationCount > 0
-
        ? "secondary"
-
        : "ghost"}
-
      active={buttonActive}>
-
      <Icon name="inbox" />
-
      {#if notificationCount !== undefined && notificationCount > 0}
-
        {notificationCount}
-
      {/if}
-
    </OutlineButton>
-
  {/snippet}
-

-
  {#snippet popover()}
-
    <Border
-
      bind:innerElement={borderComponent}
-
      variant="ghost"
-
      styleWidth="40rem"
-
      stylePadding="1rem"
-
      styleAlignItems="flex-start"
-
      styleOverflow="auto"
-
      styleMaxHeight="calc(100vh - 5rem)">
-
      <Inbox
-
        {clearAll}
-
        {clearByIds}
-
        {clearByRepo}
-
        loadNew={loadNotifications}
-
        {notificationCount}
-
        {notificationsByRepo}
-
        {showAll} />
-
    </Border>
-
  {/snippet}
-
</Popover>
added src/components/InboxList.svelte
@@ -0,0 +1,179 @@
+
<script lang="ts">
+
  import type { NotificationsByRepo } from "@bindings/cob/inbox/NotificationsByRepo";
+

+
  import Button from "@app/components/Button.svelte";
+
  import ConfirmClear from "@app/components/ConfirmClear.svelte";
+
  import NotificationsByRepoComponent from "@app/components/NotificationsByRepo.svelte";
+

+
  interface Props {
+
    clearAll: () => Promise<void>;
+
    clearByIds: (ids: string[]) => Promise<void>;
+
    clearByRepo: (rid: string) => Promise<void>;
+
    loadNew: () => Promise<void>;
+
    notificationCount: number | undefined;
+
    notificationsByRepo: NotificationsByRepo[];
+
    showAll: (rid: string) => Promise<void>;
+
  }
+

+
  const {
+
    clearAll,
+
    clearByIds,
+
    clearByRepo,
+
    loadNew,
+
    notificationCount,
+
    notificationsByRepo,
+
    showAll,
+
  }: Props = $props();
+

+
  let pinnedRepos: string[] = $state(loadPinnedRepos());
+
  let hiddenRepos: string[] = $state(loadHiddenRepos());
+

+
  function loadPinnedRepos(): string[] {
+
    const storedPinnedRepos = localStorage
+
      ? localStorage.getItem("pinnedInboxRepos")
+
      : null;
+

+
    if (storedPinnedRepos === null) {
+
      return [];
+
    } else {
+
      return JSON.parse(storedPinnedRepos);
+
    }
+
  }
+

+
  function updatePinnedRepos(newRepos: string[]) {
+
    pinnedRepos = newRepos;
+
    localStorage.setItem("pinnedInboxRepos", JSON.stringify(newRepos));
+
  }
+

+
  function togglePin(rid: string) {
+
    const repos = loadPinnedRepos();
+
    if (repos.includes(rid)) {
+
      updatePinnedRepos(repos.filter(r => r !== rid));
+
    } else {
+
      updatePinnedRepos([rid, ...repos]);
+
    }
+
  }
+

+
  function loadHiddenRepos(): string[] {
+
    const storedHiddenRepos = localStorage
+
      ? localStorage.getItem("hiddenInboxRepos")
+
      : null;
+

+
    if (storedHiddenRepos === null) {
+
      return [];
+
    } else {
+
      return JSON.parse(storedHiddenRepos);
+
    }
+
  }
+

+
  function updateHiddenRepos(newRepos: string[]) {
+
    hiddenRepos = newRepos;
+
    localStorage.setItem("hiddenInboxRepos", JSON.stringify(newRepos));
+
  }
+

+
  function toggleHide(rid: string) {
+
    const repos = loadHiddenRepos();
+
    if (repos.includes(rid)) {
+
      updateHiddenRepos(repos.filter(r => r !== rid));
+
    } else {
+
      updateHiddenRepos([rid, ...repos]);
+
    }
+
  }
+

+
  function sortedRepos(
+
    allRepos: NotificationsByRepo[],
+
    pinned: string[],
+
    hidden: string[],
+
  ) {
+
    // Preserve pinning order.
+
    const pinnedRepos = pinned
+
      .map(p => allRepos.find(r => r.rid === p))
+
      .filter((repo): repo is NotificationsByRepo => repo !== undefined);
+

+
    const sortedRepos = allRepos
+
      .filter(r => !pinned.includes(r.rid) && !hidden.includes(r.rid))
+
      .sort((a, b) => a.name.localeCompare(b.name));
+
    const hiddenRepos = allRepos
+
      .filter(r => hidden.includes(r.rid))
+
      .sort((a, b) => a.name.localeCompare(b.name));
+

+
    return [...pinnedRepos, ...sortedRepos, ...hiddenRepos];
+
  }
+

+
  function loadedNotificationCount() {
+
    return notificationsByRepo.reduce((acc, repo) => {
+
      return acc + repo.count;
+
    }, 0);
+
  }
+
</script>
+

+
<style>
+
  .container {
+
    width: 100%;
+
    display: flex;
+
    flex-direction: column;
+
    min-height: 100%;
+
  }
+
  .header {
+
    font: var(--txt-heading-m);
+
    display: flex;
+
    align-items: center;
+
    min-height: 2rem;
+
  }
+
  .clear-inbox {
+
    margin-left: auto;
+
    display: none;
+
    color: var(--color-text-tertiary);
+
  }
+
  .header:hover .clear-inbox {
+
    display: flex;
+
  }
+
  .repo-list {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 1rem;
+
    margin-top: 1rem;
+
  }
+
</style>
+

+
<div class="container">
+
  {#if notificationCount !== undefined && notificationCount > 0}
+
    <div class="header">
+
      <div class="global-flex">
+
        Inbox
+
        <span class="global-counter-badge">{notificationCount}</span>
+
      </div>
+
      {#if notificationCount > loadedNotificationCount()}
+
        <div
+
          class="txt-missing txt-body-m-regular global-flex"
+
          style:margin-left="1rem">
+
          <Button variant="naked" onclick={loadNew}>
+
            See {notificationCount - loadedNotificationCount()} new
+
          </Button>
+
        </div>
+
      {/if}
+
      <div class="clear-inbox">
+
        <ConfirmClear count={notificationCount} clear={clearAll} />
+
      </div>
+
    </div>
+
  {/if}
+

+
  {#if notificationCount !== undefined && notificationCount > 0}
+
    <div class="repo-list">
+
      {#each sortedRepos(notificationsByRepo, pinnedRepos, hiddenRepos) as repo}
+
        <NotificationsByRepoComponent
+
          count={repo.count}
+
          groupedNotifications={repo.notifications}
+
          hidden={hiddenRepos.includes(repo.rid)}
+
          name={repo.name}
+
          pinned={pinnedRepos.includes(repo.rid)}
+
          rid={repo.rid}
+
          {clearByIds}
+
          {clearByRepo}
+
          {showAll}
+
          {toggleHide}
+
          {togglePin} />
+
      {/each}
+
    </div>
+
  {/if}
+
</div>
modified src/components/InlineTitle.svelte
@@ -18,19 +18,19 @@

<style>
  .content :global(code) {
-
    font-family: var(--font-family-monospace);
+
    font-family: monospace;
    padding: 0.125rem 0.25rem;
-
    background-color: var(--color-fill-ghost);
+
    background-color: var(--color-surface-subtle);
    font-size: inherit;
  }
</style>

<span
  class="content"
-
  class:txt-large={fontSize === "large"}
-
  class:txt-medium={fontSize === "medium"}
-
  class:txt-regular={fontSize === "regular"}
-
  class:txt-small={fontSize === "small"}
-
  class:txt-tiny={fontSize === "tiny"}>
+
  class:txt-heading-l={fontSize === "large"}
+
  class:txt-heading-m={fontSize === "medium"}
+
  class:txt-body-l-regular={fontSize === "regular"}
+
  class:txt-body-m-regular={fontSize === "small"}
+
  class:txt-body-s-regular={fontSize === "tiny"}>
  {@html dompurify.sanitize(formatInlineTitle(escape(content)))}
</span>
deleted src/components/IssueSecondColumn.svelte
@@ -1,185 +0,0 @@
-
<script lang="ts">
-
  import type { IssueStatus } from "@app/views/repo/router";
-
  import type { Issue } from "@bindings/cob/issue/Issue";
-
  import type { RepoInfo } from "@bindings/repo/RepoInfo";
-

-
  import fuzzysort from "fuzzysort";
-

-
  import * as router from "@app/lib/router";
-
  import { modifierKey } from "@app/lib/utils";
-

-
  import Border from "@app/components/Border.svelte";
-
  import Icon from "@app/components/Icon.svelte";
-
  import IssueStateFilterButton from "@app/components/IssueStateFilterButton.svelte";
-
  import IssueTeaser from "@app/components/IssueTeaser.svelte";
-
  import NakedButton from "@app/components/NakedButton.svelte";
-
  import OutlineButton from "@app/components/OutlineButton.svelte";
-
  import TextInput from "@app/components/TextInput.svelte";
-

-
  const activeRouteStore = router.activeRouteStore;
-

-
  interface Props {
-
    changeFilter: (status: IssueStatus) => void;
-
    issues: Issue[];
-
    repo: RepoInfo;
-
    selectedIssueId?: string;
-
    status: IssueStatus;
-
  }
-

-
  const { changeFilter, issues, repo, selectedIssueId, status }: Props =
-
    $props();
-

-
  const project = $derived(repo.payloads["xyz.radicle.project"]!);
-

-
  let showFilters: boolean = $state(false);
-
  let searchInput = $state("");
-

-
  const searchableIssues = $derived(
-
    issues
-
      .flatMap(i => {
-
        return {
-
          issue: i,
-
          labels: i.labels.join(" "),
-
          assignees: i.assignees
-
            .map(a => {
-
              return a.alias ?? "";
-
            })
-
            .join(" "),
-
          author: i.author.alias ?? "",
-
        };
-
      })
-
      .filter((item): item is NonNullable<typeof item> => item !== undefined),
-
  );
-

-
  const searchResults = $derived(
-
    fuzzysort.go(searchInput, searchableIssues, {
-
      keys: ["issue.title", "labels", "assignees", "author", "issue.id"],
-
      threshold: 0.5,
-
      all: true,
-
    }),
-
  );
-
</script>
-

-
<style>
-
  .container {
-
    display: flex;
-
    align-items: center;
-
    min-height: 2.5rem;
-
    margin-bottom: 1rem;
-
    min-width: 28rem;
-
  }
-
  .list {
-
    display: flex;
-
    flex-direction: column;
-
    gap: 2px;
-
  }
-
</style>
-

-
<div class="container">
-
  <div class="global-flex">
-
    <IssueStateFilterButton
-
      {status}
-
      counters={project.meta.issues}
-
      {changeFilter} />
-
    <NakedButton
-
      styleHeight="2.5rem"
-
      keyShortcuts="ctrl+f"
-
      variant="ghost"
-
      active={showFilters}
-
      onclick={() => {
-
        if (showFilters) {
-
          showFilters = false;
-
          searchInput = "";
-
        } else {
-
          showFilters = true;
-
        }
-
      }}>
-
      <Icon name="filter" />
-
    </NakedButton>
-
  </div>
-
  <div class="global-flex" style:margin-left="auto">
-
    <OutlineButton
-
      variant="ghost"
-
      styleHeight="2.5rem"
-
      active={$activeRouteStore.resource === "repo.createIssue"}
-
      onclick={() => {
-
        if ($activeRouteStore.resource === "repo.createIssue") {
-
          window.history.back();
-
        } else {
-
          void router.push({
-
            resource: "repo.createIssue",
-
            rid: repo.rid,
-
            status,
-
          });
-
        }
-
      }}>
-
      <Icon name="add" />New issue
-
    </OutlineButton>
-
  </div>
-
</div>
-

-
{#if showFilters}
-
  <div class="global-flex" style:margin="1rem 0">
-
    <TextInput
-
      onSubmit={async () => {
-
        if (searchResults.length === 1) {
-
          await router.push({
-
            resource: "repo.issue",
-
            rid: repo.rid,
-
            issue: searchResults[0].obj.issue.id,
-
            status,
-
          });
-
        }
-
      }}
-
      onDismiss={() => {
-
        showFilters = false;
-
        searchInput = "";
-
      }}
-
      placeholder={`Fuzzy filter issues ${modifierKey()} + f`}
-
      autofocus
-
      keyShortcuts="ctrl+f"
-
      bind:value={searchInput}>
-
      {#snippet left()}
-
        <div
-
          style:color="var(--color-foreground-dim)"
-
          style:padding-left="0.5rem">
-
          <Icon name="filter" />
-
        </div>
-
      {/snippet}
-
    </TextInput>
-
  </div>
-
{/if}
-

-
{#if searchResults.length > 0}
-
  <div class="list">
-
    {#each searchResults as result}
-
      <IssueTeaser
-
        selected={result.obj.issue.id === selectedIssueId}
-
        focussed={searchResults.length === 1 && searchInput !== ""}
-
        compact
-
        issue={result.obj.issue}
-
        {status}
-
        rid={repo.rid} />
-
    {/each}
-
  </div>
-
{/if}
-

-
{#if searchResults.length === 0}
-
  <Border
-
    variant="ghost"
-
    styleFlexDirection="column"
-
    styleOverflow="hidden"
-
    styleAlignItems="center"
-
    styleJustifyContent="center">
-
    <div class="global-flex" style:height="84px" style:justify-content="center">
-
      <div class="txt-missing txt-small global-flex" style:gap="0.25rem">
-
        <Icon name="none" />
-
        {#if issues.length > 0 && searchResults.length === 0}
-
          No matching issues.
-
        {:else}
-
          No {status === "all" ? "" : status} issues.
-
        {/if}
-
      </div>
-
    </div>
-
  </Border>
-
{/if}
modified src/components/IssueStateButton.svelte
@@ -6,7 +6,7 @@

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

-
  import Border from "@app/components/Border.svelte";
+
  import Button from "@app/components/Button.svelte";
  import DropdownList from "@app/components/DropdownList.svelte";
  import DropdownListItem from "@app/components/DropdownListItem.svelte";
  import Icon from "@app/components/Icon.svelte";
@@ -16,56 +16,57 @@
  interface Props {
    selectedState: State;
    onSelect: (selectedStatus: State) => void;
+
    disabled?: boolean;
  }

-
  const { selectedState, onSelect }: Props = $props();
+
  const { selectedState, onSelect, disabled = false }: Props = $props();

  let popoverExpanded: boolean = $state(false);
</script>

-
<style>
-
  button {
-
    cursor: pointer;
-
    border: 0;
-
    background: none;
-
    margin: 0;
-
    padding: 0;
-
    display: flex;
-
    align-items: center;
-
    justify-content: center;
-
    font-size: var(--font-size-small);
-
  }
-
  .badge {
-
    gap: 0.375rem;
-
    padding-right: 0.625rem;
-
  }
-
</style>
-

<Popover
  popoverPadding="0"
-
  popoverPositionTop="2rem"
-
  popoverPositionLeft="0"
+
  placement="bottom-start"
  bind:expanded={popoverExpanded}>
  {#snippet toggle(onclick)}
-
    <button {onclick}>
+
    <Button
+
      variant="outline"
+
      {disabled}
+
      {onclick}
+
      active={popoverExpanded}
+
      title={disabled
+
        ? "You must be a delegate to change the issue state"
+
        : undefined}>
      <span
-
        class="global-counter badge"
-
        style:color={issueStatusColor[selectedState.status]}
-
        style:background-color={issueStatusBackgroundColor[
-
          selectedState.status
-
        ]}>
+
        class="global-chip"
+
        style:padding="0"
+
        style:margin-left="-0.25rem"
+
        style:color={disabled
+
          ? undefined
+
          : issueStatusColor[selectedState.status]}
+
        style:background-color={disabled
+
          ? undefined
+
          : issueStatusBackgroundColor[selectedState.status]}>
        <Icon
          name={selectedState.status === "open"
            ? "issue"
            : `issue-${selectedState.status}`} />
+
      </span>
+
      <span style:color={disabled ? undefined : "var(--color-text-secondary)"}>
        {capitalize(selectedState.status)}
        {selectedState.status === "closed" ? `as ${selectedState.reason}` : ""}
-
        <Icon name={popoverExpanded ? "chevron-up" : "chevron-down"} />
      </span>
-
    </button>
+
      <Icon name={popoverExpanded ? "chevron-up" : "chevron-down"} />
+
    </Button>
  {/snippet}
  {#snippet popover()}
-
    <Border variant="ghost">
+
    <div
+
      style:border="1px solid var(--color-border-subtle)"
+
      style:border-radius="var(--border-radius-sm)"
+
      style:display="flex"
+
      style:gap="0.5rem"
+
      style:align-items="center"
+
      style:background-color="var(--color-surface-canvas)">
      <DropdownList
        items={[
          { status: "open" },
@@ -75,23 +76,29 @@
        {#snippet item(state)}
          <DropdownListItem
            selected={isEqual(selectedState, state)}
+
            styleGap="0.5rem"
            onclick={() => {
              onSelect(state);
              closeFocused();
            }}>
            <span
-
              class="global-flex"
-
              style:color={issueStatusColor[state.status]}>
+
              class="global-chip"
+
              style:padding="0"
+
              style:margin-left="-0.5rem"
+
              style:color={issueStatusColor[state.status]}
+
              style:background-color={issueStatusBackgroundColor[state.status]}>
              <Icon
                name={state.status === "open"
                  ? "issue"
                  : `issue-${state.status}`} />
+
            </span>
+
            <span style:color="var(--color-text-secondary)">
              {capitalize(state.status)}
              {state.status === "closed" ? `as ${state.reason}` : ""}
            </span>
          </DropdownListItem>
        {/snippet}
      </DropdownList>
-
    </Border>
+
    </div>
  {/snippet}
</Popover>
deleted src/components/IssueStateFilterButton.svelte
@@ -1,83 +0,0 @@
-
<script lang="ts">
-
  import type { IssueStatus } from "@app/views/repo/router";
-
  import type { ProjectPayloadMeta } from "@bindings/repo/ProjectPayloadMeta";
-

-
  import capitalize from "lodash/capitalize";
-

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

-
  import Border from "@app/components/Border.svelte";
-
  import DropdownList from "@app/components/DropdownList.svelte";
-
  import DropdownListItem from "@app/components/DropdownListItem.svelte";
-
  import Icon from "@app/components/Icon.svelte";
-
  import OutlineButton from "@app/components/OutlineButton.svelte";
-
  import { closeFocused } from "@app/components/Popover.svelte";
-
  import Popover from "@app/components/Popover.svelte";
-

-
  interface Props {
-
    changeFilter: (status: IssueStatus) => void;
-
    status: IssueStatus;
-
    counters: ProjectPayloadMeta["issues"];
-
  }
-

-
  const { changeFilter, counters, status }: Props = $props();
-

-
  let popoverExpanded: boolean = $state(false);
-
</script>
-

-
{#snippet iconSnippet(status: IssueStatus)}
-
  <div
-
    class="icon"
-
    style:color={status === "all" ? undefined : issueStatusColor[status]}>
-
    <Icon name={status === "closed" ? "issue-closed" : "issue"} />
-
  </div>
-
{/snippet}
-

-
{#snippet counterSnippet(status: IssueStatus)}
-
  <div style:margin-left="auto" style:padding-left="0.25rem">
-
    {#if status === "all"}
-
      {counters.open + counters.closed}
-
    {:else}
-
      {counters[status]}
-
    {/if}
-
  </div>
-
{/snippet}
-

-
<Popover
-
  popoverPositionLeft="0"
-
  popoverPositionTop="3rem"
-
  bind:expanded={popoverExpanded}>
-
  {#snippet toggle(onclick)}
-
    <OutlineButton
-
      variant="ghost"
-
      {onclick}
-
      styleHeight="2.5rem"
-
      active={popoverExpanded}>
-
      {@render iconSnippet(status)}
-
      {capitalize(status)}
-
      {@render counterSnippet(status)}
-
      <Icon name={popoverExpanded ? "chevron-up" : "chevron-down"} />
-
    </OutlineButton>
-
  {/snippet}
-

-
  {#snippet popover()}
-
    <Border variant="ghost">
-
      <DropdownList items={["all", "open", "closed"] as IssueStatus[]}>
-
        {#snippet item(state)}
-
          <DropdownListItem
-
            styleGap="0.5rem"
-
            styleMinHeight="2.5rem"
-
            selected={status === state}
-
            onclick={() => {
-
              changeFilter(state);
-
              closeFocused();
-
            }}>
-
            {@render iconSnippet(state)}
-
            {capitalize(state)}
-
            {@render counterSnippet(state)}
-
          </DropdownListItem>
-
        {/snippet}
-
      </DropdownList>
-
    </Border>
-
  {/snippet}
-
</Popover>
modified src/components/IssueTeaser.svelte
@@ -10,7 +10,6 @@
    issueStatusColor,
  } from "@app/lib/utils";

-
  import Border from "@app/components/Border.svelte";
  import Icon from "@app/components/Icon.svelte";
  import Id from "@app/components/Id.svelte";
  import InlineTitle from "@app/components/InlineTitle.svelte";
@@ -18,22 +17,13 @@
  import NodeId from "@app/components/NodeId.svelte";

  interface Props {
-
    compact?: boolean;
    focussed?: boolean;
    issue: Issue;
    rid: string;
-
    selected?: boolean;
    status: IssueStatus;
  }

-
  const {
-
    compact = false,
-
    focussed,
-
    issue,
-
    rid,
-
    selected = false,
-
    status,
-
  }: Props = $props();
+
  const { focussed, issue, rid, status }: Props = $props();
</script>

<style>
@@ -42,31 +32,28 @@
    align-items: center;
    gap: 0.25rem;
    min-height: 5rem;
-
    background-color: var(--color-background-float);
+
    background-color: var(--color-surface-canvas);
    padding: 1rem;
    cursor: pointer;
-
    font-size: var(--font-size-regular);
+
    font: var(--txt-body-l-regular);
    word-break: break-word;
    width: 100%;
  }
-
  .selected {
-
    background-color: var(--color-fill-float-hover);
-
  }
  .issue-teaser:hover {
-
    background-color: var(--color-fill-float-hover);
+
    background-color: var(--color-surface-subtle);
  }
  .status {
    padding: 0;
    margin-right: 1rem;
  }
  .issue-teaser:first-of-type {
-
    clip-path: var(--2px-top-corner-fill);
+
    border-radius: var(--border-radius-sm) var(--border-radius-sm) 0 0;
  }
  .issue-teaser:last-of-type {
-
    clip-path: var(--2px-bottom-corner-fill);
+
    border-radius: 0 0 var(--border-radius-sm) var(--border-radius-sm);
  }
  .issue-teaser:only-of-type {
-
    clip-path: var(--2px-corner-fill);
+
    border-radius: var(--border-radius-sm);
  }
</style>

@@ -76,7 +63,6 @@
    tabindex="0"
    role="button"
    class="issue-teaser"
-
    class:selected
    style:align-items="flex-start"
    style:clip-path={focussed ? "none" : undefined}
    style:padding={focussed ? "1rem" : "20px"}
@@ -85,7 +71,7 @@
    }}>
    <div class="global-flex" style:align-items="flex-start">
      <div
-
        class="global-counter status"
+
        class="global-chip status"
        style:color={issueStatusColor[issue.state.status]}
        style:background-color={issueStatusBackgroundColor[issue.state.status]}>
        {#if issue.state.status === "open"}
@@ -99,24 +85,29 @@
        style:flex-direction="column"
        style:align-items="flex-start">
        <InlineTitle content={issue.title} />
-
        <div class="global-flex txt-small" style:flex-wrap="wrap">
+
        <div class="global-flex txt-body-m-regular" style:flex-wrap="wrap">
          <NodeId {...authorForNodeId(issue.author)} />
          opened
-
          <Id id={issue.id} clipboard={issue.id} variant="oid" />
+
          <Id id={issue.id} clipboard={issue.id} />
          {formatTimestamp(issue.timestamp)}
        </div>
      </div>
    </div>

    <div class="global-flex" style:margin-left="auto">
-
      {#if !compact}
-
        {#each issue.labels as label}
-
          <Label {label} />
-
        {/each}
-
      {/if}
+
      {#each issue.labels as label}
+
        <Label {label} />
+
      {/each}

      {#if issue.commentCount > 0}
-
        <div class="txt-small global-flex" style:gap="0.25rem">
+
        <div
+
          class="txt-body-m-regular global-flex"
+
          style:gap="0.25rem"
+
          style:border="1px solid var(--color-border-subtle)"
+
          style:border-radius="var(--border-radius-sm)"
+
          style:height="1.5rem"
+
          style:padding="0 0.5rem"
+
          style:color="var(--color-text-tertiary)">
          <Icon name="comment" />
          {issue.commentCount}
        </div>
@@ -126,11 +117,15 @@
{/snippet}

{#if focussed}
-
  <Border
-
    styleBackgroundColor="var(--color-background-float)"
-
    variant="secondary">
+
  <div
+
    style:border="1px solid var(--color-border-brand)"
+
    style:border-radius="var(--border-radius-sm)"
+
    style:display="flex"
+
    style:gap="0.5rem"
+
    style:align-items="center"
+
    style:background-color="var(--color-surface-canvas)">
    {@render issueSnippet()}
-
  </Border>
+
  </div>
{:else}
  {@render issueSnippet()}
{/if}
modified src/components/IssueTimeline.svelte
@@ -94,7 +94,7 @@
  }
</style>

-
<div class="timeline txt-small">
+
<div class="timeline txt-body-m-regular">
  {#each timeline as op}
    {#if op.type === "lifecycle"}
      <div class="timeline-item">
@@ -154,7 +154,7 @@
    {:else if op.type === "assign"}
      <div class="timeline-item">
        <div class="icon">
-
          <Icon name="user" />
+
          <Icon name="avatar-incognito" />
        </div>
        <div class="wrapper">
          <NodeId {...authorForNodeId(op.author)} />
@@ -192,7 +192,7 @@
      {#if op.previous && op.previous.type === op.type}
        <div class="timeline-item">
          <div class="icon">
-
            <Icon name="pen" />
+
            <Icon name="edit" />
          </div>
          <div class="wrapper">
            <NodeId {...authorForNodeId(op.author)} />
@@ -208,13 +208,13 @@
    {:else if op.type === "comment"}
      {#if op.id === activity[0].id}
        <div class="timeline-item">
-
          <div class="icon" style:color="var(--color-fill-success)">
+
          <div class="icon" style:color="var(--color-feedback-success-text)">
            <Icon name="issue" />
          </div>
          <div class="wrapper">
            <NodeId {...authorForNodeId(op.author)} />
            <div>
-
              opened issue <Id id={op.id} clipboard={op.id} variant="oid" />
+
              opened issue <Id id={op.id} clipboard={op.id} />
            </div>
            <div title={absoluteTimestamp(op.timestamp)}>
              {formatTimestamp(op.timestamp)}
deleted src/components/IssuesSecondColumn.svelte
@@ -1,161 +0,0 @@
-
<script lang="ts">
-
  import type { IssueStatus } from "@app/views/repo/router";
-
  import type { RepoInfo } from "@bindings/repo/RepoInfo";
-

-
  import Border from "@app/components/Border.svelte";
-
  import Icon from "@app/components/Icon.svelte";
-
  import Link from "@app/components/Link.svelte";
-
  import RepoTeaser from "@app/components/RepoTeaser.svelte";
-
  import Settings from "@app/components/Settings.svelte";
-

-
  interface Props {
-
    status: IssueStatus;
-
    repo: RepoInfo;
-
  }
-

-
  const { status, repo }: Props = $props();
-

-
  const project = $derived(repo.payloads["xyz.radicle.project"]!);
-
</script>
-

-
<style>
-
  .container {
-
    display: flex;
-
    flex-direction: column;
-
    height: 100%;
-
    justify-content: space-between;
-
  }
-
  .tab {
-
    align-items: center;
-
    background-color: var(--color-background-float);
-
    clip-path: var(--1px-corner-fill);
-
    display: flex;
-
    font-size: var(--font-size-small);
-
    justify-content: space-between;
-
    padding: 0.5rem 0.25rem 0.5rem 0.5rem;
-
    width: 100%;
-
  }
-
  .tab:not(.active) {
-
    color: var(--color-foreground-dim);
-
  }
-
  .tab:not(.active):hover {
-
    background-color: var(--color-fill-float-hover);
-
  }
-
  .active {
-
    background-color: var(--color-background-default);
-
    font-weight: var(--font-weight-semibold);
-
  }
-
  .highlight {
-
    color: var(--color-foreground-contrast);
-
  }
-
  .closed {
-
    color: var(--color-foreground-red);
-
  }
-
  .open {
-
    color: var(--color-foreground-success);
-
  }
-
</style>
-

-
<div class="container">
-
  <div>
-
    <div style:margin-bottom="0.75rem">
-
      <Link
-
        styleWidth="100%"
-
        underline={false}
-
        route={{ resource: "repo.home", rid: repo.rid }}>
-
        <div
-
          class="tab"
-
          style:color="var(--color-foreground-contrast)"
-
          style:padding-right="0.5rem"
-
          style:padding-left="0.75rem">
-
          <RepoTeaser name={project.data.name} seeding={repo.seeding} />
-
        </div>
-
      </Link>
-
    </div>
-

-
    <Border
-
      variant="ghost"
-
      styleFlexDirection="column"
-
      styleGap="2px"
-
      styleBackgroundColor="var(--color-background-float)">
-
      <Link
-
        styleWidth="100%"
-
        underline={false}
-
        route={{ resource: "repo.issues", rid: repo.rid, status: "all" }}>
-
        <div class="tab active">
-
          <div class="global-flex"><Icon name="issue" />Issues</div>
-
          <div class="global-counter">
-
            {project.meta.issues.open + project.meta.issues.closed}
-
          </div>
-
        </div>
-
      </Link>
-

-
      <Link
-
        styleWidth="100%"
-
        underline={false}
-
        route={{
-
          resource: "repo.issues",
-
          rid: repo.rid,
-
          status: "open",
-
        }}>
-
        <div class="tab" class:active={status === "open"}>
-
          <div
-
            class="global-flex"
-
            class:open={["open", "all"].includes(status)}>
-
            <Icon name="issue" />Open
-
          </div>
-
          <div class="global-counter" class:highlight={status === "all"}>
-
            {project.meta.issues.open}
-
          </div>
-
        </div>
-
      </Link>
-

-
      <Link
-
        styleWidth="100%"
-
        underline={false}
-
        route={{
-
          resource: "repo.issues",
-
          rid: repo.rid,
-
          status: "closed",
-
        }}>
-
        <div class="tab" class:active={status === "closed"}>
-
          <div
-
            class="global-flex"
-
            class:closed={["closed", "all"].includes(status)}>
-
            <Icon name="issue-closed" />Closed
-
          </div>
-
          <div class="global-counter" class:highlight={status === "all"}>
-
            {project.meta.issues.closed}
-
          </div>
-
        </div>
-
      </Link>
-
    </Border>
-

-
    <div style:margin-top="0.5rem">
-
      <Link
-
        styleWidth="100%"
-
        underline={false}
-
        route={{ resource: "repo.patches", rid: repo.rid, status: "open" }}>
-
        <div
-
          class="tab"
-
          style:color="var(--color-foreground-contrast)"
-
          style:padding-left="0.75rem">
-
          <div class="global-flex"><Icon name="patch" />Patches</div>
-
          <div class="global-counter">
-
            {project.meta.patches.draft +
-
              project.meta.patches.open +
-
              project.meta.patches.archived +
-
              project.meta.patches.merged}
-
          </div>
-
        </div>
-
      </Link>
-
    </div>
-
  </div>
-

-
  <Settings
-
    compact={false}
-
    popoverProps={{
-
      popoverPositionBottom: "3rem",
-
      popoverPositionLeft: "0",
-
    }} />
-
</div>
modified src/components/JobCob.svelte
@@ -27,10 +27,7 @@

{#await invoke<Job[]>("list_jobs", { rid, sha: commit }) then jobs}
  {#if jobs.length > 0}
-
    <HoverPopover
-
      stylePadding="0.25rem"
-
      stylePopoverPositionBottom="2rem"
-
      stylePopoverPositionRight="-1.5rem">
+
    <HoverPopover stylePadding="0.25rem" placement="bottom-end">
      {#snippet toggle()}
        {#if jobs.every(j => {
          return j.runs.every(r => {
@@ -38,11 +35,11 @@
          });
        })}
          <div
-
            class="global-counter"
+
            class="global-chip"
            style:padding="0"
-
            style:color="var(--color-fill-success)"
-
            style:background-color="var(--color-fill-diff-green)">
-
            <Icon name="checkmark-double" />
+
            style:color="var(--color-feedback-success-text)"
+
            style:background-color="var(--color-feedback-success-bg)">
+
            <Icon name="checkmark" />
          </div>
        {:else if jobs.every(j => {
          return j.runs.every(r => {
@@ -50,18 +47,18 @@
          });
        })}
          <div
-
            class="global-counter"
-
            style:color="var(--color-foreground-red)"
+
            class="global-chip"
+
            style:color="var(--color-feedback-error-text)"
            style:padding="0"
-
            style:background-color="var(--color-fill-diff-red)">
-
            <Icon name="cross-double" />
+
            style:background-color="var(--color-feedback-error-bg)">
+
            <Icon name="close" />
          </div>
        {:else}
          <div
-
            class="global-counter"
+
            class="global-chip"
            style:padding="0"
-
            style:color="var(--color-fill-gray)"
-
            style:background-color="var(--color-fill-ghost)">
+
            style:color="var(--color-text-quaternary)"
+
            style:background-color="var(--color-surface-subtle)">
            <Icon name="help" />
          </div>
        {/if}
@@ -79,30 +76,30 @@
              <DropdownListItem styleGap="0.5rem" selected={true}>
                {#if run.status === "started"}
                  <div
-
                    class="global-counter status"
-
                    style:background-color="var(--color-fill-float)"
-
                    style:color="var(--color-fill-gray)">
+
                    class="global-chip status"
+
                    style:background-color="var(--color-surface-canvas)"
+
                    style:color="var(--color-text-quaternary)">
                    <Icon name="hourglass" /> Started
                  </div>
                {:else if run.status === "failed"}
                  <div
-
                    class="global-counter status"
-
                    style:color="var(--color-foreground-red)"
-
                    style:background-color="var(--color-fill-diff-red)">
-
                    <Icon name="cross" /> Failed
+
                    class="global-chip status"
+
                    style:color="var(--color-feedback-error-text)"
+
                    style:background-color="var(--color-feedback-error-bg)">
+
                    <Icon name="close" /> Failed
                  </div>
                {:else if run.status === "succeeded"}
                  <div
-
                    class="global-counter status"
-
                    style:color="var(--color-fill-success)"
-
                    style:background-color="var(--color-fill-diff-green)">
+
                    class="global-chip status"
+
                    style:color="var(--color-feedback-success-text)"
+
                    style:background-color="var(--color-feedback-success-bg)">
                    <Icon name="checkmark" /> Passed
                  </div>
                {/if}
                <NodeId {...authorForNodeId(run.node)} />
                <div
                  style:margin-left="auto"
-
                  style:color="var(--color-fill-gray)">
+
                  style:color="var(--color-text-quaternary)">
                  <Icon name="open-external" />
                </div>
              </DropdownListItem>
modified src/components/Label.svelte
@@ -6,6 +6,20 @@
  const { label }: Props = $props();
</script>

-
<div class="global-counter txt-small" style:max-width="10rem">
+
<style>
+
  .label {
+
    display: flex;
+
    align-items: center;
+
    justify-content: center;
+
    border: 1px solid var(--color-border-subtle);
+
    border-radius: var(--border-radius-sm);
+
    height: 1.5rem;
+
    padding: 0 0.5rem;
+
    color: var(--color-text-tertiary);
+
    max-width: 10rem;
+
  }
+
</style>
+

+
<div class="label txt-body-m-regular">
  <div class="txt-overflow" title={label}>{label}</div>
</div>
modified src/components/LabelInput.svelte
@@ -1,4 +1,5 @@
<script lang="ts">
+
  import Button from "@app/components/Button.svelte";
  import Icon from "@app/components/Icon.svelte";
  import Label from "@app/components/Label.svelte";
  import TextInput from "@app/components/TextInput.svelte";
@@ -8,6 +9,7 @@
    labels: string[];
    submitInProgress: boolean;
    save: (updatedLabels: string[]) => void;
+
    preview?: boolean;
  }

  const {
@@ -15,6 +17,7 @@
    labels,
    submitInProgress = false,
    save,
+
    preview = false,
  }: Props = $props();

  let updatedLabels: string[] = $state([]);
@@ -73,33 +76,34 @@
</script>

<style>
-
  .add-icon {
-
    display: none;
-
  }
-
  .title-button:hover .add-icon {
-
    display: flex;
-
  }
-
  .title-button {
-
    font-size: var(--font-size-small);
-
    color: var(--color-foreground-dim);
-
  }
-
  .body {
+
  .row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
-
    flex-direction: row;
    gap: 0.5rem;
-
    font-size: var(--font-size-small);
-
    margin-top: 1rem;
  }
  .validation-message {
    display: flex;
    align-items: center;
    gap: 0.25rem;
-
    color: var(--color-foreground-red);
+
    color: var(--color-feedback-error-text);
    position: relative;
    margin-top: 0.5rem;
  }
+
  .removable-label {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.25rem;
+
    background: none;
+
    border: none;
+
    padding: 0;
+
    cursor: pointer;
+
  }
+
  .input-row {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.5rem;
+
  }
  button {
    border: 0;
    cursor: pointer;
@@ -107,97 +111,87 @@
    background-color: transparent;
    border: none;
    display: flex;
-
    color: var(--color-foreground-default);
+
    color: var(--color-text-secondary);
    padding: 0;
    align-items: center;
  }
</style>

-
<div class="global-flex">
-
  <button
-
    disabled={!allowedToEdit}
-
    style:color={allowedToEdit
-
      ? "var(--color-foreground-dim)"
-
      : "var(--color-foreground-disabled)"}
-
    title={allowedToEdit
-
      ? undefined
-
      : "Only delegates are allowed to add labels"}
-
    style:cursor={allowedToEdit ? "pointer" : "default"}
-
    class="title-button"
-
    onclick={() => {
-
      inputValue = "";
-
      showInput = !showInput;
-
    }}>
-
    {#if updatedLabels.length === 0}
-
      Add labels
-
    {:else}
-
      Labels
-
    {/if}
-

-
    {#if !showInput && allowedToEdit}
-
      <span class="add-icon">
-
        <Icon name="add"></Icon>
-
      </span>
-
    {/if}
-
  </button>
-

-
  {#if allowedToEdit}
-
    <div class="global-flex edit-icons">
-
      {#if showInput}
-
        <Icon
-
          onclick={addLabel}
-
          name="checkmark"
-
          disabled={!valid || inputValue === ""} />
-
        <Icon
+
{#if preview}
+
  <div class="row">
+
    <Button variant="outline" disabled>
+
      <Icon name="label" />
+
      {#if updatedLabels.length === 0}
+
        Add labels
+
      {:else}
+
        Labels
+
      {/if}
+
    </Button>
+
    {#each updatedLabels as label}
+
      <Label {label} />
+
    {/each}
+
  </div>
+
{:else}
+
  <div class="row">
+
    {#if showInput}
+
      <div class="input-row">
+
        <div style:flex="1" style:min-width="0">
+
          <TextInput
+
            autofocus
+
            {valid}
+
            disabled={submitInProgress}
+
            placeholder="Add label"
+
            bind:value={inputValue}
+
            onSubmit={addLabel} />
+
        </div>
+
        <Button
+
          variant="outline"
          onclick={() => {
-
            inputValue = "";
            showInput = false;
-
          }}
-
          name="cross" />
-
      {/if}
-
    </div>
-
  {/if}
-
</div>
-

-
{#if showInput}
-
  <div style:margin-top="1rem">
-
    <TextInput
-
      autofocus
-
      {valid}
-
      disabled={submitInProgress}
-
      placeholder="Add label"
-
      bind:value={inputValue}
-
      onSubmit={addLabel} />
-
    {#if !valid && validationMessage}
-
      <div class="validation-message">
-
        <Icon name="warning" />{validationMessage}
+
            inputValue = "";
+
          }}>
+
          <Icon name="close" />
+
        </Button>
      </div>
+
    {:else}
+
      <Button
+
        variant="outline"
+
        disabled={!allowedToEdit}
+
        title={allowedToEdit
+
          ? undefined
+
          : "Only delegates are allowed to add labels"}
+
        onclick={() => {
+
          inputValue = "";
+
          showInput = true;
+
        }}>
+
        <Icon name="label" />
+
        {#if updatedLabels.length === 0}
+
          Add labels
+
        {:else}
+
          Labels
+
        {/if}
+
      </Button>
    {/if}
-
  </div>
-
{/if}

-
{#if updatedLabels.length > 0}
-
  <div class="body">
-
    {#if allowedToEdit}
-
      {#each updatedLabels as label}
+
    {#each updatedLabels as label}
+
      {#if allowedToEdit}
        <button
-
          class="global-counter txt-small"
-
          style:background-color="var(--color-fill-counter)"
-
          style:padding="0 0.5rem"
-
          style:max-width="10rem"
+
          class="removable-label"
          onclick={() => (removeToggles[label] = !removeToggles[label])}>
-
          <div class="txt-overflow" title={label}>{label}</div>
+
          <Label {label} />
          {#if removeToggles[label]}
-
            <span style:margin-right="0.5rem">
-
              <Icon name="cross" onclick={() => removeLabel(label)} />
-
            </span>
+
            <Icon name="close" onclick={() => removeLabel(label)} />
          {/if}
        </button>
-
      {/each}
-
    {:else}
-
      {#each updatedLabels as label}
+
      {:else}
        <Label {label} />
-
      {/each}
-
    {/if}
+
      {/if}
+
    {/each}
  </div>
+

+
  {#if !valid && validationMessage}
+
    <div class="validation-message">
+
      <Icon name="warning" />{validationMessage}
+
    </div>
+
  {/if}
{/if}
deleted src/components/Link.svelte
@@ -1,54 +0,0 @@
-
<script lang="ts">
-
  import type { Snippet } from "svelte";
-

-
  import { push, routeToPath } from "@app/lib/router";
-
  import type { Route } from "@app/lib/router/definitions";
-

-
  interface Props {
-
    children: Snippet;
-
    route: Route;
-
    disabled?: boolean;
-
    underline?: boolean;
-
    styleWidth?: string;
-
    styleColor?: string;
-
  }
-

-
  const {
-
    children,
-
    route,
-
    disabled = false,
-
    underline = true,
-
    styleWidth,
-
    styleColor,
-
  }: Props = $props();
-

-
  function navigateToRoute(event: MouseEvent): void {
-
    event.preventDefault();
-
    if (disabled) {
-
      return;
-
    }
-

-
    void push(route);
-
  }
-
</script>
-

-
<style>
-
  a {
-
    color: var(--color-foreground-contrast);
-
    text-decoration: none;
-
  }
-
  .underline:hover {
-
    text-decoration: underline;
-
    text-decoration-thickness: 1px;
-
    text-underline-offset: 2px;
-
  }
-
</style>
-

-
<a
-
  onclick={navigateToRoute}
-
  href={routeToPath(route)}
-
  class:underline
-
  style:color={styleColor}
-
  style:width={styleWidth}>
-
  {@render children()}
-
</a>
modified src/components/Markdown.svelte
@@ -186,9 +186,8 @@
    user-select: text;
  }
  .front-matter {
-
    font-size: var(--font-size-tiny);
-
    font-family: var(--font-family-monospace);
-
    border: 1px dashed var(--color-border-default);
+
    font: var(--txt-body-s-regular);
+
    border: 1px dashed var(--color-border-mid);
    padding: 0.5rem;
    margin-bottom: 2rem;
  }
@@ -203,19 +202,17 @@
  }

  .markdown :global(h1) {
-
    font-size: calc(var(--font-size-x-large) * 0.75);
-
    font-weight: var(--font-weight-semibold);
+
    font: var(--txt-heading-l);
    padding: 1rem 0 0.5rem 0;
    margin: 0 0 0.75rem;
-
    border-bottom: 1px solid var(--color-border-hint);
+
    border-bottom: 1px solid var(--color-border-subtle);
  }

  .markdown :global(h2) {
-
    font-size: var(--font-size-medium);
-
    font-weight: var(--font-weight-regular);
+
    font: var(--txt-heading-m);
    padding: 0.25rem 0;
    margin: 2rem 0 0.5rem;
-
    border-bottom: 1px solid var(--color-border-hint);
+
    border-bottom: 1px solid var(--color-border-subtle);
  }

  .markdown :global(.pre-wrapper) {
@@ -231,7 +228,7 @@
  }

  .markdown :global(radicle-clipboard) {
-
    background-color: var(--color-fill-ghost);
+
    background-color: var(--color-surface-subtle);
  }

  .markdown :global(.pre-wrapper:hover > radicle-clipboard) {
@@ -239,29 +236,26 @@
  }

  .markdown :global(h3) {
-
    font-size: calc(var(--font-size-medium) * 0.9);
-
    font-weight: var(--font-weight-semibold);
+
    font: var(--txt-heading-m);
    padding: 0.5rem 0;
    margin: 1rem 0 0.25rem;
  }

  .markdown :global(h4) {
-
    font-weight: var(--font-weight-semibold);
-
    font-size: var(--font-size-regular);
+
    font: var(--txt-body-l-semibold);
    padding: 0.5rem 0;
    margin: 1rem 0 0.125rem;
  }

  .markdown :global(h5),
  .markdown :global(h6) {
-
    font-weight: var(--font-weight-semibold);
-
    font-size: var(--font-size-small);
+
    font: var(--txt-body-m-semibold);
    padding: 0.35rem 0;
    margin: 1rem 0 0.125rem;
  }

  .markdown :global(h6) {
-
    color: var(--color-foreground-dim);
+
    color: var(--color-text-secondary);
  }

  .markdown :global(p) {
@@ -279,21 +273,21 @@
    align-items: center;
    gap: 0.5rem;
    margin-left: -1.2rem;
-
    color: var(--color-foreground-dim);
+
    color: var(--color-text-secondary);
  }
  .markdown :global(li.task-item:not(:last-child)) {
    margin-bottom: 0.25rem;
  }

  .markdown :global(blockquote) {
-
    color: var(--color-foreground-dim);
-
    border-left: 0.3rem solid var(--color-fill-ghost);
+
    color: var(--color-text-secondary);
+
    border-left: 0.3rem solid var(--color-surface-subtle);
    padding: 0 0 0 1rem;
    margin: 1rem 0 1rem 0;
  }

  .markdown :global(strong) {
-
    font-weight: var(--font-weight-semibold);
+
    font-weight: 600;
  }

  .markdown :global(.footnote-ref) {
@@ -304,11 +298,11 @@
  .markdown :global(.footnote-ref),
  .markdown :global(.footnote > .marker),
  .markdown :global(.footnote > .ref-arrow) {
-
    color: var(--color-foreground-dim);
+
    color: var(--color-text-secondary);
  }
  .markdown :global(.footnote-ref:hover),
  .markdown :global(.footnote .ref-arrow:hover) {
-
    color: var(--color-background-default);
+
    color: var(--color-surface-base);
  }
  .markdown :global(.footnote) {
    margin-bottom: 0;
@@ -320,9 +314,8 @@
  }

  .markdown :global(code) {
-
    font-family: var(--font-family-monospace);
-
    font-size: var(--font-size-small);
-
    background-color: var(--color-fill-ghost);
+
    font: var(--txt-code-regular);
+
    background-color: var(--color-surface-subtle);
    padding: 0.125rem 0.25rem;
  }

@@ -336,13 +329,12 @@
  }

  .markdown :global(pre) {
-
    font-family: var(--font-family-monospace);
-
    font-size: var(--font-size-regular);
-
    background-color: var(--color-fill-ghost);
+
    font: var(--txt-code-regular);
+
    background-color: var(--color-surface-subtle);
    padding: 1rem !important;
    overflow: scroll;
    scrollbar-width: none;
-
    clip-path: var(--2px-corner-fill);
+
    border-radius: var(--border-radius-sm);
  }

  .markdown :global(pre::-webkit-scrollbar) {
@@ -351,19 +343,19 @@

  .markdown :global(a),
  .markdown :global(a > code) {
-
    color: var(--color-foreground-contrast);
+
    color: var(--color-text-primary);
    background: none;
    padding: 0;
  }
  .markdown :global(a) {
    text-decoration: underline;
-
    text-decoration-color: var(--color-foreground-dim);
+
    text-decoration-color: var(--color-text-secondary);
  }
  .markdown :global(a.no-underline) {
    text-decoration: none;
  }
  .markdown :global(a:hover) {
-
    text-decoration-color: var(--color-foreground-contrast);
+
    text-decoration-color: var(--color-text-primary);
  }

  .markdown :global(hr) {
@@ -372,7 +364,7 @@
    overflow: hidden;
    background: transparent;
    border: 0;
-
    border-bottom: 1px solid var(--color-border-hint);
+
    border-bottom: 1px solid var(--color-border-subtle);
  }

  .markdown :global(ol) {
@@ -405,17 +397,17 @@
    margin: 1.5rem 0;
    border-collapse: collapse;
    border-style: hidden;
-
    box-shadow: 0 0 0 1px var(--color-border-hint);
+
    box-shadow: 0 0 0 1px var(--color-border-subtle);
    overflow: hidden;
  }
  .markdown :global(td) {
    text-align: left;
    text-overflow: ellipsis;
-
    border: 1px solid var(--color-border-hint);
+
    border: 1px solid var(--color-border-subtle);
    padding: 0.5rem 1rem;
  }
  .markdown :global(tr:nth-child(even)) {
-
    background-color: var(--color-background-default);
+
    background-color: var(--color-surface-base);
  }
  .markdown :global(th) {
    text-align: center;
@@ -443,7 +435,7 @@
      <tbody>
        {#each frontMatter as [key, val]}
          <tr>
-
            <td><span class="txt-bold">{key}</span></td>
+
            <td><span class="txt-body-l-semibold">{key}</span></td>
            <td>{val}</td>
          </tr>
        {/each}
deleted src/components/MoreBreadcrumbsButton.svelte
@@ -1,42 +0,0 @@
-
<script lang="ts">
-
  import type { Snippet } from "svelte";
-

-
  import Border from "@app/components/Border.svelte";
-
  import Icon from "@app/components/Icon.svelte";
-
  import NakedButton from "@app/components/NakedButton.svelte";
-
  import Popover from "@app/components/Popover.svelte";
-

-
  interface Props {
-
    children: Snippet;
-
  }
-

-
  const { children }: Props = $props();
-

-
  let popoverExpanded: boolean = $state(false);
-
</script>
-

-
<Popover
-
  popoverPositionLeft="0"
-
  popoverPositionTop="2.5rem"
-
  bind:expanded={popoverExpanded}>
-
  {#snippet toggle(onclick)}
-
    <NakedButton
-
      variant="ghost"
-
      stylePadding="0 4px"
-
      {onclick}
-
      active={popoverExpanded}>
-
      <Icon name="more-vertical" />
-
    </NakedButton>
-
  {/snippet}
-

-
  {#snippet popover()}
-
    <Border variant="ghost">
-
      <div
-
        class="global-flex txt-monospace"
-
        style:flex-direction="column"
-
        style:align-items="flex-start">
-
        {@render children()}
-
      </div>
-
    </Border>
-
  {/snippet}
-
</Popover>
deleted src/components/NakedButton.svelte
@@ -1,287 +0,0 @@
-
<script lang="ts">
-
  import type { Snippet } from "svelte";
-

-
  interface Props {
-
    id?: string;
-
    children: Snippet;
-
    title?: string;
-
    disabled?: boolean;
-
    variant: "primary" | "secondary" | "ghost";
-
    onclick?: (e: MouseEvent) => void;
-
    styleHeight?: "2rem" | "2.5rem";
-
    stylePadding?: string;
-
    active?: boolean;
-
    keyShortcuts?: string;
-
  }
-

-
  const {
-
    id,
-
    children,
-
    title,
-
    disabled,
-
    variant,
-
    onclick,
-
    styleHeight = "2rem",
-
    stylePadding = "0 0.5rem",
-
    active = false,
-
    keyShortcuts,
-
  }: Props = $props();
-

-
  const style = $derived(
-
    `--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;
-
    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:not(.disabled) .p1-3 {
-
    background-color: var(--button-color-1);
-
  }
-

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

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

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

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

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

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

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

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

-
  .container.active:not(.disabled) {
-
    color: var(--color-foreground-emphasized);
-
  }
-

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

-
  .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
-
  {id}
-
  class="container"
-
  class:disabled
-
  class:active
-
  onclick={!disabled ? onclick : undefined}
-
  {title}
-
  aria-keyshortcuts={keyShortcuts}
-
  role="button"
-
  tabindex="0"
-
  {style}
-
  style:height={styleHeight}>
-
  <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" style:padding={stylePadding}>
-
    {@render children()}
-
  </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>
modified src/components/NewPatchButton.svelte
@@ -1,57 +1,57 @@
<script lang="ts">
-
  import Border from "@app/components/Border.svelte";
  import Button from "@app/components/Button.svelte";
  import Command from "@app/components/Command.svelte";
  import Icon from "@app/components/Icon.svelte";
-
  import OutlineButton from "@app/components/OutlineButton.svelte";
  import Popover from "@app/components/Popover.svelte";

  interface Props {
    outline?: boolean;
+
    ghost?: boolean;
    rid: string;
  }

-
  const { outline = false, rid }: Props = $props();
+
  const { outline = false, ghost = false, rid }: Props = $props();

  let popoverExpanded: boolean = $state(false);
</script>

-
<Popover
-
  popoverPositionRight="0"
-
  popoverPositionTop="3rem"
-
  bind:expanded={popoverExpanded}>
+
<Popover placement="bottom-end" bind:expanded={popoverExpanded}>
  {#snippet toggle(onclick)}
    {#if outline}
-
      <OutlineButton
-
        styleHeight="2.5rem"
-
        variant="ghost"
+
      <Button
+
        variant="outline"
+
        styleHeight="2rem"
        {onclick}
        active={popoverExpanded}>
-
        <Icon name="add" />New patch
-
      </OutlineButton>
+
        <Icon name="plus" />
+
        <span class="global-hide-on-small-desktop-down">New patch</span>
+
      </Button>
    {:else}
      <Button
-
        styleHeight="2.5rem"
-
        variant="secondary"
+
        styleHeight="2rem"
+
        variant={ghost ? "ghost" : "secondary"}
        {onclick}
        active={popoverExpanded}>
-
        <Icon name="add" />New patch
+
        <Icon name="plus" />
+
        <span class="global-hide-on-small-desktop-down">New patch</span>
      </Button>
    {/if}
  {/snippet}

  {#snippet popover()}
-
    <div class="txt-small">
-
      <Border
-
        styleAlignItems="flex-start"
-
        styleBackgroundColor="var(--color-background-float)"
-
        styleFlexDirection="column"
-
        styleGap="2rem"
-
        stylePadding="1rem"
-
        styleWidth="28rem"
-
        variant="ghost">
+
    <div class="txt-body-m-regular">
+
      <div
+
        style:border="1px solid var(--color-border-subtle)"
+
        style:border-radius="var(--border-radius-sm)"
+
        style:display="flex"
+
        style:gap="2rem"
+
        style:align-items="flex-start"
+
        style:background-color="var(--color-surface-canvas)"
+
        style:flex-direction="column"
+
        style:padding="1rem"
+
        style:width="30rem">
        <div>
-
          <div class="txt-semibold" style:margin-bottom="0.5rem">
+
          <div class="txt-body-l-semibold" style:margin-bottom="0.5rem">
            Create a new patch
          </div>
          <div
@@ -63,12 +63,12 @@
            run:
            <Command
              command="git push rad HEAD:refs/patches"
-
              styleWidth="100%" />
+
              styleWidth="fit-content" />
          </div>
        </div>

        <div style:margin-bottom="1rem">
-
          <div class="txt-semibold" style:margin-bottom="0.5rem">
+
          <div class="txt-body-l-semibold" style:margin-bottom="0.5rem">
            Don't have a working copy yet?
          </div>
          <div
@@ -77,10 +77,10 @@
            style:align-items="flex-start"
            style:gap="0.5rem">
            To checkout a working copy of this repo, run:
-
            <Command command={`rad checkout ${rid}`} styleWidth="100%" />
+
            <Command command={`rad checkout ${rid}`} styleWidth="fit-content" />
          </div>
        </div>
-
      </Border>
+
      </div>
    </div>
  {/snippet}
</Popover>
deleted src/components/NodeBreadcrumb.svelte
@@ -1,30 +0,0 @@
-
<script lang="ts">
-
  import type { Config } from "@bindings/config/Config";
-

-
  import * as router from "@app/lib/router";
-
  import { didFromPublicKey, explorerUrl } from "@app/lib/utils";
-

-
  import Link from "@app/components/Link.svelte";
-
  import NodeId from "@app/components/NodeId.svelte";
-
  import BreadcrumbCopyButton from "@app/views/repo/BreadcrumbCopyButton.svelte";
-

-
  const activeRouteStore = router.activeRouteStore;
-

-
  interface Props {
-
    config: Config;
-
  }
-

-
  const { config }: Props = $props();
-
</script>
-

-
{#if $activeRouteStore.resource === "home"}
-
  <NodeId publicKey={config.publicKey} alias={config.alias} />
-
  <BreadcrumbCopyButton
-
    icon="user"
-
    id={didFromPublicKey(config.publicKey)}
-
    url={explorerUrl(`users/${didFromPublicKey(config.publicKey)}`)} />
-
{:else}
-
  <Link route={{ resource: "home", activeTab: "all" }}>
-
    <NodeId publicKey={config.publicKey} alias={config.alias} />
-
  </Link>
-
{/if}
modified src/components/NodeId.svelte
@@ -1,22 +1,20 @@
<script lang="ts">
  import { truncateId } from "@app/lib/utils";

-
  import Avatar from "@app/components/Avatar.svelte";
+
  import UserAvatar from "@app/components/UserAvatar.svelte";

  interface Props {
    publicKey: string;
    alias?: string;
    inline?: boolean;
-
    styleFontSize?: string;
-
    styleFontWeight?: string;
+
    styleFont?: string;
  }

  const {
    publicKey,
    alias,
    inline = false,
-
    styleFontSize = "var(--font-size-small)",
-
    styleFontWeight = "var(--font-weight-semibold)",
+
    styleFont = undefined,
  }: Props = $props();
</script>

@@ -25,9 +23,22 @@
    display: flex;
    align-items: center;
    gap: 0.375rem;
+
    font: var(--txt-body-m-regular);
+
  }
+
  .avatar-container {
+
    width: 1rem;
+
    height: 1rem;
+
    overflow: hidden;
+
    flex-shrink: 0;
+
  }
+
  .avatar-container :global(img) {
+
    display: block;
+
    width: 100%;
+
    height: 100%;
+
    object-fit: cover;
  }
  .no-alias {
-
    color: var(--color-foreground-dim);
+
    color: var(--color-text-secondary);
  }
  .inline {
    display: inline-flex;
@@ -39,12 +50,10 @@
  }
</style>

-
<div
-
  class="avatar-alias"
-
  class:inline
-
  style:font-size={styleFontSize}
-
  style:font-weight={styleFontWeight}>
-
  <Avatar {publicKey} />
+
<div class="avatar-alias" class:inline style:font={styleFont}>
+
  <div class="avatar-container">
+
    <UserAvatar nodeId={publicKey} styleWidth="1rem" />
+
  </div>
  {#if alias}
    <span class="txt-overflow alias">
      {alias}
modified src/components/NodeStatusButton.svelte
@@ -1,10 +1,9 @@
<script lang="ts">
  import { nodeRunning } from "@app/lib/events";

-
  import Border from "@app/components/Border.svelte";
+
  import Button from "@app/components/Button.svelte";
  import Command from "@app/components/Command.svelte";
  import Icon from "@app/components/Icon.svelte";
-
  import NakedButton from "@app/components/NakedButton.svelte";
  import Popover from "@app/components/Popover.svelte";

  let popoverExpanded: boolean = $state(false);
@@ -12,28 +11,40 @@

<Popover
  popoverPadding="0"
-
  popoverPositionTop="2.5rem"
-
  bind:expanded={popoverExpanded}
-
  popoverPositionRight="0">
+
  placement="top-start"
+
  bind:expanded={popoverExpanded}>
  {#snippet toggle(onclick)}
-
    <NakedButton variant="ghost" {onclick} active={popoverExpanded}>
+
    <Button
+
      variant="naked"
+
      {onclick}
+
      active={popoverExpanded}
+
      styleWidth="100%"
+
      styleJustifyContent="flex-start">
      {#if $nodeRunning}
-
        <Icon name="online" />
+
        <span style:color="var(--color-text-tertiary)">
+
          <Icon name="online" />
+
        </span>
        Online
      {:else}
-
        <Icon name="offline" />
+
        <span style:color="var(--color-text-tertiary)">
+
          <Icon name="offline" />
+
        </span>
        Offline
      {/if}
-
    </NakedButton>
+
    </Button>
  {/snippet}
  {#snippet popover()}
-
    <Border
-
      variant="ghost"
-
      stylePadding="1rem"
-
      styleMinWidth="20rem"
-
      styleAlignItems="flex-start"
-
      styleFlexDirection="column">
-
      <div class="txt-small" style:line-height="1.625rem">
+
    <div
+
      style:border="1px solid var(--color-border-subtle)"
+
      style:border-radius="var(--border-radius-sm)"
+
      style:display="flex"
+
      style:gap="0.5rem"
+
      style:align-items="flex-start"
+
      style:background-color="var(--color-surface-canvas)"
+
      style:padding="1rem"
+
      style:width="22rem"
+
      style:flex-direction="column">
+
      <div class="txt-body-m-regular" style:line-height="1.625rem">
        {#if $nodeRunning}
          Your node is up and running, your changes will be synced
          automatically.
@@ -49,6 +60,6 @@
          </div>
        {/if}
      </div>
-
    </Border>
+
    </div>
  {/snippet}
</Popover>
modified src/components/NotificationTeaser.svelte
@@ -19,9 +19,9 @@
    patchStatusColor,
  } from "@app/lib/utils";

+
  import Button from "@app/components/Button.svelte";
  import Icon from "@app/components/Icon.svelte";
  import InlineTitle from "@app/components/InlineTitle.svelte";
-
  import NakedButton from "@app/components/NakedButton.svelte";
  import NodeId from "@app/components/NodeId.svelte";
  import { closeFocused } from "@app/components/Popover.svelte";

@@ -62,10 +62,6 @@
    );
  });

-
  const clearIcon = $derived(
-
    uniqueActions.length > 1 ? "broom-double" : "broom",
-
  );
-

  const title = $derived.by(() => {
    const lastDetail = notificationItems.at(-1);
    if (lastDetail && "title" in lastDetail) {
@@ -103,8 +99,8 @@
      };
    } else {
      return {
-
        color: "var(--color-foreground-dim)",
-
        background: "var(--color-fill-ghost)",
+
        color: "var(--color-text-secondary)",
+
        background: "var(--color-surface-subtle)",
      };
    }
  });
@@ -134,41 +130,46 @@

<style>
  .notification-teaser {
+
    position: relative;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.25rem;
    min-height: 5rem;
-
    background-color: var(--color-background-float);
+
    background-color: var(--color-surface-subtle);
    padding: 1rem;
    cursor: pointer;
-
    font-size: var(--font-size-regular);
+
    font: var(--txt-body-l-regular);
    word-break: break-word;
  }
  .clear-icon {
-
    display: none;
+
    position: absolute;
+
    top: 0.5rem;
+
    right: 0.5rem;
+
    visibility: hidden;
+
    color: var(--color-text-tertiary);
  }
  .notification-teaser:hover .clear-icon {
-
    display: flex;
+
    visibility: visible;
  }
  .selected {
-
    background-color: var(--color-fill-float-hover);
+
    background-color: var(--color-surface-mid);
  }
  .notification-teaser:hover {
-
    background-color: var(--color-fill-float-hover);
+
    background-color: var(--color-surface-mid);
  }
  .status {
    padding: 0;
    margin-right: 1rem;
  }
  .notification-teaser:first-of-type {
-
    clip-path: var(--3px-top-corner-fill);
+
    border-radius: var(--border-radius-sm) var(--border-radius-sm) 0 0;
  }
  .notification-teaser:last-of-type {
-
    clip-path: var(--3px-bottom-corner-fill);
+
    border-radius: 0 0 var(--border-radius-sm) var(--border-radius-sm);
  }
  .notification-teaser:only-of-type {
-
    clip-path: var(--3px-corner-fill);
+
    border-radius: var(--border-radius-sm);
  }
</style>

@@ -184,51 +185,51 @@
      closeFocused();
    }
  }}>
-
  <div
-
    class="global-flex"
-
    style:justify-content="space-between"
-
    style:align-items="flex-start"
-
    style:width="100%">
-
    <div class="global-flex">
-
      <div
-
        class="global-counter status"
-
        style:align-self="flex-start"
-
        style:color={statusColor.color}
-
        style:background-color={statusColor.background}>
-
        <Icon name={icon} />
-
      </div>
-
      <div
-
        class="global-flex"
-
        style:flex-direction="column"
-
        style:align-items="flex-start">
-
        {#if title}
-
          <InlineTitle content={title} />
-
        {/if}
-
        <div class="txt-small">
-
          {#each uniqueActions as action}
-
            <div
-
              class="global-flex"
-
              style:gap="0.25rem"
-
              style:min-height="2rem"
-
              style:flex-wrap="wrap">
-
              <NodeId {...authorForNodeId(action.items[0].author)} />
-
              <span>{@html action.summary}</span>
-
              <span>{formatTimestamp(action.items[0].timestamp)}</span>
-
            </div>
-
          {/each}
-
        </div>
-
      </div>
+
  <div class="global-flex" style:width="100%">
+
    <div
+
      class="global-chip status"
+
      style:align-self="flex-start"
+
      style:color={statusColor.color}
+
      style:background-color={statusColor.background}>
+
      <Icon name={icon} />
    </div>
-
    <div class="clear-icon">
-
      <NakedButton
-
        stylePadding="0 0.25rem"
-
        variant="ghost"
-
        onclick={e => {
-
          e.stopPropagation();
-
          void clearByIds(notificationItems.map(n => n.rowId));
-
        }}>
-
        <Icon name={clearIcon} />
-
      </NakedButton>
+
    <div
+
      class="global-flex"
+
      style:flex-direction="column"
+
      style:align-items="flex-start"
+
      style:width="100%">
+
      {#if title}
+
        <InlineTitle content={title} />
+
      {/if}
+
      <div class="txt-body-m-regular" style:width="100%">
+
        {#each uniqueActions as action}
+
          <div
+
            class="global-flex"
+
            style:gap="0.25rem"
+
            style:min-height="2rem"
+
            style:flex-wrap="wrap"
+
            style:width="100%">
+
            <NodeId {...authorForNodeId(action.items[0].author)} />
+
            <span>{@html action.summary}</span>
+
            <span
+
              style:margin-left="auto"
+
              style:color="var(--color-text-tertiary)">
+
              {formatTimestamp(action.items[0].timestamp)}
+
            </span>
+
          </div>
+
        {/each}
+
      </div>
    </div>
  </div>
+
  <div class="clear-icon">
+
    <Button
+
      variant="naked"
+
      stylePadding="0 0.25rem"
+
      onclick={e => {
+
        e.stopPropagation();
+
        void clearByIds(notificationItems.map(n => n.rowId));
+
      }}>
+
      <Icon name="trash" />
+
    </Button>
+
  </div>
</div>
modified src/components/NotificationsByRepo.svelte
@@ -4,7 +4,6 @@
  import Button from "@app/components/Button.svelte";
  import ConfirmClear from "@app/components/ConfirmClear.svelte";
  import Icon from "@app/components/Icon.svelte";
-
  import NakedButton from "@app/components/NakedButton.svelte";
  import NotificationTeaser from "@app/components/NotificationTeaser.svelte";

  interface Props {
@@ -40,7 +39,6 @@
  .header {
    display: flex;
    align-items: center;
-
    padding-right: 1rem;
    width: 100%;
    min-height: 2rem;
    gap: 0.75rem;
@@ -48,14 +46,16 @@
  .container {
    display: flex;
    flex-direction: column;
-
    gap: 2px;
+
    gap: 1px;
  }
  .action-buttons {
    display: flex;
    gap: 0.25rem;
+
    color: var(--color-text-tertiary);
  }
  .clear-repo {
    margin-left: auto;
+
    color: var(--color-text-tertiary);
  }
  .action-buttons,
  .clear-repo {
@@ -72,32 +72,32 @@
    class="header"
    class:txt-missing={hidden}
    style:margin-bottom={!hidden ? "1rem" : undefined}>
-
    <span class="txt-bold">
+
    <span class="txt-body-l-semibold">
      {name}
    </span>
-
    {count}
+
    <span class="global-counter-badge">{count}</span>
    <div
      class="action-buttons"
      style:display={pinned || hidden ? "flex" : undefined}>
      {#if !hidden}
-
        <NakedButton
-
          variant="ghost"
+
        <Button
+
          variant="naked"
          stylePadding="0 0.25rem"
          onclick={() => {
            togglePin(rid);
          }}>
-
          <Icon name={pinned ? "pin" : "pin-hollow"} />
-
        </NakedButton>
+
          <Icon name={pinned ? "pin-filled" : "pin-hollow"} />
+
        </Button>
      {/if}
      {#if !pinned}
-
        <NakedButton
-
          variant="ghost"
+
        <Button
+
          variant="naked"
          stylePadding="0 0.25rem"
          onclick={() => {
            toggleHide(rid);
          }}>
-
          <Icon name={hidden ? "eye-closed" : "eye"} />
-
        </NakedButton>
+
          <Icon name={hidden ? "eye-slash" : "eye"} />
+
        </Button>
      {/if}
    </div>
    {#if count > 0 && !hidden}
@@ -128,7 +128,9 @@
          style:height="100%"
          style:align-items="center"
          style:justify-content="center">
-
          <div class="txt-missing txt-small global-flex" style:gap="0.25rem">
+
          <div
+
            class="txt-missing txt-body-m-regular global-flex"
+
            style:gap="0.25rem">
            <Icon name="none" />
            No notifications.
          </div>
deleted src/components/OutlineButton.svelte
@@ -1,331 +0,0 @@
-
<script lang="ts">
-
  import type { Snippet } from "svelte";
-

-
  interface Props {
-
    id?: string;
-
    popoverToggle?: string;
-
    active?: boolean;
-
    children: Snippet;
-
    disabled?: boolean;
-
    onclick?: () => void;
-
    styleHeight?: "2rem" | "2.5rem";
-
    title?: string;
-
    variant: "primary" | "secondary" | "ghost";
-
  }
-

-
  const {
-
    id,
-
    popoverToggle,
-
    active = false,
-
    children,
-
    disabled = false,
-
    onclick,
-
    styleHeight = "2rem",
-
    title,
-
    variant,
-
  }: Props = $props();
-

-
  const style = $derived(
-
    `--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);` +
-
      `--text-color-hover: ${variant === "ghost" ? "var(--color-foreground-contrast)" : "var(--color-foreground-white)"};` +
-
      `--text-color-active: ${variant === "ghost" ? "var(--color-foreground-emphasized)" : "var(--color-foreground-white)"};`,
-
  );
-
</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: 0 0.5rem;
-
    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:not(.disabled) .p1-3 {
-
    background-color: var(--button-color-1);
-
  }
-

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

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

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

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

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

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

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

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

-
  .container:hover:not(.disabled) {
-
    color: var(--text-color-hover);
-
  }
-

-
  .container.active:not(.disabled) {
-
    color: var(--text-color-active);
-
  }
-

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

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

-
  .container {
-
    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
-
  {id}
-
  data-popover-toggle={popoverToggle}
-
  class:active
-
  class:disabled
-
  class="container"
-
  onclick={!disabled ? onclick : undefined}
-
  role="button"
-
  style:cursor={!disabled ? "pointer" : "default"}
-
  style:height={styleHeight}
-
  tabindex="0"
-
  {style}
-
  {title}>
-
  <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">
-
    {@render children()}
-
  </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>
modified src/components/PatchMetadata.svelte
@@ -7,12 +7,10 @@
  import { nodeRunning } from "@app/lib/events";
  import { invoke } from "@app/lib/invoke";
  import * as roles from "@app/lib/roles";
-
  import { authorForNodeId } from "@app/lib/utils";

  import { announce } from "@app/components/AnnounceSwitch.svelte";
  import AssigneeInput from "@app/components/AssigneeInput.svelte";
  import LabelInput from "@app/components/LabelInput.svelte";
-
  import NodeId from "@app/components/NodeId.svelte";
  import PatchStateButton from "@app/components/PatchStateButton.svelte";

  interface Props {
@@ -80,16 +78,12 @@
<style>
  .metadata-section {
    padding: 0.5rem;
-
    font-size: var(--font-size-small);
+
    font: var(--txt-body-m-regular);
    display: flex;
    flex-direction: column;
    align-items: flex;
    height: 100%;
  }
-
  .metadata-section-title {
-
    margin-bottom: 0.5rem;
-
    color: var(--color-foreground-dim);
-
  }
</style>

<div
@@ -99,29 +93,24 @@
  <div
    class="metadata-section"
    style={horizontal ? "flex: 1;" : "width: 100%;"}>
-
    <div class="metadata-section-title">Author</div>
-
    <NodeId {...authorForNodeId(patch.author)} />
-
  </div>
-

-
  <div
-
    class="metadata-section"
-
    style={horizontal ? "flex: 1;" : "width: 100%;"}>
-
    <div class="metadata-section-title">Status</div>
    <PatchStateButton
      selectedState={patch.state}
      onSelect={newState => {
        void saveState(newState);
-
      }} />
+
      }}
+
      disabled={!roles.isDelegate(
+
        config.publicKey,
+
        repo.delegates.map(d => d.did),
+
      )} />
  </div>

  <div
    class="metadata-section"
    style={horizontal ? "flex: 1;" : "width: 100%;"}>
    <LabelInput
-
      allowedToEdit={!!roles.isDelegateOrAuthor(
+
      allowedToEdit={!!roles.isDelegate(
        config.publicKey,
        repo.delegates.map(delegate => delegate.did),
-
        patch.author.did,
      )}
      labels={patch.labels}
      submitInProgress={labelSaveInProgress}
@@ -132,10 +121,9 @@
    class="metadata-section"
    style={horizontal ? "flex: 1;" : "width: 100%;"}>
    <AssigneeInput
-
      allowedToEdit={!!roles.isDelegateOrAuthor(
+
      allowedToEdit={!!roles.isDelegate(
        config.publicKey,
        repo.delegates.map(delegate => delegate.did),
-
        patch.author.did,
      )}
      assignees={patch.assignees}
      submitInProgress={assigneesSaveInProgress}
modified src/components/PatchStateButton.svelte
@@ -5,7 +5,7 @@

  import { patchStatusBackgroundColor, patchStatusColor } from "@app/lib/utils";

-
  import Border from "@app/components/Border.svelte";
+
  import Button from "@app/components/Button.svelte";
  import DropdownList from "@app/components/DropdownList.svelte";
  import DropdownListItem from "@app/components/DropdownListItem.svelte";
  import Icon from "@app/components/Icon.svelte";
@@ -15,67 +15,63 @@
  interface Props {
    selectedState: State;
    onSelect: (newState: State) => void;
+
    disabled?: boolean;
  }

-
  const { selectedState, onSelect }: Props = $props();
+
  const { selectedState, onSelect, disabled = false }: Props = $props();

  let popoverExpanded: boolean = $state(false);
</script>

-
<style>
-
  button {
-
    cursor: pointer;
-
    border: 0;
-
    background: none;
-
    margin: 0;
-
    padding: 0;
-
    display: flex;
-
    align-items: center;
-
    justify-content: center;
-
    font-size: var(--font-size-small);
-
    background-color: none;
-
  }
-
  button:disabled {
-
    cursor: inherit;
-
  }
-

-
  .badge {
-
    gap: 0.375rem;
-
    padding-right: 0.625rem;
-
  }
-
</style>
-

<Popover
  popoverPadding="0"
-
  popoverPositionTop="2rem"
-
  popoverPositionLeft="0"
+
  placement="bottom-start"
  bind:expanded={popoverExpanded}>
  {#snippet toggle(onclick)}
-
    <button
-
      disabled={selectedState.status === "merged"}
+
    <Button
+
      variant="outline"
+
      disabled={selectedState.status === "merged" || disabled}
      {onclick}
+
      active={popoverExpanded}
      title={selectedState.status === "merged"
        ? "The state of merged patches can not be changed"
-
        : "Click to change patch state"}>
+
        : disabled
+
          ? "You must be a delegate to change the patch state"
+
          : undefined}>
      <span
-
        class="global-counter badge"
-
        style:color={patchStatusColor[selectedState.status]}
-
        style:background-color={patchStatusBackgroundColor[
-
          selectedState.status
-
        ]}>
+
        class="global-chip"
+
        style:padding="0"
+
        style:margin-left="-0.25rem"
+
        style:color={selectedState.status === "merged" || disabled
+
          ? undefined
+
          : patchStatusColor[selectedState.status]}
+
        style:background-color={selectedState.status === "merged" || disabled
+
          ? undefined
+
          : patchStatusBackgroundColor[selectedState.status]}>
        <Icon
          name={selectedState.status === "open"
            ? "patch"
            : `patch-${selectedState.status}`} />
+
      </span>
+
      <span
+
        style:color={selectedState.status === "merged" || disabled
+
          ? undefined
+
          : "var(--color-text-secondary)"}>
        {capitalize(selectedState.status)}
-
        {#if selectedState.status !== "merged"}
-
          <Icon name={popoverExpanded ? "chevron-up" : "chevron-down"} />
-
        {/if}
      </span>
-
    </button>
+
      {#if selectedState.status !== "merged"}
+
        <Icon name={popoverExpanded ? "chevron-up" : "chevron-down"} />
+
      {/if}
+
    </Button>
  {/snippet}
  {#snippet popover()}
-
    <Border variant="ghost">
+
    <div
+
      style:border="1px solid var(--color-border-subtle)"
+
      style:border-radius="var(--border-radius-sm)"
+
      style:display="flex"
+
      style:gap="0.5rem"
+
      style:align-items="center"
+
      style:background-color="var(--color-surface-canvas)">
      <DropdownList
        items={[
          { status: "open" },
@@ -85,22 +81,28 @@
        {#snippet item(state)}
          <DropdownListItem
            selected={selectedState.status === state.status}
+
            styleGap="0.5rem"
            onclick={() => {
              onSelect(state);
              closeFocused();
            }}>
            <span
-
              class="global-flex"
-
              style:color={patchStatusColor[state.status]}>
+
              class="global-chip"
+
              style:padding="0"
+
              style:margin-left="-0.5rem"
+
              style:color={patchStatusColor[state.status]}
+
              style:background-color={patchStatusBackgroundColor[state.status]}>
              <Icon
                name={state.status === "open"
                  ? "patch"
                  : `patch-${state.status}`} />
+
            </span>
+
            <span style:color="var(--color-text-secondary)">
              {capitalize(state.status)}
            </span>
          </DropdownListItem>
        {/snippet}
      </DropdownList>
-
    </Border>
+
    </div>
  {/snippet}
</Popover>
deleted src/components/PatchStateButtonCompact.svelte
@@ -1,120 +0,0 @@
-
<script lang="ts">
-
  import type { State } from "@bindings/cob/patch/State";
-
  import type { ComponentProps } from "svelte";
-

-
  import capitalize from "lodash/capitalize";
-

-
  import { patchStatusBackgroundColor, patchStatusColor } from "@app/lib/utils";
-

-
  import Border from "@app/components/Border.svelte";
-
  import DropdownList from "@app/components/DropdownList.svelte";
-
  import DropdownListItem from "@app/components/DropdownListItem.svelte";
-
  import Icon from "@app/components/Icon.svelte";
-
  import { closeFocused } from "@app/components/Popover.svelte";
-
  import Popover from "@app/components/Popover.svelte";
-

-
  interface Props {
-
    selectedState: State;
-
    onSelect: (newState: State) => void;
-
  }
-

-
  const { selectedState, onSelect }: Props = $props();
-
  let focus = $state(false);
-

-
  let popoverExpanded: boolean = $state(false);
-

-
  function icon(): ComponentProps<typeof Icon>["name"] {
-
    if (selectedState.status === "merged") {
-
      return "patch-merged";
-
    } else if (focus) {
-
      return "chevron-down";
-
    } else {
-
      if (selectedState.status === "open") {
-
        return "patch";
-
      } else {
-
        return `patch-${selectedState.status}`;
-
      }
-
    }
-
  }
-
</script>
-

-
<style>
-
  button {
-
    cursor: pointer;
-
    border: 0;
-
    background: none;
-
    margin: 0;
-
    padding: 0;
-
    display: flex;
-
    align-items: center;
-
    justify-content: center;
-
    font-size: var(--font-size-small);
-
  }
-
  button:disabled {
-
    cursor: inherit;
-
  }
-

-
  .badge {
-
    height: 2.5rem;
-
    width: 2.5rem;
-
    gap: 0.375rem;
-
    padding-right: 0.625rem;
-
  }
-
</style>
-

-
<Popover
-
  popoverPadding="0"
-
  popoverPositionTop="3rem"
-
  popoverPositionLeft="0"
-
  bind:expanded={popoverExpanded}>
-
  {#snippet toggle(onclick)}
-
    <button
-
      onmouseenter={() => (focus = true)}
-
      onfocus={() => (focus = true)}
-
      onblur={() => (focus = false)}
-
      onmouseleave={() => (focus = false)}
-
      disabled={selectedState.status === "merged"}
-
      {onclick}
-
      title={selectedState.status === "merged"
-
        ? "The state of merged patches can not be changed"
-
        : "Click to change patch state"}>
-
      <span
-
        class="global-counter badge"
-
        style:color={patchStatusColor[selectedState.status]}
-
        style:background-color={patchStatusBackgroundColor[
-
          selectedState.status
-
        ]}>
-
        <Icon name={icon()} />
-
      </span>
-
    </button>
-
  {/snippet}
-
  {#snippet popover()}
-
    <Border variant="ghost">
-
      <DropdownList
-
        items={[
-
          { status: "open" },
-
          { status: "draft" },
-
          { status: "archived" },
-
        ] as State[]}>
-
        {#snippet item(state)}
-
          <DropdownListItem
-
            selected={selectedState.status === state.status}
-
            onclick={() => {
-
              onSelect(state);
-
              closeFocused();
-
            }}>
-
            <span
-
              class="global-flex"
-
              style:color={patchStatusColor[state.status]}>
-
              <Icon
-
                name={state.status === "open"
-
                  ? "patch"
-
                  : `patch-${state.status}`} />
-
              {capitalize(state.status)}
-
            </span>
-
          </DropdownListItem>
-
        {/snippet}
-
      </DropdownList>
-
    </Border>
-
  {/snippet}
-
</Popover>
deleted src/components/PatchStateFilterButton.svelte
@@ -1,85 +0,0 @@
-
<script lang="ts">
-
  import type { PatchStatus } from "@app/views/repo/router";
-
  import type { ProjectPayloadMeta } from "@bindings/repo/ProjectPayloadMeta";
-

-
  import capitalize from "lodash/capitalize";
-

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

-
  import Border from "@app/components/Border.svelte";
-
  import DropdownList from "@app/components/DropdownList.svelte";
-
  import DropdownListItem from "@app/components/DropdownListItem.svelte";
-
  import Icon from "@app/components/Icon.svelte";
-
  import OutlineButton from "@app/components/OutlineButton.svelte";
-
  import { closeFocused } from "@app/components/Popover.svelte";
-
  import Popover from "@app/components/Popover.svelte";
-

-
  interface Props {
-
    counters: ProjectPayloadMeta["patches"];
-
    select: (filter: PatchStatus | undefined) => Promise<void>;
-
    status: PatchStatus | undefined;
-
  }
-

-
  const { status, select, counters }: Props = $props();
-

-
  let popoverExpanded: boolean = $state(false);
-
</script>
-

-
{#snippet iconSnippet(status: PatchStatus | undefined)}
-
  <div class="icon" style:color={status ? patchStatusColor[status] : undefined}>
-
    <Icon
-
      name={status === undefined || status === "open"
-
        ? "patch"
-
        : `patch-${status}`} />
-
  </div>
-
{/snippet}
-

-
{#snippet counterSnippet(status: PatchStatus | undefined)}
-
  <div style:margin-left="auto" style:padding-left="0.25rem">
-
    {#if status}
-
      {counters[status]}
-
    {:else}
-
      {counters.draft + counters.open + counters.archived + counters.merged}
-
    {/if}
-
  </div>
-
{/snippet}
-

-
<Popover
-
  popoverPositionLeft="0"
-
  popoverPositionTop="3rem"
-
  bind:expanded={popoverExpanded}>
-
  {#snippet toggle(onclick)}
-
    <OutlineButton
-
      variant="ghost"
-
      {onclick}
-
      styleHeight="2.5rem"
-
      active={popoverExpanded}>
-
      {@render iconSnippet(status)}
-
      {status ? capitalize(status) : "All"}
-
      {@render counterSnippet(status)}
-
      <Icon name={popoverExpanded ? "chevron-up" : "chevron-down"} />
-
    </OutlineButton>
-
  {/snippet}
-

-
  {#snippet popover()}
-
    <Border variant="ghost">
-
      <DropdownList
-
        items={[undefined, "draft", "open", "archived", "merged"] as const}>
-
        {#snippet item(state)}
-
          <DropdownListItem
-
            styleGap="0.5rem"
-
            styleMinHeight="2.5rem"
-
            selected={status === state}
-
            onclick={async () => {
-
              await select(state);
-
              closeFocused();
-
            }}>
-
            {@render iconSnippet(state)}
-
            {state ? capitalize(state) : "All"}
-
            {@render counterSnippet(state)}
-
          </DropdownListItem>
-
        {/snippet}
-
      </DropdownList>
-
    </Border>
-
  {/snippet}
-
</Popover>
modified src/components/PatchTeaser.svelte
@@ -11,7 +11,6 @@
    patchStatusColor,
  } from "@app/lib/utils";

-
  import Border from "@app/components/Border.svelte";
  import DiffStatBadge from "@app/components/DiffStatBadge.svelte";
  import Icon from "@app/components/Icon.svelte";
  import Id from "@app/components/Id.svelte";
@@ -20,24 +19,13 @@
  import NodeId from "@app/components/NodeId.svelte";

  interface Props {
-
    compact?: boolean;
    focussed?: boolean;
-
    loadPatch?: (patchId: string) => Promise<void>;
    patch: Patch;
    rid: string;
-
    selected?: boolean;
    status: PatchStatus | undefined;
  }

-
  const {
-
    compact = false,
-
    focussed,
-
    loadPatch,
-
    patch,
-
    rid,
-
    selected = false,
-
    status,
-
  }: Props = $props();
+
  const { focussed, patch, rid, status }: Props = $props();
</script>

<style>
@@ -47,31 +35,28 @@
    justify-content: space-between;
    gap: 0.25rem;
    min-height: 5rem;
-
    background-color: var(--color-background-float);
+
    background-color: var(--color-surface-canvas);
    padding: 1rem;
    cursor: pointer;
-
    font-size: var(--font-size-regular);
+
    font: var(--txt-body-l-regular);
    word-break: break-word;
    width: 100%;
  }
-
  .selected {
-
    background-color: var(--color-fill-float-hover);
-
  }
  .patch-teaser:hover {
-
    background-color: var(--color-fill-float-hover);
+
    background-color: var(--color-surface-subtle);
  }
  .status {
    padding: 0;
    margin-right: 1rem;
  }
  .patch-teaser:first-of-type {
-
    clip-path: var(--2px-top-corner-fill);
+
    border-radius: var(--border-radius-sm) var(--border-radius-sm) 0 0;
  }
  .patch-teaser:last-of-type {
-
    clip-path: var(--2px-bottom-corner-fill);
+
    border-radius: 0 0 var(--border-radius-sm) var(--border-radius-sm);
  }
  .patch-teaser:only-of-type {
-
    clip-path: var(--2px-corner-fill);
+
    border-radius: var(--border-radius-sm);
  }
</style>

@@ -80,27 +65,22 @@
  <div
    tabindex="0"
    role="button"
-
    class:selected
    class="patch-teaser"
    style:align-items="flex-start"
    style:clip-path={focussed ? "none" : undefined}
    style:padding={focussed ? "1rem" : "1.25rem"}
-
    onclick={async () => {
-
      if (loadPatch) {
-
        await loadPatch(patch.id);
-
      } else {
-
        void push({
-
          resource: "repo.patch",
-
          rid,
-
          patch: patch.id,
-
          status,
-
          reviewId: undefined,
-
        });
-
      }
+
    onclick={() => {
+
      void push({
+
        resource: "repo.patch",
+
        rid,
+
        patch: patch.id,
+
        status,
+
        reviewId: undefined,
+
      });
    }}>
    <div class="global-flex" style:align-items="flex-start">
      <div
-
        class="global-counter status"
+
        class="global-chip status"
        style:color={patchStatusColor[patch.state.status]}
        style:background-color={patchStatusBackgroundColor[patch.state.status]}>
        <Icon
@@ -113,29 +93,32 @@
        style:flex-direction="column"
        style:align-items="flex-start">
        <InlineTitle content={patch.title} />
-
        <div class="global-flex txt-small" style:flex-wrap="wrap">
+
        <div class="global-flex txt-body-m-regular" style:flex-wrap="wrap">
          <NodeId {...authorForNodeId(patch.author)} />
          opened
-
          <Id id={patch.id} clipboard={patch.id} variant="oid" />
+
          <Id id={patch.id} clipboard={patch.id} />
          {formatTimestamp(patch.timestamp)}
        </div>
      </div>
    </div>

    <div class="global-flex" style:margin-left="auto">
-
      {#if !compact}
-
        {#await cachedDiffStats(rid, patch.base, patch.head) then stats}
-
          <DiffStatBadge {stats} />
-
        {/await}
+
      {#await cachedDiffStats(rid, patch.base, patch.head) then stats}
+
        <DiffStatBadge {stats} />
+
      {/await}

-
        {#each patch.labels as label}
-
          <Label {label} />
-
        {/each}
-
      {/if}
+
      {#each patch.labels as label}
+
        <Label {label} />
+
      {/each}
      <div
-
        class="txt-small global-flex"
+
        class="txt-body-m-regular global-flex"
        style:gap="0.25rem"
-
        style:white-space="nowrap">
+
        style:white-space="nowrap"
+
        style:border="1px solid var(--color-border-subtle)"
+
        style:border-radius="var(--border-radius-sm)"
+
        style:height="1.5rem"
+
        style:padding="0 0.5rem"
+
        style:color="var(--color-text-tertiary)">
        <Icon name="revision" />
        {patch.revisionCount}
      </div>
@@ -144,11 +127,15 @@
{/snippet}

{#if focussed}
-
  <Border
-
    styleBackgroundColor="var(--color-background-float)"
-
    variant="secondary">
+
  <div
+
    style:border="1px solid var(--color-border-brand)"
+
    style:border-radius="var(--border-radius-sm)"
+
    style:display="flex"
+
    style:gap="0.5rem"
+
    style:align-items="center"
+
    style:background-color="var(--color-surface-canvas)">
    {@render patchSnippet()}
-
  </Border>
+
  </div>
{:else}
  {@render patchSnippet()}
{/if}
modified src/components/PatchTimeline.svelte
@@ -95,18 +95,18 @@
  }
</style>

-
<div class="timeline txt-small">
+
<div class="timeline txt-body-m-regular">
  {#each timeline as op}
    {#if op.type === "revision"}
      {#if op.id === patchId}
        <div class="timeline-item">
-
          <div class="icon" style:color="var(--color-fill-success)">
+
          <div class="icon" style:color="var(--color-feedback-success-text)">
            <Icon name="patch" />
          </div>
          <div class="wrapper">
            <NodeId {...authorForNodeId(op.author)} />
            <div>
-
              opened patch <Id id={op.id} clipboard={op.id} variant="oid" />
+
              opened patch <Id id={op.id} clipboard={op.id} />
            </div>
            <div title={absoluteTimestamp(op.timestamp)}>
              {formatTimestamp(op.timestamp)}
@@ -121,7 +121,7 @@
          <div class="wrapper">
            <NodeId {...authorForNodeId(op.author)} />
            <div>
-
              created revision <Id id={op.id} clipboard={op.id} variant="oid" />
+
              created revision <Id id={op.id} clipboard={op.id} />
            </div>
            <div title={absoluteTimestamp(op.timestamp)}>
              {formatTimestamp(op.timestamp)}
@@ -188,7 +188,7 @@
    {:else if op.type === "assign"}
      <div class="timeline-item">
        <div class="icon">
-
          <Icon name="user" />
+
          <Icon name="avatar-incognito" />
        </div>
        <div class="wrapper">
          <NodeId {...authorForNodeId(op.author)} />
@@ -222,7 +222,7 @@
      </div>
    {:else if op.type === "merge"}
      <div class="timeline-item">
-
        <div class="icon" style:color="var(--color-fill-primary)">
+
        <div class="icon" style:color="var(--color-brand-bg)">
          <Icon name="patch-merged" />
        </div>
        <div class="wrapper">
@@ -230,8 +230,7 @@
          <div>
            merged patch at revision <Id
              id={op.revision}
-
              clipboard={op.revision}
-
              variant="oid" />
+
              clipboard={op.revision} />
          </div>
          <div title={absoluteTimestamp(op.timestamp)}>
            {formatTimestamp(op.timestamp)}
@@ -242,7 +241,7 @@
      {#if op.previous && op.previous.type === op.type}
        <div class="timeline-item">
          <div class="icon">
-
            <Icon name="pen" />
+
            <Icon name="edit" />
          </div>
          <div class="wrapper">
            <NodeId {...authorForNodeId(op.author)} />
@@ -258,29 +257,23 @@
    {:else if op.type === "review"}
      <div class="timeline-item">
        {#if op.verdict === "accept"}
-
          <div class="icon" style:color="var(--color-foreground-success)">
-
            <Icon name="thumb-up" />
+
          <div class="icon" style:color="var(--color-feedback-success-text)">
+
            <Icon name="thumbs-up" />
          </div>
          <div class="wrapper">
            <NodeId {...authorForNodeId(op.author)} />
-
            accepted revision <Id
-
              id={op.revision}
-
              clipboard={op.revision}
-
              variant="oid" />
+
            accepted revision <Id id={op.revision} clipboard={op.revision} />
            <div title={absoluteTimestamp(op.timestamp)}>
              {formatTimestamp(op.timestamp)}
            </div>
          </div>
        {:else if op.verdict === "reject"}
-
          <div class="icon" style:color="var(--color-foreground-red)">
+
          <div class="icon" style:color="var(--color-feedback-error-text)">
            <Icon name="stop" />
          </div>
          <div class="wrapper">
            <NodeId {...authorForNodeId(op.author)} />
-
            rejected revision <Id
-
              id={op.revision}
-
              clipboard={op.revision}
-
              variant="oid" />
+
            rejected revision <Id id={op.revision} clipboard={op.revision} />
            <div title={absoluteTimestamp(op.timestamp)}>
              {formatTimestamp(op.timestamp)}
            </div>
@@ -291,10 +284,7 @@
          </div>
          <div class="wrapper">
            <NodeId {...authorForNodeId(op.author)} />
-
            reviewed revision <Id
-
              id={op.revision}
-
              clipboard={op.revision}
-
              variant="oid" />
+
            reviewed revision <Id id={op.revision} clipboard={op.revision} />
            <div title={absoluteTimestamp(op.timestamp)}>
              {formatTimestamp(op.timestamp)}
            </div>
@@ -310,8 +300,7 @@
          <NodeId {...authorForNodeId(op.author)} />
          {op.replyTo ? "replied to a comment" : "commented"} on review <Id
            id={op.review}
-
            clipboard={op.review}
-
            variant="oid" />
+
            clipboard={op.review} />
          <div title={absoluteTimestamp(op.timestamp)}>
            {formatTimestamp(op.timestamp)}
          </div>
@@ -326,8 +315,7 @@
          <NodeId {...authorForNodeId(op.author)} />
          {op.replyTo ? "replied to a comment" : "commented"} on revision <Id
            id={op.revision}
-
            clipboard={op.revision}
-
            variant="oid" />
+
            clipboard={op.revision} />
          <div title={absoluteTimestamp(op.timestamp)}>
            {formatTimestamp(op.timestamp)}
          </div>
deleted src/components/PatchesSecondColumn.svelte
@@ -1,207 +0,0 @@
-
<script lang="ts">
-
  import type { PatchStatus } from "@app/views/repo/router";
-
  import type { ProjectPayload } from "@bindings/repo/ProjectPayload";
-
  import type { RepoInfo } from "@bindings/repo/RepoInfo";
-

-
  import Border from "@app/components/Border.svelte";
-
  import Icon from "@app/components/Icon.svelte";
-
  import Link from "@app/components/Link.svelte";
-
  import RepoTeaser from "@app/components/RepoTeaser.svelte";
-
  import Settings from "@app/components/Settings.svelte";
-

-
  interface Props {
-
    project: ProjectPayload;
-
    status: PatchStatus | undefined;
-
    repo: RepoInfo;
-
  }
-
  const { project, status, repo }: Props = $props();
-
</script>
-

-
<style>
-
  .container {
-
    display: flex;
-
    flex-direction: column;
-
    height: 100%;
-
    justify-content: space-between;
-
  }
-
  .tab {
-
    align-items: center;
-
    background-color: var(--color-background-float);
-
    clip-path: var(--1px-corner-fill);
-
    display: flex;
-
    font-size: var(--font-size-small);
-
    justify-content: space-between;
-
    padding: 0.5rem 0.25rem 0.5rem 0.5rem;
-
    width: 100%;
-
  }
-
  .tab:not(.active) {
-
    color: var(--color-foreground-dim);
-
  }
-
  .tab:not(.active):hover {
-
    background-color: var(--color-fill-float-hover);
-
  }
-
  .active {
-
    background-color: var(--color-background-default);
-
    font-weight: var(--font-weight-semibold);
-
  }
-
  .highlight {
-
    color: var(--color-foreground-contrast);
-
  }
-
  .draft {
-
    color: var(--color-fill-gray);
-
  }
-
  .open {
-
    color: var(--color-fill-success);
-
  }
-
  .archived {
-
    color: var(--color-foreground-yellow);
-
  }
-
  .merged {
-
    color: var(--color-foreground-primary);
-
  }
-
</style>
-

-
<div class="container">
-
  <div>
-
    <div style:margin-bottom="0.75rem">
-
      <Link
-
        styleWidth="100%"
-
        underline={false}
-
        route={{ resource: "repo.home", rid: repo.rid }}>
-
        <div
-
          class="tab"
-
          style:color="var(--color-foreground-contrast)"
-
          style:padding-right="0.5rem"
-
          style:padding-left="0.75rem">
-
          <RepoTeaser name={project.data.name} seeding={repo.seeding} />
-
        </div>
-
      </Link>
-
    </div>
-

-
    <div style:margin-bottom="0.5rem">
-
      <Link
-
        styleWidth="100%"
-
        underline={false}
-
        route={{ resource: "repo.issues", rid: repo.rid, status: "open" }}>
-
        <div
-
          class="tab"
-
          style:color="var(--color-foreground-contrast)"
-
          style:padding-left="0.75rem">
-
          <div class="global-flex"><Icon name="issue" />Issues</div>
-
          <div class="global-counter">
-
            {project.meta.issues.open + project.meta.issues.closed}
-
          </div>
-
        </div>
-
      </Link>
-
    </div>
-

-
    <Border
-
      variant="ghost"
-
      styleFlexDirection="column"
-
      styleGap="2px"
-
      styleBackgroundColor="var(--color-background-float)">
-
      <Link
-
        styleWidth="100%"
-
        underline={false}
-
        route={{ resource: "repo.patches", rid: repo.rid, status: undefined }}>
-
        <div class="tab active">
-
          <div class="global-flex"><Icon name="patch" />Patches</div>
-
          <div class="global-counter">
-
            {project.meta.patches.draft +
-
              project.meta.patches.open +
-
              project.meta.patches.archived +
-
              project.meta.patches.merged}
-
          </div>
-
        </div>
-
      </Link>
-

-
      <Link
-
        styleWidth="100%"
-
        underline={false}
-
        route={{
-
          resource: "repo.patches",
-
          rid: repo.rid,
-
          status: "open",
-
        }}>
-
        <div class="tab" class:active={status === "open"}>
-
          <div
-
            class="global-flex"
-
            class:open={["open", undefined].includes(status)}>
-
            <Icon name="patch" />
-
            Open
-
          </div>
-
          <div class="global-counter" class:highlight={status === undefined}>
-
            {project.meta.patches.open}
-
          </div>
-
        </div>
-
      </Link>
-

-
      <Link
-
        styleWidth="100%"
-
        underline={false}
-
        route={{
-
          resource: "repo.patches",
-
          rid: repo.rid,
-
          status: "merged",
-
        }}>
-
        <div class="tab" class:active={status === "merged"}>
-
          <div
-
            class="global-flex"
-
            class:merged={["merged", undefined].includes(status)}>
-
            <Icon name="patch-merged" />Merged
-
          </div>
-
          <div class="global-counter" class:highlight={status === undefined}>
-
            {project.meta.patches.merged}
-
          </div>
-
        </div>
-
      </Link>
-

-
      <Link
-
        styleWidth="100%"
-
        underline={false}
-
        route={{
-
          resource: "repo.patches",
-
          rid: repo.rid,
-
          status: "archived",
-
        }}>
-
        <div class="tab" class:active={status === "archived"}>
-
          <div
-
            class="global-flex"
-
            class:archived={["archived", undefined].includes(status)}>
-
            <Icon name="patch-archived" />Archived
-
          </div>
-
          <div class="global-counter" class:highlight={status === undefined}>
-
            {project.meta.patches.archived}
-
          </div>
-
        </div>
-
      </Link>
-
      <Link
-
        styleWidth="100%"
-
        underline={false}
-
        route={{
-
          resource: "repo.patches",
-
          rid: repo.rid,
-
          status: "draft",
-
        }}>
-
        <div class="tab" class:active={status === "draft"}>
-
          <div
-
            class="global-flex"
-
            class:draft={["draft", undefined].includes(status)}>
-
            <Icon name="patch-draft" />
-
            Draft
-
          </div>
-
          <div class="global-counter" class:highlight={status === undefined}>
-
            {project.meta.patches.draft}
-
          </div>
-
        </div>
-
      </Link>
-
    </Border>
-
  </div>
-

-
  <Settings
-
    compact={false}
-
    popoverProps={{
-
      popoverPositionBottom: "3rem",
-
      popoverPositionLeft: "0",
-
    }} />
-
</div>
modified src/components/Path.svelte
@@ -14,16 +14,12 @@
    user-select: text;
  }
  .path {
-
    color: var(--color-fill-gray);
-
    font-weight: var(--font-weight-regular);
-
  }
-
  .filename {
-
    font-weight: var(--font-weight-semibold);
+
    color: var(--color-text-quaternary);
  }
</style>

<!-- prettier-ignore -->
-
<span class="txt-small container">
+
<span class="txt-body-m-regular container">
  <span class="path">
    {fullPath
      .match(/^.*\/|/)
modified src/components/Popover.svelte
@@ -1,7 +1,7 @@
<script lang="ts" module>
-
  let focused = $state<{ element: HTMLDivElement; id: string } | undefined>(
-
    undefined,
-
  );
+
  let focused = $state<
+
    { id: string; floatingEl: HTMLElement | undefined } | undefined
+
  >(undefined);

  export function closeFocused() {
    focused = undefined;
@@ -9,17 +9,25 @@
</script>

<script lang="ts">
+
  import type { Placement } from "@floating-ui/dom";
  import type { Snippet } from "svelte";

+
  import {
+
    autoUpdate,
+
    computePosition,
+
    flip,
+
    offset as floatingOffset,
+
    shift,
+
  } from "@floating-ui/dom";
+

+
  import { portal } from "@app/lib/portal";
+

  interface Props {
    toggle: Snippet<[() => void]>;
    popover: Snippet;
-
    popoverContainerMinWidth?: string;
+
    placement?: Placement;
+
    offset?: number;
    popoverPadding?: string;
-
    popoverPositionBottom?: string;
-
    popoverPositionLeft?: string;
-
    popoverPositionRight?: string;
-
    popoverPositionTop?: string;
    expanded?: boolean;
  }

@@ -27,45 +35,56 @@
  let {
    toggle,
    popover,
-
    popoverContainerMinWidth,
+
    placement = "bottom-start",
+
    offset: offsetPx = 4,
    popoverPadding,
-
    popoverPositionBottom,
-
    popoverPositionLeft,
-
    popoverPositionRight,
-
    popoverPositionTop,
    expanded = $bindable(false),
  }: Props = $props();
  /* eslint-enable prefer-const */

  const id = crypto.randomUUID();
-
  let thisComponent: HTMLDivElement | undefined = $state();
+
  let containerEl: HTMLDivElement | undefined = $state();
+
  let floatingEl: HTMLDivElement | undefined = $state();

  function clickOutside(ev: MouseEvent | TouchEvent) {
-
    const toggleElement = document.querySelector(
-
      `[data-popover-toggle="${id}"]`,
-
    );
-
    if (focused && !ev.composedPath().includes(focused.element)) {
-
      if (
-
        thisComponent === focused.element &&
-
        !ev.composedPath().includes(toggleElement!)
-
      ) {
+
    if (focused?.id === id) {
+
      const path = ev.composedPath();
+
      const insideContainer = containerEl && path.includes(containerEl);
+
      const insideFloating = floatingEl && path.includes(floatingEl);
+
      if (!insideContainer && !insideFloating) {
        closeFocused();
      }
    }
  }

  function toggleFn() {
-
    if (thisComponent) {
-
      if (focused?.element === thisComponent) {
-
        closeFocused();
-
      } else {
-
        focused = { element: thisComponent, id };
-
      }
+
    if (focused?.id === id) {
+
      closeFocused();
+
    } else {
+
      focused = { id, floatingEl: undefined };
    }
  }

  $effect(() => {
-
    expanded = focused?.element === thisComponent;
+
    expanded = focused?.id === id;
+
  });
+

+
  $effect(() => {
+
    if (floatingEl && containerEl) {
+
      const cleanup = autoUpdate(containerEl, floatingEl, () => {
+
        void computePosition(containerEl!, floatingEl!, {
+
          placement,
+
          middleware: [floatingOffset(offsetPx), flip(), shift({ padding: 8 })],
+
        }).then(({ x, y }) => {
+
          if (floatingEl) {
+
            floatingEl.style.left = `${x}px`;
+
            floatingEl.style.top = `${y}px`;
+
            floatingEl.style.visibility = "visible";
+
          }
+
        });
+
      });
+
      return cleanup;
+
    }
  });
</script>

@@ -75,27 +94,24 @@
  }
  .popover {
    box-shadow: var(--elevation-low);
-
    position: absolute;
+
    position: fixed;
+
    top: 0;
+
    left: 0;
+
    visibility: hidden;
    z-index: 10;
  }
</style>

<svelte:window onclick={clickOutside} ontouchstart={clickOutside} />

-
<div
-
  data-popover-id={id}
-
  bind:this={thisComponent}
-
  class="container"
-
  style:min-width={popoverContainerMinWidth}>
+
<div data-popover-id={id} bind:this={containerEl} class="container">
  {@render toggle(toggleFn)}

  {#if expanded}
    <div
+
      use:portal
+
      bind:this={floatingEl}
      class="popover"
-
      style:bottom={popoverPositionBottom}
-
      style:left={popoverPositionLeft}
-
      style:right={popoverPositionRight}
-
      style:top={popoverPositionTop}
      style:padding={popoverPadding}>
      {@render popover()}
    </div>
modified src/components/PreviewSwitch.svelte
@@ -14,8 +14,8 @@

<div class="container">
  <Button
-
    flatRight
    variant="ghost"
+
    flatRight
    active={!preview}
    onclick={() => {
      preview = !preview;
@@ -24,8 +24,8 @@
  </Button>

  <Button
-
    flatLeft
    variant="ghost"
+
    flatLeft
    active={preview}
    onclick={() => {
      preview = !preview;
added src/components/RadicleWordmark.svelte
@@ -0,0 +1,30 @@
+
<script lang="ts">
+
</script>
+

+
<style>
+
  svg {
+
    fill: currentColor;
+
  }
+
</style>
+

+
<svg
+
  width="69"
+
  height="16"
+
  viewBox="0 0 69 16"
+
  fill="none"
+
  xmlns="http://www.w3.org/2000/svg">
+
  <path
+
    d="M1.84555 13.7149H0V15.7954H6.28119V13.7149H4.466V7.01257H8.02772V4.61917H1.84555V13.7149Z" />
+
  <path
+
    d="M9.20923 12.8172C9.20923 10.7169 10.627 9.62514 13.2475 9.23834L15.1855 8.96243C15.8785 8.86078 16.2152 8.63636 16.2152 8.06474C16.2152 6.98355 15.4706 6.37233 14.1861 6.37233C12.8198 6.37233 12.0964 7.10633 11.8812 8.14659L9.3518 7.76903C9.73992 5.91293 11.2792 4.41458 14.1848 4.41458C17.0904 4.41458 18.8449 6.0159 18.8449 8.56507V15.7941H16.2653V13.7241H16.1637C15.8072 14.9479 14.594 15.9974 12.7485 15.9974C10.5967 15.9974 9.21055 14.7327 9.21055 12.8159L9.20923 12.8172ZM16.2138 10.9109V10.544L13.7874 10.9928C12.5333 11.2278 11.9828 11.7572 11.9828 12.5624C11.9828 13.4601 12.6455 13.9895 13.6554 13.9895C15.369 13.9895 16.2152 12.7459 16.2152 10.9096L16.2138 10.9109Z" />
+
  <path
+
    d="M20.5122 10.1861C20.5122 6.65872 22.3274 4.41449 25.0191 4.41449C26.5492 4.41449 27.7518 5.28182 28.3234 6.61647H28.4158V0.826372H31.0469V15.7953H28.4158V13.7465H28.3142C27.6211 15.1841 26.5505 16 25.031 16C22.3287 16 20.5135 13.7267 20.5135 10.1874L20.5122 10.1861ZM25.8455 13.7953C27.4152 13.7953 28.4053 12.3775 28.4053 10.1861C28.4053 7.99469 27.4165 6.58611 25.8455 6.58611C24.2746 6.58611 23.2858 7.99337 23.2858 10.1861C23.2858 12.3788 24.2851 13.7953 25.8455 13.7953Z" />
+
  <path
+
    d="M37.3981 13.7149H39.2436V15.7954H32.9624V13.7149H34.7776V6.69967H33.0957V4.61914H37.3981V13.7149ZM34.441 1.54983C34.441 0.693069 35.1551 0 36.0212 0C36.8872 0 37.5816 0.693069 37.5816 1.54983C37.5816 2.4066 36.899 3.08911 36.0212 3.08911C35.1433 3.08911 34.441 2.4066 34.441 1.54983Z" />
+
  <path
+
    d="M50.0937 7.98415L47.5644 8.52409C47.299 7.28052 46.5756 6.54653 45.3413 6.54653C43.7505 6.54653 42.7406 7.8825 42.7406 10.2073C42.7406 12.532 43.7598 13.868 45.3505 13.868C46.4924 13.868 47.3083 13.2462 47.5433 12.0026H50.1532C49.8574 14.5109 48.1954 16 45.3505 16C42.0871 16 39.9868 13.7267 39.9868 10.2086C39.9868 6.69042 42.0871 4.41716 45.34 4.41716C48.0317 4.41716 49.765 5.86534 50.0911 7.98547L50.0937 7.98415Z" />
+
  <path
+
    d="M55.5287 13.7148H57.3742V15.7953H51.093V13.7148H52.9082V2.90558H51.2264V0.826372H55.5287V13.7148Z" />
+
  <path
+
    d="M57.9749 10.2271C57.9749 6.74989 60.1267 4.41458 63.3174 4.41458C66.2442 4.41458 68.5386 6.34197 68.2323 10.808H60.7273C60.8805 12.7855 61.9207 13.938 63.328 13.938C64.5412 13.938 65.3055 13.336 65.6013 12.2958H68.2626C67.9775 14.1822 66.1729 15.9974 63.328 15.9974C60.0963 15.9974 57.9749 13.744 57.9749 10.2258V10.2271ZM65.6013 9.01392C65.5405 7.37167 64.7036 6.38289 63.328 6.38289C62.0739 6.38289 61.0851 7.10633 60.7894 9.01392H65.6026H65.6013Z" />
+
</svg>
modified src/components/ReactionSelector.svelte
@@ -1,27 +1,19 @@
<script lang="ts">
  import type { Reaction } from "@bindings/cob/Reaction";
+
  import type { Placement } from "@floating-ui/dom";

  import { twemoji } from "@app/lib/utils";

-
  import Border from "@app/components/Border.svelte";
  import Icon from "@app/components/Icon.svelte";
  import Popover from "@app/components/Popover.svelte";

  interface Props {
    reactions?: Reaction[];
-
    popoverPositionBottom?: string;
-
    popoverPositionRight?: string;
-
    popoverPositionLeft?: string;
+
    placement?: Placement;
    select: (reaction: Reaction) => Promise<void>;
  }

-
  const {
-
    reactions,
-
    popoverPositionBottom,
-
    popoverPositionRight,
-
    popoverPositionLeft,
-
    select,
-
  }: Props = $props();
+
  const { reactions, placement, select }: Props = $props();

  const availableReactions = ["👍", "👎", "😄", "🎉", "🙁", "🚀", "👀"];
</script>
@@ -30,7 +22,7 @@
  .selector {
    display: flex;
    align-items: center;
-
    gap: 2px;
+
    gap: 1px;
  }

  button {
@@ -38,29 +30,31 @@
    border: 0;
    background: none;
    height: 1.5rem;
-
    clip-path: var(--1px-corner-fill);
+
    border-radius: var(--border-radius-sm);
    margin: 0;
-
    font-size: var(--font-size-small);
+
    font: var(--txt-body-m-regular);
    width: 2rem;
    height: 2rem;
  }

  button:hover,
  button.active {
-
    background-color: var(--color-fill-ghost);
+
    background-color: var(--color-surface-subtle);
  }
</style>

-
<Popover
-
  {popoverPositionBottom}
-
  {popoverPositionRight}
-
  {popoverPositionLeft}
-
  popoverPadding="0">
+
<Popover {placement} popoverPadding="0">
  {#snippet toggle(onclick)}
-
    <Icon name="face" {onclick} />
+
    <Icon name="emoji" {onclick} />
  {/snippet}
  {#snippet popover()}
-
    <Border variant="ghost">
+
    <div
+
      style:border="1px solid var(--color-border-subtle)"
+
      style:border-radius="var(--border-radius-md)"
+
      style:display="flex"
+
      style:gap="0.5rem"
+
      style:align-items="center"
+
      style:background-color="var(--color-surface-canvas)">
      <div class="selector">
        {#each availableReactions as reaction}
          {@const lookedUpReaction = reactions?.find(
@@ -75,6 +69,6 @@
          </button>
        {/each}
      </div>
-
    </Border>
+
    </div>
  {/snippet}
</Popover>
modified src/components/Reactions.svelte
@@ -38,7 +38,7 @@
        <div
          role="button"
          tabindex="0"
-
          class="reaction txt-tiny"
+
          class="reaction txt-body-s-regular"
          onclick={async () => {
            if (handleReaction) {
              await handleReaction(authors, emoji);
@@ -48,7 +48,7 @@
          <span>{authors.length}</span>
        </div>
      {:else}
-
        <div class="reaction txt-tiny" style="padding: 2px 4px;">
+
        <div class="reaction txt-body-s-regular" style="padding: 2px 4px;">
          <span>{@html emojiToTwemoji(emoji, ["21a9"])}</span>
          <span>{authors.length}</span>
        </div>
added src/components/RepoAvatar.svelte
@@ -0,0 +1,37 @@
+
<script lang="ts">
+
  import { cachedRepoAvatar } from "@app/lib/avatar";
+

+
  interface Props {
+
    name: string;
+
    rid: string;
+
    styleWidth: string;
+
  }
+

+
  const { name, rid, styleWidth }: Props = $props();
+

+
  let dataUri: string | undefined = $state(undefined);
+

+
  $effect(() => {
+
    // We strip out characters from the RID that repeat for all RIDs this leads
+
    // to the generated avatars looking nicer visually.
+
    const key = `${name}${rid.replace("rad:z", "")}${name}`;
+

+
    void cachedRepoAvatar(key).then((data: string) => {
+
      dataUri = data;
+
    });
+
  });
+
</script>
+

+
{#if dataUri}
+
  <img
+
    style:width={styleWidth}
+
    style:height={styleWidth}
+
    src={dataUri}
+
    alt="Repository Avatar" />
+
{:else}
+
  <div
+
    style:width={styleWidth}
+
    style:height={styleWidth}
+
    style:background-color="var(--color-surface-subtle)">
+
  </div>
+
{/if}
deleted src/components/RepoCard.svelte
@@ -1,85 +0,0 @@
-
<script lang="ts">
-
  import type { RepoInfo } from "@bindings/repo/RepoInfo";
-

-
  import * as router from "@app/lib/router";
-
  import { formatRepositoryId, formatTimestamp } from "@app/lib/utils";
-

-
  import Border from "@app/components/Border.svelte";
-
  import Icon from "@app/components/Icon.svelte";
-
  import Id from "@app/components/Id.svelte";
-
  import RepoHeader from "@app/components/RepoHeader.svelte";
-

-
  interface Props {
-
    repo: RepoInfo;
-
    selfDid: string;
-
    focussed?: boolean;
-
  }
-

-
  const { repo, selfDid, focussed }: Props = $props();
-

-
  const project = $derived(repo.payloads["xyz.radicle.project"]!);
-
</script>
-

-
<style>
-
  .footer {
-
    margin-top: 1rem;
-
    justify-content: space-between;
-
  }
-
  .description {
-
    color: var(--color-fill-gray);
-
    margin-top: 0.25rem;
-
  }
-
  .content {
-
    width: 100%;
-
    padding: 0.5rem 0.75rem;
-
    cursor: pointer;
-
  }
-
</style>
-

-
<Border
-
  variant={focussed ? "secondary" : "ghost"}
-
  styleWidth="100%"
-
  styleOverflow="hidden"
-
  hoverable>
-
  <!-- svelte-ignore a11y_click_events_have_key_events -->
-
  <!-- svelte-ignore a11y_no_static_element_interactions -->
-
  <div
-
    class="content txt-small"
-
    onclick={event => {
-
      if (!event.defaultPrevented)
-
        void router.push({
-
          resource: "repo.home",
-
          rid: repo.rid,
-
        });
-
    }}>
-
    <RepoHeader {repo} {selfDid} />
-

-
    <div class="description txt-overflow" title={project.data.description}>
-
      {#if project.data.description}
-
        {project.data.description}
-
      {:else}
-
        No description.
-
      {/if}
-
    </div>
-
    <Id
-
      ariaLabel="repo-id"
-
      clipboard={repo.rid}
-
      shorten={false}
-
      variant="oid"
-
      id={formatRepositoryId(repo.rid)} />
-

-
    <div class="global-flex footer">
-
      <div class="global-flex">
-
        <div class="global-flex" style:gap="0.25rem">
-
          <Icon name="issue" />{project.meta.issues.open}
-
        </div>
-
        <div class="global-flex" style:gap="0.25rem">
-
          <Icon name="patch" />{project.meta.patches.open}
-
        </div>
-
      </div>
-
      <span style:color="var(--color-fill-gray)">
-
        Updated {formatTimestamp(repo.lastCommitTimestamp)}
-
      </span>
-
    </div>
-
  </div>
-
</Border>
deleted src/components/RepoCardPlaceholder.svelte
@@ -1,119 +0,0 @@
-
<script lang="ts">
-
  import { nodeRunning } from "@app/lib/events";
-
  import { invoke } from "@app/lib/invoke";
-
  import { formatRepositoryId } from "@app/lib/utils";
-

-
  import { announce } from "@app/components/AnnounceSwitch.svelte";
-
  import Border from "@app/components/Border.svelte";
-
  import Icon from "@app/components/Icon.svelte";
-
  import Id from "@app/components/Id.svelte";
-
  import NakedButton from "@app/components/NakedButton.svelte";
-

-
  interface Props {
-
    reload: () => Promise<void>;
-
    rid: string;
-
  }
-

-
  const { reload, rid }: Props = $props();
-

-
  async function unseed() {
-
    try {
-
      await invoke<null>("unseed", {
-
        rid: rid,
-
        opts: { announce: $nodeRunning && $announce },
-
      });
-
      await reload();
-
    } catch (error) {
-
      console.error("Seeding failed", error);
-
    }
-
  }
-
</script>
-

-
<style>
-
  .unseed {
-
    display: none;
-
    color: var(--color-fill-gray);
-
    height: 1.375rem;
-
  }
-
  .container:hover .unseed {
-
    display: flex;
-
  }
-
  .header {
-
    display: flex;
-
    align-items: center;
-
    justify-content: space-between;
-
    width: 100%;
-
    gap: 0.5rem;
-
  }
-
  .footer {
-
    margin-top: 1rem;
-
  }
-
</style>
-

-
<div class="container">
-
  <Border
-
    variant="float"
-
    styleWidth="100%"
-
    styleHeight="8.375rem"
-
    styleAlignItems="flex-start"
-
    styleFlexDirection="column"
-
    styleGap="0"
-
    stylePadding="0.5rem 0.75rem"
-
    styleOverflow="hidden">
-
    <div class="header txt-small">
-
      <div class="global-flex txt-overflow">
-
        <div
-
          class="global-counter"
-
          style:background-color="var(--color-fill-ghost)">
-
        </div>
-
        <span class="global-flex" style:height="1.375rem">
-
          <div
-
            style:height="1rem"
-
            style:width="7rem"
-
            style:background-color="var(--color-fill-ghost)">
-
          </div>
-
        </span>
-
      </div>
-
      <div class="global-flex">
-
        <div class="global-flex unseed">
-
          <NakedButton
-
            stylePadding="0 0.25rem"
-
            variant="ghost"
-
            onclick={unseed}>
-
            <Icon name="broom" />
-
            Remove
-
          </NakedButton>
-
        </div>
-
      </div>
-
    </div>
-
    <div class="global-flex" style:height="1.375rem" style:margin-top="0.25rem">
-
      <div
-
        style:height="0.875rem"
-
        style:width="13rem"
-
        style:background-color="var(--color-fill-ghost)">
-
      </div>
-
    </div>
-
    <Id
-
      ariaLabel="repo-id"
-
      clipboard={rid}
-
      shorten={false}
-
      variant="oid"
-
      id={formatRepositoryId(rid)} />
-

-
    <div
-
      class="global-flex footer txt-small"
-
      style:margin-top="auto"
-
      style:width="100%">
-
      <span
-
        title={$nodeRunning
-
          ? "This may take a while depending on your network connectivity and repo size."
-
          : "Your node is offline. Start your node to fetch this repo."}
-
        class="global-flex"
-
        style:color="var(--color-fill-gray)"
-
        style:margin-left="auto">
-
        <Icon name="hourglass" />
-
        Queued for fetching…
-
      </span>
-
    </div>
-
  </Border>
-
</div>
modified src/components/RepoHeader.svelte
@@ -1,85 +1,169 @@
<script lang="ts">
  import type { RepoInfo } from "@bindings/repo/RepoInfo";

-
  import * as router from "@app/lib/router";
+
  import debounce from "lodash/debounce";

+
  import { writeToClipboard } from "@app/lib/invoke";
+
  import { explorerUrl, truncateDid } from "@app/lib/utils";
+

+
  import Button from "@app/components/Button.svelte";
+
  import CheckoutRepoButton from "@app/components/CheckoutRepoButton.svelte";
+
  import HoverPopover from "@app/components/HoverPopover.svelte";
  import Icon from "@app/components/Icon.svelte";
+
  import Id from "@app/components/Id.svelte";
+
  import JobCob from "@app/components/JobCob.svelte";
+
  import UserAvatar from "@app/components/UserAvatar.svelte";
+
  import VisibilityBadge from "@app/components/VisibilityBadge.svelte";

  interface Props {
    repo: RepoInfo;
-
    selfDid: string;
  }

-
  const { repo, selfDid }: Props = $props();
+
  const { repo }: Props = $props();

  const project = $derived(repo.payloads["xyz.radicle.project"]!);
+

+
  let copyIcon: "link" | "checkmark" = $state("link");
+
  const restoreCopyIcon = debounce(() => {
+
    copyIcon = "link";
+
  }, 1000);
+

+
  async function copyLink() {
+
    await writeToClipboard(explorerUrl(repo.rid));
+
    copyIcon = "checkmark";
+
    restoreCopyIcon();
+
  }
</script>

<style>
  .header {
    display: flex;
    align-items: center;
-
    width: 100%;
+
    flex-direction: row;
+
    gap: 1rem;
+
    padding: 0.625rem 1rem;
+
    border-bottom: 1px solid var(--color-border-subtle);
+
    flex-shrink: 0;
+
  }
+
  .name {
+
    font: var(--txt-body-l-semibold);
+
    color: var(--color-text-primary);
+
  }
+
  .description {
+
    font: var(--txt-body-m-regular);
+
    color: var(--color-text-secondary);
+
  }
+
  .meta {
+
    display: flex;
+
    align-items: center;
+
    gap: 1rem;
+
    margin-left: auto;
+
  }
+
  .meta-item {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.375rem;
+
    font: var(--txt-body-m-regular);
+
  }
+
  .meta-label {
+
    color: var(--color-text-secondary);
+
  }
+
  .meta-value {
+
    color: var(--color-text-primary);
+
  }
+
  .avatars {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.25rem;
+
  }
+
  .avatar-wrap {
+
    width: 1.25rem;
+
    height: 1.25rem;
+
    overflow: hidden;
+
    flex-shrink: 0;
+
  }
+
  .actions {
+
    display: flex;
+
    align-items: center;
    gap: 0.5rem;
  }
+
  a {
+
    color: inherit;
+
    display: inline-flex;
+
    align-items: center;
+
    gap: 0.25rem;
+
    text-decoration: none;
+
    color: var(--color-text-secondary);
+
  }
+
  a:hover {
+
    color: var(--color-text-primary);
+
  }
</style>

-
<div class="header txt-small">
-
  <div class="global-counter" style:background-color="var(--color-fill-ghost)">
-
    {project.data.name[0]}
+
<div class="header">
+
  <div class="txt-selectable">
+
    <div class="name txt-overflow">{project.data.name}</div>
+
    {#if project.data.description}
+
      <div class="description txt-overflow">{project.data.description}</div>
+
    {/if}
  </div>
-
  <div
-
    role="button"
-
    style:min-width="0"
-
    style:margin-right="auto"
-
    tabindex="0"
-
    onclick={event => {
-
      event.preventDefault();
-
      void router.push({
-
        resource: "repo.home",
-
        rid: repo.rid,
-
      });
-
    }}
-
    onkeypress={event => {
-
      if (event.key === "Enter" || event.key === " ") {
-
        event.preventDefault();
-
        void router.push({
-
          resource: "repo.home",
-
          rid: repo.rid,
-
        });
-
      }
-
    }}
-
    title={project.data.name}>
-
    <div class="txt-regular txt-overflow txt-semibold" style:max-width="100%">
-
      {project.data.name}
-
    </div>
-
  </div>
-
  {#if repo.visibility.type === "private"}
-
    <div
-
      class="global-counter"
-
      style:padding="0"
-
      style:background-color="var(--color-fill-private)">
-
      <div style:color="var(--color-foreground-yellow)">
-
        <Icon name="lock" />
-
      </div>
+

+
  <div class="meta">
+
    <VisibilityBadge type={repo.visibility.type} />
+

+
    <div class="meta-item">
+
      <span class="meta-label">{project.data.defaultBranch}</span>
+
      <Icon name="arrow-right" />
+
      <Id
+
        id={project.meta.head}
+
        clipboard={project.meta.head}
+
        placement="bottom-start" />
+
      <JobCob rid={repo.rid} commit={project.meta.head} />
    </div>
-
  {/if}
-
  {#if repo.delegates.find(x => x.did === selfDid)}
-
    <div
-
      class="global-counter"
-
      style:padding="0"
-
      style:background-color="var(--color-fill-delegate)">
-
      <div style:color="var(--color-fill-primary)">
-
        <Icon name="delegate" />
+

+
    <div class="meta-item">
+
      <span class="meta-label">Delegates</span>
+
      <span class="meta-value">{repo.threshold}/{repo.delegates.length}</span>
+
      <div class="avatars">
+
        {#each repo.delegates as delegate}
+
          <HoverPopover placement="bottom-start" stylePadding="0.25rem 0.5rem">
+
            {#snippet toggle()}
+
              <div class="avatar-wrap">
+
                <UserAvatar nodeId={delegate.did} styleWidth="1.25rem" />
+
              </div>
+
            {/snippet}
+
            {#snippet popover()}
+
              <a
+
                class="global-flex txt-body-m-regular"
+
                style:white-space="nowrap"
+
                style:text-decoration="none"
+
                style:width="100%"
+
                href={explorerUrl(`users/${delegate.did}`)}
+
                target="_blank">
+
                {#if delegate.alias}
+
                  <span class="txt-overflow alias">
+
                    {delegate.alias}
+
                  </span>
+
                {:else}
+
                  <span class="no-alias">
+
                    {truncateDid(delegate.did)}
+
                  </span>
+
                {/if}
+
                <span style:margin-left="auto">
+
                  <Icon name="open-external" />
+
                </span>
+
              </a>
+
            {/snippet}
+
          </HoverPopover>
+
        {/each}
      </div>
    </div>
-
  {/if}
-
  <div
-
    class="global-counter"
-
    style:padding="0 0.375rem"
-
    style:background-color="var(--color-fill-ghost)"
-
    style:gap="0.25rem">
-
    <Icon name="seedling-filled" />
-
    {repo.seeding}
+
  </div>
+

+
  <div class="actions">
+
    <Button variant="ghost" styleHeight="2rem" onclick={copyLink}>
+
      <Icon name={copyIcon} />Copy Link
+
    </Button>
+
    <CheckoutRepoButton rid={repo.rid} />
  </div>
</div>
deleted src/components/RepoHomeSecondColumn.svelte
@@ -1,144 +0,0 @@
-
<script lang="ts">
-
  import type { RepoInfo } from "@bindings/repo/RepoInfo";
-
  import type { Tree } from "@bindings/source/Tree";
-

-
  import { useOverlayScrollbars } from "overlayscrollbars-svelte";
-

-
  import Border from "@app/components/Border.svelte";
-
  import Icon from "@app/components/Icon.svelte";
-
  import Link from "@app/components/Link.svelte";
-
  import RepoTeaser from "@app/components/RepoTeaser.svelte";
-
  import Settings from "@app/components/Settings.svelte";
-
  import TreeComponent from "@app/components/Tree.svelte";
-

-
  interface Props {
-
    repo: RepoInfo;
-
    tree: Tree;
-
    fetchTree: (path: string) => Promise<Tree>;
-
    fetchBlob: (path: string) => Promise<void>;
-
  }
-

-
  const { repo, tree, fetchTree, fetchBlob }: Props = $props();
-

-
  let innerElement: HTMLElement | undefined = $state();
-

-
  $effect(() => {
-
    if (innerElement) {
-
      const [initialize] = useOverlayScrollbars({
-
        options: () => ({
-
          scrollbars: {
-
            theme: "global-os-theme-radicle",
-
            autoHide: "scroll",
-
          },
-
        }),
-
        defer: true,
-
      });
-

-
      initialize({ target: innerElement });
-
    }
-
  });
-

-
  const project = $derived(repo.payloads["xyz.radicle.project"]!);
-
</script>
-

-
<style>
-
  .container {
-
    display: flex;
-
    flex-direction: column;
-
    height: 100%;
-
    justify-content: space-between;
-
  }
-
  .tab {
-
    align-items: center;
-
    background-color: var(--color-background-float);
-
    clip-path: var(--1px-corner-fill);
-
    display: flex;
-
    font-size: var(--font-size-small);
-
    justify-content: space-between;
-
    padding: 0.5rem 0.25rem 0.5rem 0.5rem;
-
    width: 100%;
-
  }
-
  .tab:not(.active) {
-
    color: var(--color-foreground-dim);
-
  }
-
  .tab:not(.active):hover {
-
    background-color: var(--color-fill-float-hover);
-
  }
-
  .active {
-
    background-color: var(--color-background-default);
-
    font-weight: var(--font-weight-semibold);
-
    padding: 0.25rem 0.25rem 0.25rem 0.5rem;
-
  }
-
</style>
-

-
<div class="container">
-
  <div>
-
    <div style:margin-bottom="0.75rem">
-
      <Border
-
        variant="ghost"
-
        styleMaxWidth="20rem"
-
        flatBottom={tree.entries.length > 0}
-
        styleBackgroundColor="var(--color-background-default)">
-
        <div class="tab active" style:color="var(--color-foreground-contrast)">
-
          <RepoTeaser name={project.data.name} seeding={repo.seeding} />
-
        </div>
-
      </Border>
-
      {#if tree.entries.length > 0}
-
        <Border
-
          bind:innerElement
-
          variant="ghost"
-
          styleMaxHeight="calc(100vh - 20rem)"
-
          styleOverflow="scroll"
-
          styleMaxWidth="20rem"
-
          flatTop
-
          styleWidth="100%">
-
          <TreeComponent {tree} {fetchTree} {fetchBlob} />
-
        </Border>
-
      {/if}
-
    </div>
-

-
    <div style:margin-bottom="0.5rem">
-
      <Link
-
        styleWidth="100%"
-
        underline={false}
-
        route={{ resource: "repo.issues", rid: repo.rid, status: "open" }}>
-
        <div
-
          class="tab"
-
          style:color="var(--color-foreground-contrast)"
-
          style:padding-left="0.75rem">
-
          <div class="global-flex"><Icon name="issue" />Issues</div>
-
          <div class="global-counter">
-
            {project.meta.issues.open + project.meta.issues.closed}
-
          </div>
-
        </div>
-
      </Link>
-
    </div>
-

-
    <div style:margin-top="0.5rem">
-
      <Link
-
        styleWidth="100%"
-
        underline={false}
-
        route={{ resource: "repo.patches", rid: repo.rid, status: "open" }}>
-
        <div
-
          class="tab"
-
          style:color="var(--color-foreground-contrast)"
-
          style:padding-left="0.75rem">
-
          <div class="global-flex"><Icon name="patch" />Patches</div>
-
          <div class="global-counter">
-
            {project.meta.patches.draft +
-
              project.meta.patches.open +
-
              project.meta.patches.archived +
-
              project.meta.patches.merged}
-
          </div>
-
        </div>
-
      </Link>
-
    </div>
-
  </div>
-

-
  <Settings
-
    compact={false}
-
    popoverProps={{
-
      popoverPositionBottom: "3rem",
-
      popoverPositionLeft: "0",
-
    }} />
-
</div>
deleted src/components/RepoMetadata.svelte
@@ -1,134 +0,0 @@
-
<script lang="ts">
-
  import type { RepoInfo } from "@bindings/repo/RepoInfo";
-

-
  import sortBy from "lodash/sortBy.js";
-

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

-
  import Border from "@app/components/Border.svelte";
-
  import Icon from "@app/components/Icon.svelte";
-
  import Id from "@app/components/Id.svelte";
-
  import NodeId from "@app/components/NodeId.svelte";
-
  import VisibilityBadge from "@app/components/VisibilityBadge.svelte";
-

-
  import JobCob from "./JobCob.svelte";
-

-
  interface Props {
-
    horizontal?: boolean;
-
    repo: RepoInfo;
-
  }
-

-
  const { horizontal = false, repo }: Props = $props();
-

-
  const project = $derived(repo.payloads["xyz.radicle.project"]!);
-
</script>
-

-
<style>
-
  .list {
-
    flex-direction: column;
-
    align-items: flex-start;
-
  }
-
  .horizontal {
-
    flex-direction: row;
-
    flex-wrap: wrap;
-
  }
-
  .metadata-divider {
-
    width: 2px;
-
    background-color: var(--color-fill-ghost);
-
    height: calc(100% + 4px);
-
    top: 0;
-
    position: relative;
-
  }
-
  .metadata-divider-horizontal {
-
    height: 2px;
-
    background-color: var(--color-fill-ghost);
-
    width: calc(100% + 4px);
-
    top: 0;
-
    left: -2px;
-
    position: relative;
-
  }
-
  .metadata-section {
-
    padding: 0.5rem;
-
    font-size: var(--font-size-small);
-
    display: flex;
-
    flex-direction: column;
-
    align-items: flex-start;
-
    flex: 1;
-
    white-space: nowrap;
-
  }
-
  .metadata-section-title {
-
    margin-bottom: 0.5rem;
-
    color: var(--color-foreground-dim);
-
  }
-
</style>
-

-
<Border
-
  variant="ghost"
-
  styleGap="0"
-
  styleFlexDirection="column"
-
  styleAlignItems="flex-start">
-
  <div class="metadata-section" style:flex="1" style:width="100%">
-
    <div class="metadata-section-title">Visibility</div>
-
    <VisibilityBadge type={repo.visibility.type} />
-
  </div>
-

-
  {#if repo.visibility.type === "private"}
-
    <div class="metadata-divider-horizontal"></div>
-

-
    <div class="metadata-section" style:flex="1" style:width="100%">
-
      <div class="metadata-section-title">Allow</div>
-
      {#if repo.visibility.allow}
-
        <div class="global-flex list" class:horizontal>
-
          {#each repo.visibility.allow as node}
-
            <NodeId {...authorForNodeId(node)} />
-
          {/each}
-
        </div>
-
      {:else}
-
        <div class="global-flex txt-missing" style:gap="0.25rem">
-
          <Icon name="none" /> None
-
        </div>
-
      {/if}
-
    </div>
-
  {/if}
-

-
  <div class="metadata-divider-horizontal"></div>
-

-
  <div class="metadata-section" style:flex="1" style:width="100%">
-
    <div class="metadata-section-title">Delegates</div>
-
    <div class="global-flex list" class:horizontal>
-
      {#each sortBy(repo.delegates, d => {
-
        return d.alias?.toLowerCase();
-
      }) as delegate}
-
        <NodeId {...authorForNodeId(delegate)} />
-
      {/each}
-
    </div>
-
  </div>
-

-
  <div class="metadata-divider-horizontal"></div>
-

-
  <div
-
    class="global-flex"
-
    style:gap="0"
-
    style:flex-direction="row"
-
    style:width="100%">
-
    <div class="metadata-section">
-
      <div class="metadata-section-title">Default branch</div>
-
      <span class="global-flex">
-
        <span class="txt-selectable">{project.data.defaultBranch}</span>
-
        ->
-
        <Id
-
          id={project.meta.head}
-
          clipboard={project.meta.head}
-
          variant="commit" />
-
        <JobCob rid={repo.rid} commit={project.meta.head} />
-
      </span>
-
    </div>
-

-
    <div class="metadata-divider"></div>
-

-
    <div class="metadata-section">
-
      <div class="metadata-section-title">Threshold</div>
-
      {repo.threshold}
-
    </div>
-
  </div>
-
</Border>
deleted src/components/RepoTeaser.svelte
@@ -1,35 +0,0 @@
-
<script lang="ts">
-
  import Icon from "@app/components/Icon.svelte";
-

-
  interface Props {
-
    name: string;
-
    seeding: number;
-
  }
-

-
  const { name, seeding }: Props = $props();
-
</script>
-

-
<style>
-
  .teaser {
-
    align-items: center;
-
    width: 100%;
-
  }
-

-
  .seeding {
-
    margin-left: auto;
-
    padding: 0 0.375rem;
-
    background-color: var(--color-fill-ghost);
-
    gap: 0.25rem;
-
  }
-
</style>
-

-
<div class="global-flex teaser">
-
  <Icon name="repo" />
-
  <span title={name} class="txt-small txt-overflow txt-semibold">
-
    {name}
-
  </span>
-
  <div class="global-counter txt-small seeding">
-
    <Icon name="seedling-filled" />
-
    {seeding}
-
  </div>
-
</div>
modified src/components/Review.svelte
@@ -9,27 +9,23 @@
  import type { RepoInfo } from "@bindings/repo/RepoInfo";

  import partial from "lodash/partial";
-
  import uniqBy from "lodash/uniqBy";

  import type { DraftReview } from "@app/lib/draftReviewStorage";
  import { draftReviewStorage } from "@app/lib/draftReviewStorage";
  import { nodeRunning } from "@app/lib/events";
  import { invoke } from "@app/lib/invoke";
  import * as roles from "@app/lib/roles";
-
  import { push } from "@app/lib/router";
-
  import { authorForNodeId, publicKeyFromDid } from "@app/lib/utils";
+
  import * as router from "@app/lib/router";
+
  import { formatOid, publicKeyFromDid, truncateId } from "@app/lib/utils";

  import { announce } from "@app/components/AnnounceSwitch.svelte";
-
  import Border from "@app/components/Border.svelte";
  import Button from "@app/components/Button.svelte";
  import Changes from "@app/components/Changes.svelte";
  import CommentComponent from "@app/components/Comment.svelte";
  import Discussion from "@app/components/Discussion.svelte";
  import Icon from "@app/components/Icon.svelte";
  import Id from "@app/components/Id.svelte";
-
  import LabelInput from "@app/components/LabelInput.svelte";
-
  import NakedButton from "@app/components/NakedButton.svelte";
-
  import NodeId from "@app/components/NodeId.svelte";
+
  import ScrollArea from "@app/components/ScrollArea.svelte";
  import VerdictBadge from "@app/components/VerdictBadge.svelte";
  import VerdictButton from "@app/components/VerdictButton.svelte";

@@ -53,18 +49,6 @@
    revision,
  }: Props = $props();

-
  const contributors = $derived(
-
    uniqBy(
-
      [
-
        review.author,
-
        ...review.comments.map(c => {
-
          return c.author;
-
        }),
-
      ],
-
      "did",
-
    ),
-
  );
-

  let publishingInProgress = $state(false);
  const canPublish = $derived(review.verdict || review.summary);

@@ -113,7 +97,6 @@
  );

  let verdict: Review["verdict"] = $state(review.verdict);
-
  let labelSaveInProgress: boolean = $state(false);

  async function editReview(
    reviewId: string,
@@ -128,7 +111,6 @@
    }

    try {
-
      labelSaveInProgress = true;
      if ("draft" in review) {
        draftReviewStorage.update(review.id, {
          verdict,
@@ -152,7 +134,6 @@
    } catch (error) {
      console.error("Editing review failed: ", error);
    } finally {
-
      labelSaveInProgress = false;
      await loadReview();
    }
  }
@@ -285,130 +266,86 @@
</script>

<style>
-
  .content {
-
    padding: 1rem 1rem 1rem 0;
+
  .page {
+
    display: flex;
+
    flex-direction: column;
+
    height: 100%;
  }
-
  .title {
-
    font-size: var(--font-size-medium);
-
    font-weight: var(--font-weight-medium);
-
    -webkit-user-select: text;
-
    user-select: text;
+
  .topbar {
    display: flex;
    align-items: center;
-
    white-space: nowrap;
-
    min-height: 2.5rem;
-
    gap: 0.75rem;
-
    margin-bottom: 1rem;
+
    padding: 0 1rem;
+
    height: 2.5rem;
+
    flex-shrink: 0;
+
    gap: 0.375rem;
+
    border-bottom: 1px solid var(--color-border-subtle);
+
    font: var(--txt-body-m-regular);
+
    color: var(--color-text-secondary);
+
  }
+
  .topbar-link {
+
    cursor: pointer;
+
    background: none;
+
    border: none;
+
    padding: 0;
+
    font: var(--txt-body-m-regular);
+
    color: var(--color-text-secondary);
  }
-
  .metadata-divider {
-
    width: 2px;
-
    background-color: var(--color-fill-ghost);
-
    height: calc(100% + 4px);
-
    top: 0;
-
    position: relative;
+
  .topbar-link:hover {
+
    color: var(--color-text-primary);
  }
-
  .metadata-section {
-
    padding: 0.5rem;
-
    font-size: var(--font-size-small);
+
  .title {
    display: flex;
-
    flex-direction: column;
-
    align-items: flex-start;
-
    height: 100%;
+
    align-items: center;
+
    gap: 0.75rem;
+
    margin-bottom: 1rem;
+
    font: var(--txt-heading-s);
  }
-
  .metadata-section-title {
-
    margin-bottom: 0.5rem;
-
    color: var(--color-foreground-dim);
+
  .main {
+
    padding: 1.5rem 2rem;
+
    min-width: 0;
  }
  .review-body {
-
    margin-top: 1rem;
-
    margin-bottom: 1rem;
-
    position: relative;
-
    z-index: 1;
-
  }
-
  /* We put the background and clip-path in a separate element to prevent
-
     popovers being clipped in the main element. */
-
  .review-body::after {
-
    position: absolute;
-
    z-index: -1;
-
    content: " ";
-
    background-color: var(--color-background-float);
-
    clip-path: var(--2px-corner-fill);
-
    width: 100%;
-
    height: 100%;
-
    top: 0;
+
    margin: 1rem 0;
+
    background-color: var(--color-surface-canvas);
+
    border-radius: var(--border-radius-md);
  }
</style>

-
<div class="content">
-
  <div style:margin-bottom="1rem">
-
    <div class="title">
-
      <NakedButton
-
        styleHeight="2.5rem"
-
        variant="ghost"
-
        onclick={onNavigateBack}>
-
        <Icon name="arrow-left" />
-
      </NakedButton>
-
      <span class="global-flex" style:gap="0">
-
        <NodeId
-
          {...authorForNodeId(review.author)}
-
          styleFontSize="var(--font-size-medium)"
-
          styleFontWeight="var(--font-weight-medium)" />'s review
+
<div class="page">
+
  <div class="topbar">
+
    <Icon name="patch" />
+
    <button
+
      class="topbar-link"
+
      onclick={() =>
+
        router.push({
+
          resource: "repo.patches",
+
          rid: repo.rid,
+
          status: undefined,
+
        })}>
+
      All Patches
+
    </button>
+
    <Icon name="chevron-right" />
+
    <button class="topbar-link" onclick={onNavigateBack}>
+
      {formatOid(patchId)}
+
    </button>
+
    <Icon name="chevron-right" />
+
    <span>Review</span>
+
  </div>
+

+
  <ScrollArea style="flex: 1; min-height: 0;">
+
    <div class="main">
+
      <div class="title">
        {#if "draft" in review}
          <span
-
            class="global-counter"
-
            style:margin-left="0.5rem"
+
            class="global-chip"
            title="This review is not yet visible to your peers">
            Draft
          </span>
        {/if}
-
      </span>
-
      {#if "draft" in review}
-
        <div style:margin-inline-start="auto"></div>
-
        <NakedButton
-
          styleHeight="2.5rem"
-
          variant="ghost"
-
          onclick={() => {
-
            draftReviewStorage.delete(review.id);
-
            void push({
-
              resource: "repo.patch",
-
              rid: repo.rid,
-
              patch: patchId,
-
              reviewId: undefined,
-
              status: undefined,
-
            });
-
          }}>
-
          <Icon name="trash" />Delete
-
        </NakedButton>
-
        <Button
-
          styleHeight="2.5rem"
-
          variant="secondary"
-
          title={canPublish
-
            ? undefined
-
            : "Add a summary or select a verdict to publish the review"}
-
          disabled={!canPublish || publishingInProgress}
-
          onclick={async () => {
-
            publishingInProgress = true;
-
            try {
-
              await draftReviewStorage.publish(review.id);
-
              await push({
-
                resource: "repo.patch",
-
                rid: repo.rid,
-
                patch: patchId,
-
                reviewId: undefined,
-
                status: undefined,
-
              });
-
            } finally {
-
              publishingInProgress = false;
-
            }
-
          }}>
-
          <Icon name="checkout" />Publish
-
        </Button>
-
      {/if}
-
    </div>
-

-
    <Border variant="ghost" styleGap="0">
-
      <div class="metadata-section" style:min-width="8rem">
-
        <div class="metadata-section-title">Verdict</div>
+
        <span>
+
          {review.author.alias ??
+
            truncateId(publicKeyFromDid(review.author.did))}'s verdict
+
        </span>
        {#if !!roles.isDelegateOrAuthor( config.publicKey, repo.delegates.map(delegate => delegate.did), review.author.did, )}
          <VerdictButton
            selectedVerdict={verdict}
@@ -427,92 +364,106 @@
        {:else}
          <VerdictBadge {verdict} />
        {/if}
+
        {#if "draft" in review}
+
          <div class="global-flex" style:margin-left="auto" style:gap="0.5rem">
+
            <Button
+
              variant="naked"
+
              styleHeight="2rem"
+
              onclick={() => {
+
                draftReviewStorage.delete(review.id);
+
                void router.push({
+
                  resource: "repo.patch",
+
                  rid: repo.rid,
+
                  patch: patchId,
+
                  reviewId: undefined,
+
                  status: undefined,
+
                });
+
              }}>
+
              <Icon name="trash" />Delete
+
            </Button>
+
            <Button
+
              styleHeight="2rem"
+
              variant="secondary"
+
              title={canPublish
+
                ? undefined
+
                : "Add a summary or select a verdict to publish the review"}
+
              disabled={!canPublish || publishingInProgress}
+
              onclick={async () => {
+
                publishingInProgress = true;
+
                try {
+
                  await draftReviewStorage.publish(review.id);
+
                  await router.push({
+
                    resource: "repo.patch",
+
                    rid: repo.rid,
+
                    patch: patchId,
+
                    reviewId: undefined,
+
                    status: undefined,
+
                  });
+
                } finally {
+
                  publishingInProgress = false;
+
                }
+
              }}>
+
              <Icon name="checkout" />Publish
+
            </Button>
+
          </div>
+
        {/if}
      </div>
-

-
      <div class="metadata-divider"></div>
-

-
      <div class="metadata-section" style:flex="1">
-
        <LabelInput
-
          allowedToEdit={!!roles.isDelegateOrAuthor(
-
            config.publicKey,
-
            repo.delegates.map(delegate => delegate.did),
-
            review.author.did,
-
          )}
-
          labels={review.labels}
-
          submitInProgress={labelSaveInProgress}
-
          save={async labels => {
-
            await editReview(review.id, verdict, labels, review.summary);
-
          }} />
+
      <div class="review-body">
+
        <CommentComponent
+
          disableAttachments
+
          rid={repo.rid}
+
          disallowEmptyBody={!("draft" in review) &&
+
            review.verdict === undefined}
+
          emptyBodyTooltip="Summary is mandatory when verdict is None"
+
          styleWidth="100%"
+
          caption={"draft" in review ? "draft review" : "published review"}
+
          id={"draft" in review ? undefined : review.id}
+
          author={review.author}
+
          timestamp={"draft" in review ? undefined : review.timestamp}
+
          editComment={(publicKeyFromDid(review.author.did) ===
+
            config.publicKey ||
+
            undefined) &&
+
            partial(async (id: string, summary: string) => {
+
              await editReview(id, verdict, review.labels, summary);
+
            }, review.id)}
+
          body={review.summary}>
+
          {#snippet beforeTimestamp()}
+
            on revision <Id id={revision.id} clipboard={revision.id} />
+
          {/snippet}
+
        </CommentComponent>
      </div>

-
      <div class="metadata-divider"></div>
-

-
      <div class="metadata-section" style:flex="1">
-
        <div class="metadata-section-title">Participants</div>
-
        <div class="global-flex">
-
          {#each contributors as contributor}
-
            <NodeId {...authorForNodeId(contributor)} />
-
          {/each}
-
        </div>
-
      </div>
-
    </Border>
+
      {#if !("draft" in review)}
+
        <Discussion
+
          cobId={patchId}
+
          repoDelegates={repo.delegates}
+
          rid={repo.rid}
+
          {commentThreads}
+
          {config}
+
          {createComment}
+
          {editComment}
+
          {reactOnComment} />
+
      {/if}

-
    <div class="review-body">
-
      <CommentComponent
-
        disableAttachments
+
      <Changes
+
        codeComments={{
+
          changeCommentStatus:
+
            "draft" in review ? undefined : changeCommentStatus,
+
          config,
+
          createComment,
+
          editComment,
+
          reactOnComment: "draft" in review ? undefined : reactOnComment,
+
          deleteComment: "draft" in review ? deleteDraftComment : undefined,
+
          repoDelegates: repo.delegates,
+
          canReply: !("draft" in review),
+
          disableAttachments:
+
            "draft" in review ? "Publish your review to attach files" : false,
+
          rid: repo.rid,
+
          threads: codeCommentThreads,
+
        }}
        rid={repo.rid}
-
        disallowEmptyBody={!("draft" in review) && review.verdict === undefined}
-
        emptyBodyTooltip="Summary is mandatory when verdict is None"
-
        styleWidth="100%"
-
        caption={"draft" in review ? "draft review" : "published review"}
-
        id={"draft" in review ? undefined : review.id}
-
        author={review.author}
-
        timestamp={"draft" in review ? undefined : review.timestamp}
-
        editComment={(publicKeyFromDid(review.author.did) ===
-
          config.publicKey ||
-
          undefined) &&
-
          partial(async (id: string, summary: string) => {
-
            await editReview(id, verdict, review.labels, summary);
-
          }, review.id)}
-
        body={review.summary}>
-
        {#snippet beforeTimestamp()}
-
          on revision <Id
-
            id={revision.id}
-
            clipboard={revision.id}
-
            variant="oid" />
-
        {/snippet}
-
      </CommentComponent>
+
        {patchId}
+
        {revision} />
    </div>
-
  </div>
-

-
  {#if !("draft" in review)}
-
    <Discussion
-
      cobId={patchId}
-
      repoDelegates={repo.delegates}
-
      rid={repo.rid}
-
      {commentThreads}
-
      {config}
-
      {createComment}
-
      {editComment}
-
      {reactOnComment} />
-
  {/if}
-

-
  <Changes
-
    codeComments={{
-
      changeCommentStatus: "draft" in review ? undefined : changeCommentStatus,
-
      config,
-
      createComment,
-
      editComment,
-
      reactOnComment: "draft" in review ? undefined : reactOnComment,
-
      deleteComment: "draft" in review ? deleteDraftComment : undefined,
-
      repoDelegates: repo.delegates,
-
      canReply: !("draft" in review),
-
      disableAttachments:
-
        "draft" in review ? "Publish your review to attach files" : false,
-
      rid: repo.rid,
-
      threads: codeCommentThreads,
-
    }}
-
    rid={repo.rid}
-
    {patchId}
-
    {revision} />
+
  </ScrollArea>
</div>
modified src/components/Revision.svelte
@@ -159,24 +159,12 @@
<style>
  .patch-body {
    margin-bottom: 1rem;
-
    position: relative;
-
    z-index: 1;
-
  }
-
  /* We put the background and clip-path in a separate element to prevent
-
     popovers being clipped in the main element. */
-
  .patch-body::after {
-
    position: absolute;
-
    z-index: -1;
-
    content: " ";
-
    background-color: var(--color-background-float);
-
    clip-path: var(--2px-corner-fill);
-
    width: 100%;
-
    height: 100%;
-
    top: 0;
+
    background-color: var(--color-surface-canvas);
+
    border-radius: var(--border-radius-sm);
  }
</style>

-
<div class="txt-small patch-body">
+
<div class="txt-body-m-regular patch-body">
  <CommentComponent
    caption={revision.id === patchId ? "opened patch" : "created revision"}
    {rid}
modified src/components/RevisionBadges.svelte
@@ -14,16 +14,16 @@
{#if revisions.length > 1}
  {#if revision.id === revisions[0].id}
    <span
-
      class="global-counter"
+
      class="global-chip"
      style:height="1.375rem"
-
      style:color="var(--color-foreground-contrast)">
+
      style:color="var(--color-text-primary)">
      Initial
    </span>
  {:else if revision.id === revisions.slice(-1)[0].id}
    <span
-
      class="global-counter"
+
      class="global-chip"
      style:height="1.375rem"
-
      style:color="var(--color-foreground-contrast)">
+
      style:color="var(--color-text-primary)">
      Latest
    </span>
  {/if}
modified src/components/RevisionReviews.svelte
@@ -50,15 +50,15 @@
    padding: 0;
  }
  .accepted {
-
    color: var(--color-foreground-success);
+
    color: var(--color-feedback-success-text);
  }

  .rejected {
-
    color: var(--color-foreground-red);
+
    color: var(--color-feedback-error-text);
  }

  .no-verdict {
-
    color: var(--color-foreground-dim);
+
    color: var(--color-text-secondary);
  }
</style>

@@ -89,14 +89,12 @@
          <Icon name={verdictIcon(review.verdict)} />
        </div>
        <span class="global-flex" style:gap="0">
-
          <NodeId
-
            {...authorForNodeId(review.author)}
-
            styleFontWeight="var(--font-weight-regular)" />'s
+
          <NodeId {...authorForNodeId(review.author)} />'s
        </span>
        review
        {#if "draft" in review}
          <span
-
            class="global-counter"
+
            class="global-chip"
            title="This review is not yet visible to your peers">
            Draft
          </span>
deleted src/components/RevisionSelector.svelte
@@ -1,155 +0,0 @@
-
<script lang="ts">
-
  import type { Patch } from "@bindings/cob/patch/Patch";
-
  import type { Revision } from "@bindings/cob/patch/Revision";
-

-
  import orderBy from "lodash/orderBy";
-
  import uniqBy from "lodash/uniqBy";
-

-
  import { authorForNodeId, formatOid } from "@app/lib/utils";
-

-
  import Border from "@app/components/Border.svelte";
-
  import DropdownList from "@app/components/DropdownList.svelte";
-
  import DropdownListItem from "@app/components/DropdownListItem.svelte";
-
  import Icon from "@app/components/Icon.svelte";
-
  import NakedButton from "@app/components/NakedButton.svelte";
-
  import NodeId from "@app/components/NodeId.svelte";
-
  import Popover, { closeFocused } from "@app/components/Popover.svelte";
-
  import RevisionBadges from "@app/components/RevisionBadges.svelte";
-

-
  interface Props {
-
    patch: Patch;
-
    revisions: Revision[];
-
    selectedRevision: Revision;
-
    selectRevision: (revision: Revision) => void;
-
  }
-

-
  /* eslint-disable prefer-const */
-
  let { patch, revisions, selectedRevision, selectRevision }: Props = $props();
-
  /* eslint-enable prefer-const */
-

-
  const revisionAuthors = $derived(
-
    orderBy(
-
      uniqBy(
-
        revisions.map(r => {
-
          return r.author;
-
        }),
-
        "did",
-
      ),
-
      [
-
        o => {
-
          return o.did === patch.author.did;
-
        },
-
      ],
-
      ["desc"],
-
    ),
-
  );
-

-
  let popoverExpanded: boolean = $state(false);
-
</script>
-

-
<style>
-
  .dropdown-group:not(:last-of-type) {
-
    margin-bottom: 1rem;
-
  }
-
  .icon {
-
    min-width: 1rem;
-
  }
-
</style>
-

-
<Popover
-
  popoverPadding="0"
-
  popoverPositionTop="37px"
-
  popoverPositionLeft="0"
-
  bind:expanded={popoverExpanded}>
-
  {#snippet toggle(onclick)}
-
    <NakedButton
-
      active={popoverExpanded}
-
      variant="ghost"
-
      onclick={(e: MouseEvent) => {
-
        e.stopPropagation();
-
        e.preventDefault();
-
        onclick();
-
      }}
-
      stylePadding="0 0.25rem">
-
      <div style:color="var(--color-foreground-contrast)">
-
        <Icon name="chevron-down" />
-
      </div>
-
    </NakedButton>
-
  {/snippet}
-

-
  {#snippet popover()}
-
    <Border variant="ghost">
-
      <div style:max-width="20rem" style:padding-top="0.5rem">
-
        {#each revisionAuthors as author}
-
          <div class="dropdown-group">
-
            <div style:padding-left="0.5rem" style:padding-bottom="0.5rem">
-
              <NodeId {...authorForNodeId(author)} />
-
            </div>
-
            <DropdownList
-
              items={orderBy(
-
                revisions.filter(r => {
-
                  return r.author.did === author.did;
-
                }),
-
                "timestamp",
-
                ["desc"],
-
              )}>
-
              {#snippet item(revision)}
-
                <DropdownListItem
-
                  selected={revision.id === selectedRevision.id}
-
                  onclick={() => {
-
                    closeFocused();
-
                    selectRevision(revision);
-
                  }}>
-
                  <div class="global-flex txt-overflow">
-
                    <div class="icon">
-
                      {#if patch.state.status === "merged" && patch.state.revision === revision.id}
-
                        <div style:color="var(--color-fill-primary)">
-
                          <Icon name="patch-merged" />
-
                        </div>
-
                      {:else if revision.reviews && revision.reviews.length > 0 && revision.reviews.every( r => {
-
                            return r.verdict === "accept";
-
                          }, )}
-
                        <div style:color="var(--color-fill-success)">
-
                          <Icon name="comment-checkmark" />
-
                        </div>
-
                      {:else if revision.reviews && revision.reviews.length > 0 && revision.reviews.every( r => {
-
                            return r.verdict === "reject";
-
                          }, )}
-
                        <div style:color="var(--color-foreground-red)">
-
                          <Icon name="comment-cross" />
-
                        </div>
-
                      {:else if revision.reviews && revision.reviews.length}
-
                        <div style:color="var(--color-foreground-dim)">
-
                          <Icon name="none" />
-
                        </div>
-
                      {:else if revision?.discussion?.length}
-
                        <div style:color="var(--color-foreground-dim)">
-
                          <Icon name="comment" />
-
                        </div>
-
                      {/if}
-
                    </div>
-
                    <span class="global-oid">
-
                      {formatOid(revision.id)}
-
                    </span>
-
                    <RevisionBadges {revision} {revisions} />
-
                    <span class="txt-overflow">
-
                      {#if revision.description[0].body.trim()}
-
                        {revision.description[0].body}
-
                      {:else}
-
                        <span
-
                          class="txt-missing"
-
                          style:font-weight="var(--font-weight-regular)">
-
                          No description.
-
                        </span>
-
                      {/if}
-
                    </span>
-
                  </div>
-
                </DropdownListItem>
-
              {/snippet}
-
            </DropdownList>
-
          </div>
-
        {/each}
-
      </div>
-
    </Border>
-
  {/snippet}
-
</Popover>
modified src/components/Revisions.svelte
@@ -56,10 +56,8 @@
{#each revisionAuthors as author}
  <div class="author-revisions">
    <div style:padding-bottom="0.5rem">
-
      <span class="global-flex txt-small" style:gap="0">
-
        <NodeId
-
          {...authorForNodeId(author)}
-
          styleFontWeight="var(--font-weight-regular)" />'s revisions
+
      <span class="global-flex txt-body-m-regular" style:gap="0">
+
        <NodeId {...authorForNodeId(author)} />'s revisions
      </span>
    </div>
    {#each orderBy( revisions.filter(r => {
@@ -73,13 +71,13 @@
          }}>
          <div class="global-flex txt-overflow" style:width="100%">
            {#if patch.state.status === "merged" && patch.state.revision === revision.id}
-
              <div style:color="var(--color-fill-primary)">
+
              <div style:color="var(--color-brand-bg)">
                <Icon name="patch-merged" />
              </div>
            {:else}
              <Icon name="revision" />
            {/if}
-
            <span class="global-oid">
+
            <span class="txt-id">
              {revision.id.substring(0, 4)}
            </span>
            <RevisionBadges {revision} {revisions} />
@@ -87,19 +85,12 @@
              {#if revision.description[0].body.trim()}
                {revision.description[0].body.split("\n")[0]}
              {:else}
-
                <span
-
                  class="txt-missing"
-
                  style:font-weight="var(--font-weight-regular)">
-
                  No description.
-
                </span>
+
                <span class="txt-missing">No description.</span>
              {/if}
            </span>
            <div class="global-flex" style:margin-left="auto">
              {#if revision.discussion && revision.discussion.length > 0}
-
                <div
-
                  class="global-flex"
-
                  style:font-weight="var(--font-weight-regular)"
-
                  style:gap="0.25rem">
+
                <div class="global-flex" style:gap="0.25rem">
                  <Icon name="comment" />{revision.discussion.length}
                </div>
              {/if}
added src/components/ScrollArea.svelte
@@ -0,0 +1,37 @@
+
<script lang="ts">
+
  import type { Snippet } from "svelte";
+

+
  import { OverlayScrollbarsComponent } from "overlayscrollbars-svelte";
+

+
  interface Props {
+
    children: Snippet;
+
    style?: string;
+
    onScrollHalf?: () => void;
+
  }
+

+
  const {
+
    children,
+
    style = "height: 100%;",
+
    onScrollHalf = undefined,
+
  }: Props = $props();
+
</script>
+

+
<OverlayScrollbarsComponent
+
  element="div"
+
  {style}
+
  options={{
+
    scrollbars: { theme: "global-os-theme-radicle", autoHide: "scroll" },
+
  }}
+
  events={onScrollHalf
+
    ? {
+
        scroll: instance => {
+
          const el = instance.elements().target;
+
          if (el.scrollTop + el.clientHeight >= el.scrollHeight / 2) {
+
            onScrollHalf();
+
          }
+
        },
+
      }
+
    : undefined}
+
  defer>
+
  {@render children()}
+
</OverlayScrollbarsComponent>
deleted src/components/Settings.svelte
@@ -1,124 +0,0 @@
-
<script lang="ts" module>
-
  export const settingsPopoverToggleId = "settings-popover-toggle";
-
</script>
-

-
<script lang="ts">
-
  import type { ComponentProps } from "svelte";
-

-
  import { updateChecker } from "@app/lib/updateChecker.svelte";
-

-
  import AnnounceSwitch from "@app/components/AnnounceSwitch.svelte";
-
  import Border from "@app/components/Border.svelte";
-
  import CopyableId from "@app/components/CopyableId.svelte";
-
  import ExternalLink from "@app/components/ExternalLink.svelte";
-
  import FontSizeSwitch from "@app/components/FontSizeSwitch.svelte";
-
  import Icon from "@app/components/Icon.svelte";
-
  import NakedButton from "@app/components/NakedButton.svelte";
-
  import OutlineButton from "@app/components/OutlineButton.svelte";
-
  import Popover from "@app/components/Popover.svelte";
-
  import ThemeSwitch from "@app/components/ThemeSwitch.svelte";
-
  import UpdateSwitch from "@app/components/UpdateSwitch.svelte";
-

-
  interface Props {
-
    compact?: boolean;
-
    styleHeight?: ComponentProps<typeof NakedButton>["styleHeight"];
-
    popoverProps: Partial<ComponentProps<typeof Popover>>;
-
  }
-

-
  const {
-
    compact = true,
-
    styleHeight = "2.5rem",
-
    popoverProps,
-
  }: Props = $props();
-

-
  let popoverExpanded: boolean = $state(false);
-
</script>
-

-
<Popover {...popoverProps} bind:expanded={popoverExpanded}>
-
  {#snippet toggle(onclick)}
-
    {#if updateChecker.newVersion}
-
      <OutlineButton
-
        {styleHeight}
-
        id={settingsPopoverToggleId}
-
        title="Settings"
-
        {onclick}
-
        variant="secondary"
-
        active={popoverExpanded}>
-
        <Icon name="settings" />
-
        {#if !compact}
-
          Settings
-
        {/if}
-
      </OutlineButton>
-
    {:else}
-
      <NakedButton
-
        id={settingsPopoverToggleId}
-
        title="Settings"
-
        variant="ghost"
-
        {onclick}
-
        {styleHeight}
-
        active={popoverExpanded}>
-
        <Icon name="settings" />
-
        {#if !compact}
-
          Settings
-
        {/if}
-
      </NakedButton>
-
    {/if}
-
  {/snippet}
-
  {#snippet popover()}
-
    <Border variant="ghost" stylePadding="0.5rem 1rem" styleWidth="27rem">
-
      <div
-
        class="global-flex txt-small"
-
        style:flex-direction="column"
-
        style:align-items="flex-start"
-
        style:gap="1rem"
-
        style:width="100%">
-
        <div
-
          class="global-flex"
-
          style:justify-content="space-between"
-
          style:width="100%"
-
          style:min-height="2rem">
-
          Version
-

-
          <div class="global-flex">
-
            {#if updateChecker.currentVersion}
-
              <CopyableId id={updateChecker.currentVersion} />
-
            {/if}
-
            {#if updateChecker.newVersion}
-
              -> <ExternalLink href="https://radicle.xyz/desktop">
-
                Update to {updateChecker.newVersion}
-
              </ExternalLink>
-
            {/if}
-
          </div>
-
        </div>
-

-
        <div
-
          class="global-flex"
-
          style:justify-content="space-between"
-
          style:width="100%">
-
          Check for updates <UpdateSwitch
-
            active={updateChecker.isEnabled}
-
            disable={updateChecker.disable}
-
            enable={updateChecker.enable} />
-
        </div>
-
        <div
-
          class="global-flex"
-
          style:justify-content="space-between"
-
          style:width="100%">
-
          Theme <ThemeSwitch />
-
        </div>
-
        <div
-
          class="global-flex"
-
          style:justify-content="space-between"
-
          style:width="100%">
-
          Announce changes <AnnounceSwitch />
-
        </div>
-
        <div
-
          class="global-flex"
-
          style:justify-content="space-between"
-
          style:width="100%">
-
          Font size <FontSizeSwitch />
-
        </div>
-
      </div>
-
    </Border>
-
  {/snippet}
-
</Popover>
deleted src/components/Sidebar.svelte
@@ -1,136 +0,0 @@
-
<script lang="ts">
-
  import * as router from "@app/lib/router";
-

-
  import Border from "@app/components/Border.svelte";
-
  import Icon from "@app/components/Icon.svelte";
-
  import NakedButton from "@app/components/NakedButton.svelte";
-
  import Settings from "@app/components/Settings.svelte";
-
  import { getLayout, storeLayout } from "@app/views/repo/Layout.svelte";
-

-
  interface Props {
-
    activeTab: "issues" | "patches";
-
    rid: string;
-
  }
-

-
  const { activeTab, rid }: Props = $props();
-
</script>
-

-
<style>
-
  .sidebar-button {
-
    cursor: pointer;
-
    border: 0;
-
    background: none;
-
    height: 2.5rem;
-
    width: 2.5rem;
-
    clip-path: var(--2px-corner-fill);
-
    margin: 0;
-
    display: flex;
-
    align-items: center;
-
    justify-content: center;
-
    color: var(--color-foreground-contrast);
-
    background-color: var(--color-background-float);
-
  }
-

-
  .sidebar-button:hover {
-
    background-color: var(--color-fill-float-hover);
-
  }
-
</style>
-

-
<div class="global-flex" style:flex-direction="column" style:gap="0.5rem">
-
  <div class="global-flex" style:height="2.5rem">
-
    <button
-
      class="sidebar-button"
-
      onclick={() => {
-
        void router.push({
-
          resource: "repo.home",
-
          rid,
-
        });
-
      }}>
-
      <Icon name="repo" />
-
    </button>
-
  </div>
-
  {#if activeTab === "issues"}
-
    <Border
-
      styleCursor="pointer"
-
      onclick={() => {
-
        void router.push({
-
          resource: "repo.issues",
-
          rid,
-
          status: "open",
-
        });
-
      }}
-
      variant="ghost"
-
      styleWidth="2.5rem"
-
      styleHeight="2.5rem"
-
      styleJustifyContent="center">
-
      <Icon name="issue" />
-
    </Border>
-
  {:else}
-
    <button
-
      class="sidebar-button"
-
      onclick={() => {
-
        void router.push({
-
          resource: "repo.issues",
-
          rid,
-
          status: "open",
-
        });
-
      }}>
-
      <Icon name="issue" />
-
    </button>
-
  {/if}
-

-
  {#if activeTab === "patches"}
-
    <Border
-
      styleCursor="pointer"
-
      onclick={() => {
-
        void router.push({
-
          resource: "repo.patches",
-
          rid,
-
          status: "open",
-
        });
-
      }}
-
      variant="ghost"
-
      styleWidth="2.5rem"
-
      styleHeight="2.5rem"
-
      styleJustifyContent="center">
-
      <Icon name="patch" />
-
    </Border>
-
  {:else}
-
    <button
-
      class="sidebar-button"
-
      onclick={() => {
-
        void router.push({
-
          resource: "repo.patches",
-
          rid,
-
          status: "open",
-
        });
-
      }}>
-
      <Icon name="patch" />
-
    </button>
-
  {/if}
-
</div>
-

-
<div style:z-index="20">
-
  <NakedButton
-
    styleHeight="2.5rem"
-
    variant="ghost"
-
    onclick={() => {
-
      if (getLayout()) {
-
        storeLayout("two-column");
-
      } else {
-
        storeLayout("one-column");
-
      }
-
    }}>
-
    {#if getLayout()}
-
      <Icon name="expand-panel" />
-
    {:else}
-
      <Icon name="collapse-panel" />
-
    {/if}
-
  </NakedButton>
-

-
  <Settings
-
    popoverProps={{
-
      popoverPositionBottom: "3rem",
-
      popoverPositionLeft: "0",
-
    }} />
-
</div>
added src/components/SidebarRepoList.svelte
@@ -0,0 +1,433 @@
+
<script lang="ts">
+
  import type { RepoInfo } from "@bindings/repo/RepoInfo";
+
  import type { RepoSummary } from "@bindings/repo/RepoSummary";
+

+
  import { onMount } from "svelte";
+
  import { boolean } from "zod";
+

+
  import { nodeRunning } from "@app/lib/events";
+
  import { dynamicInterval, resetDynamicInterval } from "@app/lib/interval";
+
  import { invoke } from "@app/lib/invoke";
+
  import * as router from "@app/lib/router";
+
  import useLocalStorage from "@app/lib/useLocalStorage.svelte";
+
  import { formatRepositoryId } from "@app/lib/utils";
+

+
  import AddRepoButton from "@app/components/AddRepoButton.svelte";
+
  import Clipboard from "@app/components/Clipboard.svelte";
+
  import Icon from "@app/components/Icon.svelte";
+
  import RepoAvatar from "@app/components/RepoAvatar.svelte";
+
  import ScrollArea from "@app/components/ScrollArea.svelte";
+

+
  interface Props {
+
    initialRepos: RepoSummary[];
+
    initialSeededNotReplicated: string[];
+
    activeRepo?: RepoInfo;
+
  }
+

+
  const {
+
    initialRepos,
+
    initialSeededNotReplicated,
+
    activeRepo = undefined,
+
  }: Props = $props();
+

+
  let repos = $state<RepoSummary[]>(initialRepos);
+
  let seededNotReplicated = $state<string[]>(initialSeededNotReplicated);
+
  let filterOpen = $state(false);
+
  let filterQuery = $state("");
+
  let filterInputElement: HTMLInputElement | undefined = $state(undefined);
+

+
  $effect(() => {
+
    repos = initialRepos;
+
  });
+

+
  $effect(() => {
+
    seededNotReplicated = initialSeededNotReplicated;
+
  });
+

+
  $effect(() => {
+
    if (filterOpen && filterInputElement) {
+
      filterInputElement.focus({ preventScroll: true });
+
    }
+
  });
+

+
  $effect(() => {
+
    if (seededNotReplicated.length > 0) {
+
      dynamicInterval("seededNotReplicated", reloadRepos, 5_000);
+
    } else {
+
      resetDynamicInterval("seededNotReplicated");
+
    }
+
  });
+

+
  onMount(() => {
+
    return () => resetDynamicInterval("seededNotReplicated");
+
  });
+

+
  const filteredRepos = $derived(
+
    filterQuery.trim()
+
      ? repos.filter(r =>
+
          r.name.toLowerCase().includes(filterQuery.trim().toLowerCase()),
+
        )
+
      : repos,
+
  );
+

+
  const reposExpanded = useLocalStorage(
+
    "sidebarReposExpanded",
+
    boolean(),
+
    true,
+
    !window.localStorage,
+
  );
+

+
  const fetchingExpanded = useLocalStorage(
+
    "sidebarFetchingExpanded",
+
    boolean(),
+
    true,
+
    !window.localStorage,
+
  );
+

+
  async function reloadRepos() {
+
    [repos, seededNotReplicated] = await Promise.all([
+
      invoke<RepoSummary[]>("list_repos_summary"),
+
      invoke<string[]>("seeded_not_replicated"),
+
    ]);
+
  }
+

+
  async function unseed(rid: string) {
+
    try {
+
      await invoke<null>("unseed", { rid });
+
      await reloadRepos();
+
    } catch (error) {
+
      console.error("Unseed failed", error);
+
    }
+
  }
+

+
  const activeRoute = router.activeRouteStore;
+

+
  function activeRid(): string | undefined {
+
    return activeRepo?.rid;
+
  }
+

+
  function isRepoHome(rid: string): boolean {
+
    return $activeRoute.resource === "repo.home" && activeRid() === rid;
+
  }
+

+
  function isIssues(rid: string): boolean {
+
    return (
+
      ($activeRoute.resource === "repo.issues" ||
+
        $activeRoute.resource === "repo.issue") &&
+
      activeRid() === rid
+
    );
+
  }
+

+
  function isPatches(rid: string): boolean {
+
    return (
+
      ($activeRoute.resource === "repo.patches" ||
+
        $activeRoute.resource === "repo.patch") &&
+
      activeRid() === rid
+
    );
+
  }
+
</script>
+

+
<style>
+
  .repos-list {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 0.25rem;
+
    padding: 0.5rem 0;
+
  }
+
  .section-header {
+
    font: var(--txt-body-m-regular);
+
    font-variant-ligatures: none;
+
    color: var(--color-text-secondary);
+
    padding: 0.5rem 0 0.25rem 0.5rem;
+
    display: flex;
+
    align-items: center;
+
    gap: 0.25rem;
+
    justify-content: space-between;
+
    cursor: pointer;
+
    user-select: none;
+
  }
+
  .section-header-label {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.25rem;
+
    flex: 1;
+
    min-width: 0;
+
  }
+
  .section-header-actions {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.25rem;
+
  }
+
  .nav-item {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.5rem;
+
    padding: 0.375rem 0.5rem;
+
    border-radius: var(--border-radius-sm);
+
    font: var(--txt-body-m-regular);
+
    color: var(--color-text-primary);
+
    cursor: pointer;
+
    width: 100%;
+
    text-decoration: none;
+
  }
+
  .nav-item:hover {
+
    background-color: var(--color-surface-subtle);
+
  }
+
  .nav-item.active {
+
    background-color: var(--color-surface-subtle);
+
  }
+
  .nav-item .global-counter-badge {
+
    margin-left: auto;
+
  }
+
  .sub-item {
+
    padding-left: 2rem;
+
  }
+
  .pending-item {
+
    color: var(--color-text-secondary);
+
    cursor: default;
+
  }
+
  .pending-avatar {
+
    width: 1rem;
+
    height: 1rem;
+
    flex-shrink: 0;
+
    border: 1px solid var(--color-border-subtle);
+
  }
+
  .pending-item .remove-icon {
+
    display: none;
+
    margin-left: auto;
+
    color: var(--color-text-tertiary);
+
    border-radius: var(--border-radius-sm);
+
  }
+
  .pending-item:hover .remove-icon {
+
    display: flex;
+
  }
+
  .pending-item .remove-icon:hover {
+
    background-color: var(--color-surface-mid);
+
  }
+
  .nav-item .copy-rid {
+
    visibility: hidden;
+
    margin-left: auto;
+
    color: var(--color-text-tertiary);
+
    border-radius: var(--border-radius-sm);
+
  }
+
  .nav-item:hover .copy-rid {
+
    visibility: visible;
+
  }
+
  .nav-item .copy-rid:hover {
+
    background-color: var(--color-surface-mid);
+
  }
+
  .filter-button {
+
    display: flex;
+
    align-items: center;
+
    justify-content: center;
+
    background: none;
+
    border: 0;
+
    padding: 0.125rem;
+
    margin-left: -0.125rem;
+
    border-radius: var(--border-radius-sm);
+
    color: var(--color-text-secondary);
+
    cursor: pointer;
+
  }
+
  .filter-button:hover {
+
    color: var(--color-text-primary);
+
    background-color: var(--color-surface-subtle);
+
  }
+
  .filter-input {
+
    background: none;
+
    border: 0;
+
    outline: none;
+
    font: var(--txt-body-m-regular);
+
    color: var(--color-text-primary);
+
    flex: 1;
+
    min-width: 0;
+
  }
+
  .filter-input::placeholder {
+
    color: var(--color-text-secondary);
+
  }
+
  .icon {
+
    color: var(--color-text-tertiary);
+
  }
+
</style>
+

+
{#if seededNotReplicated.length > 0}
+
  <div
+
    class="section-header"
+
    onclick={() => (fetchingExpanded.value = !fetchingExpanded.value)}
+
    role="button"
+
    tabindex="0"
+
    onkeydown={e => {
+
      if (e.key === "Enter" || e.key === " ") {
+
        fetchingExpanded.value = !fetchingExpanded.value;
+
      }
+
    }}>
+
    <span class="section-header-label">
+
      <span class="icon"><Icon name="hourglass" /></span>
+
      Fetching {seededNotReplicated.length > 1
+
        ? ` (${seededNotReplicated.length})`
+
        : ""}
+
      <span class="icon">
+
        <Icon name={fetchingExpanded.value ? "chevron-down" : "chevron-up"} />
+
      </span>
+
    </span>
+
  </div>
+

+
  {#if fetchingExpanded.value}
+
    <div style:display="flex" style:flex-direction="column" style:gap="0.25rem">
+
      {#each seededNotReplicated as rid (rid)}
+
        <div
+
          class="nav-item pending-item"
+
          title="{$nodeRunning ? 'Fetching' : 'Queued'} {rid}">
+
          <span class="pending-avatar"></span>
+
          <span class="txt-overflow">{formatRepositoryId(rid)}</span>
+
          <button
+
            class="remove-icon filter-button"
+
            title="Remove"
+
            onclick={() => unseed(rid)}>
+
            <span class="icon"><Icon name="trash" /></span>
+
          </button>
+
        </div>
+
      {/each}
+
    </div>
+
  {/if}
+
{/if}
+

+
<div
+
  class="section-header"
+
  onclick={() => {
+
    if (!filterOpen) {
+
      reposExpanded.value = !reposExpanded.value;
+
    }
+
  }}
+
  role="button"
+
  tabindex="0"
+
  onkeydown={e => {
+
    if (e.key === "Enter" || e.key === " ") {
+
      if (!filterOpen) {
+
        reposExpanded.value = !reposExpanded.value;
+
      }
+
    }
+
  }}>
+
  {#if filterOpen}
+
    <span
+
      class="section-header-label"
+
      onclick={e => e.stopPropagation()}
+
      role="none">
+
      <button
+
        class="filter-button"
+
        title="Clear filter"
+
        onclick={() => {
+
          filterOpen = false;
+
          filterQuery = "";
+
        }}>
+
        <span class="icon"><Icon name="search" /></span>
+
      </button>
+
      <input
+
        bind:this={filterInputElement}
+
        class="filter-input"
+
        placeholder="Filter repos…"
+
        bind:value={filterQuery}
+
        onkeydown={e => {
+
          if (e.key === "Escape") {
+
            filterOpen = false;
+
            filterQuery = "";
+
          } else if (e.key === "Enter" && filteredRepos.length > 0) {
+
            void router.push({
+
              resource: "repo.home",
+
              rid: filteredRepos[0].rid,
+
            });
+
            filterQuery = "";
+
          }
+
        }} />
+
    </span>
+
  {:else}
+
    <span class="section-header-label">
+
      <span onclick={e => e.stopPropagation()} role="none">
+
        <button
+
          class="filter-button"
+
          title="Filter repos"
+
          aria-keyshortcuts="ctrl+f"
+
          onclick={() => {
+
            filterOpen = true;
+
            reposExpanded.value = true;
+
          }}>
+
          <span class="icon"><Icon name="filter" /></span>
+
        </button>
+
      </span>
+
      All Repos{repos.length > 1 ? ` (${repos.length})` : ""}
+
      <span class="icon">
+
        <Icon name={reposExpanded.value ? "chevron-down" : "chevron-up"} />
+
      </span>
+
    </span>
+
  {/if}
+
  <span class="section-header-actions">
+
    <span onclick={e => e.stopPropagation()} role="none">
+
      <AddRepoButton
+
        onOpen={() => (reposExpanded.value = true)}
+
        reload={reloadRepos}
+
        {repos}
+
        {seededNotReplicated} />
+
    </span>
+
  </span>
+
</div>
+

+
{#if reposExpanded.value}
+
  <ScrollArea
+
    style="flex: 1; min-height: 0; mask-image: linear-gradient(to bottom, transparent 0, black 0.5rem, black calc(100% - 0.5rem), transparent 100%);">
+
    <div class="repos-list">
+
      {#each filteredRepos as repo (repo.rid)}
+
        <a
+
          class="nav-item"
+
          class:active={isRepoHome(repo.rid)}
+
          href={router.routeToPath({ resource: "repo.home", rid: repo.rid })}>
+
          <RepoAvatar name={repo.name} rid={repo.rid} styleWidth="1rem" />
+
          <span class="txt-overflow">{repo.name}</span>
+
          <span
+
            class="copy-rid"
+
            role="none"
+
            title="Copy RID"
+
            onclick={e => {
+
              e.preventDefault();
+
              e.stopPropagation();
+
            }}>
+
            <Clipboard text={repo.rid} noPopover />
+
          </span>
+
        </a>
+
        {#if activeRid() === repo.rid}
+
          {@const activeProject = activeRepo?.payloads["xyz.radicle.project"]}
+
          <a
+
            class="nav-item sub-item"
+
            class:active={isIssues(repo.rid)}
+
            href={router.routeToPath({
+
              resource: "repo.issues",
+
              rid: repo.rid,
+
              status: "open",
+
            })}>
+
            <span class="icon"><Icon name="issue" /></span>
+
            Issues
+
            {#if activeProject && activeProject.meta.issues.open > 0}
+
              <span class="global-counter-badge">
+
                {activeProject.meta.issues.open}
+
              </span>
+
            {/if}
+
          </a>
+
          <a
+
            class="nav-item sub-item"
+
            class:active={isPatches(repo.rid)}
+
            href={router.routeToPath({
+
              resource: "repo.patches",
+
              rid: repo.rid,
+
              status: "open",
+
            })}>
+
            <span class="icon"><Icon name="patch" /></span>
+
            Patches
+
            {#if activeProject && activeProject.meta.patches.open > 0}
+
              <span class="global-counter-badge">
+
                {activeProject.meta.patches.open}
+
              </span>
+
            {/if}
+
          </a>
+
        {/if}
+
      {/each}
+
    </div>
+
  </ScrollArea>
+
{/if}
deleted src/components/Source/File.svelte
@@ -1,46 +0,0 @@
-
<script lang="ts">
-
  import Icon from "@app/components/Icon.svelte";
-

-
  const {
-
    name,
-
    fetchBlob,
-
    active,
-
  }: { name: string; fetchBlob: () => Promise<void>; active: boolean } =
-
    $props();
-
</script>
-

-
<style>
-
  .file {
-
    width: 100%;
-
    cursor: pointer;
-
  }
-
  .file:hover,
-
  .active {
-
    background-color: var(--color-background-float);
-
  }
-
  .active:hover {
-
    background-color: var(--color-fill-ghost-hover);
-
  }
-
  .file:hover .icon,
-
  .active .icon {
-
    color: var(--color-foreground-contrast);
-
  }
-
</style>
-

-
<!-- svelte-ignore a11y_no_static_element_interactions -->
-
<!-- svelte-ignore a11y_click_events_have_key_events -->
-
<div
-
  class="file"
-
  class:active
-
  style:padding="0 0.5rem"
-
  style:margin="0.15rem 0"
-
  onclick={fetchBlob}>
-
  <div class="global-flex" style:padding="0.25rem 0">
-
    <div class="icon txt-missing">
-
      <Icon name="file" />
-
    </div>
-
    <div class="txt-small">
-
      {name}
-
    </div>
-
  </div>
-
</div>
deleted src/components/Source/Folder.svelte
@@ -1,69 +0,0 @@
-
<script lang="ts">
-
  import type { Tree } from "@bindings/source/Tree";
-

-
  import Icon from "@app/components/Icon.svelte";
-
  import File from "@app/components/Source/File.svelte";
-
  import Folder from "@app/components/Source/Folder.svelte";
-
  import { getCurrentPath } from "@app/views/repo/RepoHome.svelte";
-

-
  interface Props {
-
    fetchTree: (path: string) => Promise<Tree>;
-
    fetchBlob: (path: string) => Promise<void>;
-
    currentPath: string;
-
    name: string;
-
    prefix: string;
-
  }
-

-
  const { name, fetchBlob, currentPath, prefix, fetchTree }: Props = $props();
-
  let expanded = $derived(getCurrentPath().indexOf(prefix) === 0);
-

-
  const treePromise = $derived(
-
    expanded ? fetchTree(prefix) : Promise.resolve(undefined),
-
  );
-
</script>
-

-
<style>
-
  .folder {
-
    cursor: pointer;
-
  }
-
  .folder:hover {
-
    background-color: var(--color-background-float);
-
  }
-
</style>
-

-
<div class="folder" style:padding-left="0.5rem">
-
  <!-- svelte-ignore a11y_no_static_element_interactions -->
-
  <!-- svelte-ignore a11y_click_events_have_key_events -->
-
  <div
-
    class="global-flex txt-small"
-
    style:padding="0.25rem 0"
-
    onclick={() => (expanded = !expanded)}>
-
    <div class:txt-missing={!expanded}>
-
      <Icon name={expanded ? "folder-open" : "folder-closed"} />
-
    </div>
-
    {name}
-
  </div>
-
</div>
-
{#if expanded}
-
  {#await treePromise then tree}
-
    {#if tree}
-
      {#each tree.entries as entry (entry.path)}
-
        <div style:margin-left="1.5rem">
-
          {#if entry.kind === "tree"}
-
            <Folder
-
              {fetchTree}
-
              {fetchBlob}
-
              name={entry.name}
-
              {currentPath}
-
              prefix={`${entry.path}/`} />
-
          {:else if entry.kind === "blob"}
-
            <File
-
              name={entry.name}
-
              fetchBlob={() => fetchBlob(entry.path)}
-
              active={entry.path === getCurrentPath()} />
-
          {/if}
-
        </div>
-
      {/each}
-
    {/if}
-
  {/await}
-
{/if}
deleted src/components/Tab.svelte
@@ -1,87 +0,0 @@
-
<script lang="ts">
-
  import type { Snippet } from "svelte";
-

-
  interface Props {
-
    children: Snippet;
-
    onclick?: () => void;
-
    disabled?: boolean;
-
    active?: boolean;
-
    flatLeft?: boolean;
-
    flatRight?: boolean;
-
  }
-

-
  const {
-
    children,
-
    onclick = undefined,
-
    disabled = false,
-
    active = false,
-
    flatLeft = false,
-
    flatRight = false,
-
  }: Props = $props();
-
</script>
-

-
<style>
-
  .container {
-
    white-space: nowrap;
-

-
    -webkit-touch-callout: none;
-
    -webkit-user-select: none;
-
    user-select: none;
-
    display: flex;
-
    flex-direction: row;
-
    font-size: var(--font-size-small);
-
    height: 2.375rem;
-
  }
-

-
  .wrapper {
-
    position: relative;
-
    display: flex;
-
    gap: 0.5rem;
-
    padding: 0.1875rem 0;
-
    align-items: center;
-
  }
-

-
  .active {
-
    font-weight: var(--font-weight-semibold);
-
    color: var(--color-foreground-emphasized);
-
  }
-

-
  .wrapper.active::after {
-
    position: absolute;
-
    z-index: 1;
-
    content: " ";
-
    background-color: var(--color-fill-secondary);
-
    height: 2px;
-
    bottom: -4px;
-
    width: 100%;
-
  }
-

-
  .wrapper:hover:not(.active)::after {
-
    position: absolute;
-
    z-index: 1;
-
    content: " ";
-
    background-color: var(--color-foreground-dim);
-
    height: 2px;
-
    bottom: -4px;
-
    width: 100%;
-
  }
-

-
  .container.disabled {
-
    color: var(--color-foreground-disabled);
-
  }
-
</style>
-

-
<!-- svelte-ignore a11y_click_events_have_key_events -->
-
<div
-
  class="container"
-
  style:cursor={!disabled ? "pointer" : "default"}
-
  class:disabled
-
  class:flat-right={flatRight}
-
  class:flat-left={flatLeft}
-
  onclick={!disabled ? onclick : undefined}
-
  role="button"
-
  tabindex="0">
-
  <div class="wrapper" class:active>
-
    {@render children()}
-
  </div>
-
</div>
modified src/components/TextInput.svelte
@@ -4,8 +4,6 @@

  import { onMount } from "svelte";

-
  import Border from "@app/components/Border.svelte";
-

  interface Props {
    autofocus?: boolean;
    autoselect?: boolean;
@@ -75,22 +73,20 @@

<style>
  input {
-
    background: var(--color-background-ghost);
-
    font-family: inherit;
-
    font-size: var(--font-size-small);
-
    color: var(--color-foreground-contrast);
+
    background: transparent;
+
    font: var(--txt-body-m-regular);
+
    color: var(--color-text-primary);
    line-height: 1.6;
    outline: none;
    text-overflow: ellipsis;
    width: 100%;
    height: 100%;
    margin: 0;
-
    height: 2rem;
+
    height: 30px; /* + 2px border = 2rem */
    border: 0;
  }
  input::placeholder {
-
    font-family: var(--font-family-sans-serif);
-
    color: var(--color-foreground-dim);
+
    color: var(--color-text-secondary);
    opacity: 1 !important;
  }
  input[disabled] {
@@ -98,9 +94,14 @@
  }
</style>

-
<Border
-
  variant={valid ? (focussed ? "secondary" : "ghost") : "danger"}
-
  styleWidth="100%">
+
<div
+
  style:border={`1px solid ${!valid ? "var(--color-feedback-error-border)" : focussed ? "var(--color-border-brand)" : "var(--color-border-subtle)"}`}
+
  style:border-radius="var(--border-radius-sm)"
+
  style:display="flex"
+
  style:gap="0.5rem"
+
  style:align-items="center"
+
  style:background-color="var(--color-surface-base)"
+
  style:width="100%">
  {@render left?.()}
  <input
    style:padding={left ? "0.25rem 0.75rem 0.25rem 0" : "0.25rem 0.75rem"}
@@ -127,4 +128,4 @@
    spellcheck="false"
    onkeydown={handleKeydown}
    {oninput} />
-
</Border>
+
</div>
modified src/components/Textarea.svelte
@@ -1,5 +1,4 @@
<script lang="ts">
-
  import type { ComponentProps } from "svelte";
  import type {
    ClipboardEventHandler,
    FormEventHandler,
@@ -9,11 +8,9 @@

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

-
  import Border from "@app/components/Border.svelte";
-

  interface Props {
    draggingOver?: boolean;
-
    borderVariant?: ComponentProps<typeof Border>["variant"];
+
    borderVariant?: "float" | "ghost";
    onpaste?: ClipboardEventHandler<HTMLTextAreaElement>;
    focus?: boolean;
    oninput?: FormEventHandler<HTMLTextAreaElement>;
@@ -50,6 +47,11 @@
  }: Props = $props();
  /* eslint-enable prefer-const */

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

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

@@ -123,7 +125,7 @@
  textarea {
    background-color: transparent;
    border: 0;
-
    color: var(--color-foreground-default);
+
    color: var(--color-text-secondary);
    font-family: inherit;
    height: 100%;
    width: 100%;
@@ -146,7 +148,7 @@
  }

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

  textarea::-webkit-scrollbar {
@@ -159,8 +161,8 @@
  }

  textarea::-webkit-scrollbar-thumb {
-
    background-color: var(--color-fill-ghost);
-
    border-radius: 4px;
+
    background-color: var(--color-surface-subtle);
+
    border-radius: var(--border-radius-md);
  }

  .dragover {
@@ -171,16 +173,20 @@
    align-items: center;
    width: 100%;
    height: 100%;
-
    background-color: var(--color-background-float);
+
    background-color: var(--color-surface-canvas);
  }
</style>

-
<Border
-
  variant={focussed ? "secondary" : borderVariant}
-
  stylePosition="relative"
-
  styleWidth="100%"
-
  {styleAlignItems}
-
  {styleMinHeight}>
+
<div
+
  style:border={`1px solid ${focussed ? "var(--color-border-brand)" : borderColors[borderVariant]}`}
+
  style:border-radius="var(--border-radius-sm)"
+
  style:display="flex"
+
  style:gap="0.5rem"
+
  style:align-items={styleAlignItems}
+
  style:background-color="var(--color-surface-base)"
+
  style:position="relative"
+
  style:width="100%"
+
  style:min-height={styleMinHeight}>
  <textarea
    style:min-height={styleMinHeight}
    style:padding={stylePadding}
@@ -188,7 +194,7 @@
    bind:this={textareaElement}
    bind:value
    aria-label="textarea-comment"
-
    class="txt-small"
+
    class="txt-body-m-regular"
    style:resize={size === "resizable" ? "vertical" : undefined}
    style:overflow={size === "resizable" || size === "fixed-height"
      ? "scroll"
@@ -202,8 +208,8 @@
    onkeydown={handleKeydown}>
  </textarea>
  {#if draggingOver}
-
    <div class="txt-small dragover">
+
    <div class="txt-body-m-regular dragover">
      Drop files to add them as embeds. Embeds are limited to 10Mb.
    </div>
  {/if}
-
</Border>
+
</div>
modified src/components/ThemeSwitch.svelte
@@ -1,36 +1,48 @@
<script lang="ts" module>
-
  type Theme = "dark" | "light";
+
  import { writable } from "svelte/store";

-
  export const theme = writable<Theme>(loadTheme());
+
  type Theme = "dark" | "light";

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

-
    if (storedTheme === null) {
-
      return matches ? "dark" : "light";
-
    } else {
-
      return storedTheme as Theme;
+
  function initTheme(): Theme {
+
    const storedTheme = localStorage ? localStorage.getItem("theme") : null;
+
    if (storedTheme === "dark" || storedTheme === "light") {
+
      return storedTheme;
    }
+
    return loadTheme();
  }

+
  export const followSystemTheme = writable<boolean>(
+
    typeof localStorage !== "undefined"
+
      ? !localStorage.getItem("theme")
+
      : false,
+
  );
+

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

  export function storeTheme(newTheme: Theme): void {
+
    followSystemTheme.set(false);
    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.",
-
      );
+
    }
+
  }
+

+
  export function setSystemTheme(): void {
+
    followSystemTheme.set(true);
+
    theme.set(loadTheme());
+
    if (localStorage) {
+
      localStorage.removeItem("theme");
    }
  }
</script>

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

  import Button from "@app/components/Button.svelte";
-
  import Icon from "@app/components/Icon.svelte";
</script>

<style>
@@ -42,24 +54,25 @@

<div class="container">
  <Button
-
    flatRight
-
    active={$theme === "dark"}
    variant="ghost"
-
    onclick={() => {
-
      storeTheme("dark");
-
    }}>
-
    <Icon name="moon" />
-
    Dark
+
    flatRight
+
    active={$followSystemTheme}
+
    onclick={setSystemTheme}>
+
    System
  </Button>
-

  <Button
-
    flatLeft
    variant="ghost"
-
    active={$theme === "light"}
-
    onclick={() => {
-
      storeTheme("light");
-
    }}>
-
    <Icon name="sun" />
+
    flatLeft
+
    flatRight
+
    active={!$followSystemTheme && $theme === "light"}
+
    onclick={() => storeTheme("light")}>
    Light
  </Button>
+
  <Button
+
    variant="ghost"
+
    flatLeft
+
    active={!$followSystemTheme && $theme === "dark"}
+
    onclick={() => storeTheme("dark")}>
+
    Dark
+
  </Button>
</div>
modified src/components/Thread.svelte
@@ -8,7 +8,6 @@

  import { scrollIntoView } from "@app/lib/utils";

-
  import Border from "@app/components/Border.svelte";
  import CommentComponent from "@app/components/Comment.svelte";
  import ExtendedTextarea from "@app/components/ExtendedTextarea.svelte";
  import Icon from "@app/components/Icon.svelte";
@@ -67,8 +66,8 @@
  const replies = $derived(thread.replies);
  const style = $derived(
    replies.length > 0 || showReplyForm
-
      ? "--local-clip-path: var(--2px-top-corner-fill)"
-
      : "--local-clip-path: var(--2px-corner-fill)",
+
      ? `--local-border-radius: var(--border-radius-sm) var(--border-radius-sm) 0 0`
+
      : `--local-border-radius: var(--border-radius-sm)`,
  );
</script>

@@ -77,24 +76,12 @@
    display: flex;
    flex-direction: column;
    width: 100%;
-
    font-family: var(--font-family-sans-serif);
  }

  .top-level-comment {
-
    position: relative;
-
    z-index: 1;
-
  }
-
  /* We put the background and clip-path in a separate element to prevent
-
     popovers being clipped in the main element. */
-
  .top-level-comment::after {
-
    position: absolute;
-
    z-index: -1;
-
    content: " ";
-
    background-color: var(--color-background-float);
-
    clip-path: var(--local-clip-path);
-
    width: 100%;
-
    height: 100%;
-
    top: 0;
+
    background-color: var(--color-surface-canvas);
+
    border: 1px solid var(--color-border-subtle);
+
    border-radius: var(--local-border-radius);
  }
</style>

@@ -112,9 +99,9 @@
          reactions={reply.reactions}
          timestamp={reply.edits[0].timestamp}
          body={reply.edits.slice(-1)[0].body}
-
          editComment={canEditComment(root.author.did) &&
-
            editComment?.bind(null, root.id)}
-
          reactOnComment={reactOnComment?.bind(null, root.id)} />
+
          editComment={canEditComment(reply.author.did) &&
+
            editComment?.bind(null, reply.id)}
+
          reactOnComment={reactOnComment?.bind(null, reply.id)} />
      {/each}
    {/if}
    {#if createReply && showReplyForm}
@@ -166,14 +153,24 @@
  {#if replies.length > 0 || (createReply && showReplyForm)}
    {#if inline}
      <div
-
        style:background-color="var(--color-background-deafult)"
-
        style:border="2px solid var(--color-border-hint)">
+
        style:background-color="var(--color-surface-canvas)"
+
        style:border="1px solid var(--color-border-subtle)">
        {@render repliesSnippet()}
      </div>
    {:else}
-
      <Border variant="ghost" styleOverflow="hidden" flatTop={!inline}>
+
      <div
+
        style:border="1px solid var(--color-border-subtle)"
+
        style:border-top="none"
+
        style:border-radius="var(--border-radius-sm)"
+
        style:border-top-left-radius={!inline ? "0" : undefined}
+
        style:border-top-right-radius={!inline ? "0" : undefined}
+
        style:display="flex"
+
        style:gap="0.5rem"
+
        style:align-items="center"
+
        style:background-color="var(--color-surface-canvas)"
+
        style:overflow="hidden">
        {@render repliesSnippet()}
-
      </Border>
+
      </div>
    {/if}
  {/if}
</div>
modified src/components/Tree.svelte
@@ -1,34 +1,33 @@
<script lang="ts">
  import type { Tree } from "@bindings/source/Tree";

-
  import File from "@app/components/Source/File.svelte";
-
  import Folder from "@app/components/Source/Folder.svelte";
-
  import { getCurrentPath } from "@app/views/repo/RepoHome.svelte";
+
  import FileTreeFile from "@app/components/FileTreeFile.svelte";
+
  import FileTreeFolder from "@app/components/FileTreeFolder.svelte";

  interface Props {
    tree: Tree;
-
    path?: string;
+
    currentPath: string;
    fetchTree: (path: string) => Promise<Tree>;
    fetchBlob: (path: string) => Promise<void>;
  }

-
  const { path = "", tree, fetchTree, fetchBlob }: Props = $props();
+
  const { currentPath, tree, fetchTree, fetchBlob }: Props = $props();
</script>

-
<div>
+
<div style:display="flex" style:flex-direction="column" style:gap="0.25rem">
  {#each tree.entries as entry (entry.name)}
    {#if entry.kind === "tree"}
-
      <Folder
+
      <FileTreeFolder
        name={entry.name}
        prefix={`${entry.path}/`}
-
        currentPath={path}
+
        {currentPath}
        {fetchTree}
        {fetchBlob} />
    {:else}
-
      <File
+
      <FileTreeFile
        name={entry.name}
        fetchBlob={() => fetchBlob(entry.path)}
-
        active={entry.path === getCurrentPath()} />
+
        active={entry.path === currentPath} />
    {/if}
  {/each}
</div>
modified src/components/UpdateSwitch.svelte
@@ -18,11 +18,11 @@
</style>

<div class="container">
-
  <Button flatRight active={!active} variant="ghost" onclick={disable}>
+
  <Button variant="ghost" flatRight active={!active} onclick={disable}>
    Disable
  </Button>

-
  <Button flatLeft variant="ghost" active={Boolean(active)} onclick={enable}>
+
  <Button variant="ghost" flatLeft active={Boolean(active)} onclick={enable}>
    Enable
  </Button>
</div>
added src/components/UserAvatar.svelte
@@ -0,0 +1,26 @@
+
<script lang="ts">
+
  import { cachedUserAvatar } from "@app/lib/avatar";
+

+
  interface Props {
+
    nodeId: string;
+
    styleWidth: string;
+
  }
+

+
  const { nodeId, styleWidth }: Props = $props();
+

+
  let dataUri: string | undefined = $state(undefined);
+

+
  $effect(() => {
+
    void cachedUserAvatar(nodeId.replace("did:key:", "")).then(data => {
+
      dataUri = data;
+
    });
+
  });
+
</script>
+

+
{#if dataUri}
+
  <img
+
    style:width={styleWidth}
+
    style:height={styleWidth}
+
    src={dataUri}
+
    alt="Avatar" />
+
{/if}
modified src/components/VerdictBadge.svelte
@@ -21,34 +21,36 @@
  .badge {
    gap: 0.375rem;
    padding-right: 0.625rem;
+
    width: fit-content;
+
    font: var(--txt-body-m-regular);
  }
  .no-verdict {
-
    background-color: var(--color-fill-ghost);
-
    color: var(--color-foreground-dim);
+
    background-color: var(--color-surface-subtle);
+
    color: var(--color-text-secondary);
  }
  .no-verdict.hoverable:hover {
-
    background-color: var(--color-fill-ghost-hover);
+
    background-color: var(--color-surface-mid);
  }

  .accepted {
-
    background-color: var(--color-fill-diff-green-light);
-
    color: var(--color-foreground-success);
+
    background-color: var(--color-feedback-success-bg);
+
    color: var(--color-feedback-success-text);
  }
  .accepted.hoverable:hover {
-
    background-color: var(--color-fill-diff-green);
+
    background-color: var(--color-feedback-success-bg);
  }

  .rejected {
-
    background-color: var(--color-fill-diff-red-light);
-
    color: var(--color-foreground-red);
+
    background-color: var(--color-feedback-error-bg);
+
    color: var(--color-feedback-error-text);
  }
  .rejected.hoverable:hover {
-
    background-color: var(--color-fill-diff-red);
+
    background-color: var(--color-feedback-error-bg);
  }
</style>

<span
-
  class="global-counter badge"
+
  class="global-chip badge"
  class:hoverable
  class:no-verdict={verdict === undefined}
  class:accepted={verdict === "accept"}
modified src/components/VerdictButton.svelte
@@ -5,13 +5,12 @@

  import { verdictIcon } from "@app/lib/utils";

-
  import Border from "@app/components/Border.svelte";
+
  import Button from "@app/components/Button.svelte";
  import DropdownList from "@app/components/DropdownList.svelte";
  import DropdownListItem from "@app/components/DropdownListItem.svelte";
  import Icon from "@app/components/Icon.svelte";
  import Popover from "@app/components/Popover.svelte";
  import { closeFocused } from "@app/components/Popover.svelte";
-
  import VerdictBadge from "@app/components/VerdictBadge.svelte";

  interface Props {
    onSelect: (selectedVerdict: Review["verdict"]) => Promise<void>;
@@ -23,47 +22,48 @@
  const { onSelect, draft, summaryMissing, selectedVerdict }: Props = $props();

  let popoverExpanded: boolean = $state(false);
-
</script>
-

-
<style>
-
  button {
-
    cursor: pointer;
-
    border: 0;
-
    background: none;
-
    margin: 0;
-
    padding: 0;
-
    display: flex;
-
    align-items: center;
-
    justify-content: center;
-
    font-size: var(--font-size-small);
-
  }
-
  .accepted {
-
    color: var(--color-foreground-success);
-
  }

-
  .rejected {
-
    color: var(--color-foreground-red);
+
  function verdictBgColor(verdict: Review["verdict"]): string {
+
    if (verdict === "accept") return "var(--color-feedback-success-bg)";
+
    if (verdict === "reject") return "var(--color-feedback-error-bg)";
+
    return "var(--color-surface-subtle)";
  }

-
  .no-verdict {
-
    color: var(--color-foreground-dim);
+
  function verdictColor(verdict: Review["verdict"]): string {
+
    if (verdict === "accept") return "var(--color-feedback-success-text)";
+
    if (verdict === "reject") return "var(--color-feedback-error-text)";
+
    return "var(--color-text-secondary)";
  }
-
</style>
+
</script>

<Popover
  popoverPadding="0"
-
  popoverPositionLeft="0"
-
  popoverPositionTop="2rem"
+
  placement="bottom-start"
  bind:expanded={popoverExpanded}>
  {#snippet toggle(onclick)}
-
    <button {onclick}>
-
      <VerdictBadge verdict={selectedVerdict} hoverable>
-
        <Icon name={popoverExpanded ? "chevron-up" : "chevron-down"} />
-
      </VerdictBadge>
-
    </button>
+
    <Button variant="outline" {onclick} active={popoverExpanded}>
+
      <span
+
        class="global-chip"
+
        style:padding="0"
+
        style:margin-left="-0.25rem"
+
        style:background-color={verdictBgColor(selectedVerdict)}
+
        style:color={verdictColor(selectedVerdict)}>
+
        <Icon name={verdictIcon(selectedVerdict)} />
+
      </span>
+
      <span style:color="var(--color-text-secondary)">
+
        {selectedVerdict ? capitalize(`${selectedVerdict}ed`) : "None"}
+
      </span>
+
      <Icon name={popoverExpanded ? "chevron-up" : "chevron-down"} />
+
    </Button>
  {/snippet}
  {#snippet popover()}
-
    <Border variant="ghost">
+
    <div
+
      style:border="1px solid var(--color-border-subtle)"
+
      style:border-radius="var(--border-radius-sm)"
+
      style:display="flex"
+
      style:gap="0.5rem"
+
      style:align-items="center"
+
      style:background-color="var(--color-surface-canvas)">
      <DropdownList items={[undefined, "accept", "reject"] as const}>
        {#snippet item(verdict)}
          <DropdownListItem
@@ -74,21 +74,25 @@
              verdict === undefined &&
              summaryMissing}
            selected={selectedVerdict === verdict}
+
            styleGap="0.5rem"
            onclick={async () => {
              await onSelect(verdict);
              closeFocused();
            }}>
            <span
-
              class="global-flex"
-
              class:accepted={verdict === "accept"}
-
              class:rejected={verdict === "reject"}
-
              class:no-verdict={verdict === undefined}>
+
              class="global-chip"
+
              style:padding="0"
+
              style:margin-left="-0.5rem"
+
              style:background-color={verdictBgColor(verdict)}
+
              style:color={verdictColor(verdict)}>
              <Icon name={verdictIcon(verdict)} />
+
            </span>
+
            <span style:color="var(--color-text-secondary)">
              {verdict ? capitalize(`${verdict}ed`) : "None"}
            </span>
          </DropdownListItem>
        {/snippet}
      </DropdownList>
-
    </Border>
+
    </div>
  {/snippet}
</Popover>
modified src/components/VisibilityBadge.svelte
@@ -16,22 +16,23 @@
  .badge {
    gap: 0.375rem;
    padding-right: 0.625rem;
+
    font: var(--txt-body-s-regular);
  }

  .public {
-
    background-color: var(--color-fill-diff-green-light);
-
    color: var(--color-foreground-success);
+
    background-color: var(--color-feedback-success-bg);
+
    color: var(--color-feedback-success-text);
  }
  .private {
-
    background-color: var(--color-fill-private);
-
    color: var(--color-foreground-yellow);
+
    background-color: var(--color-feedback-warning-bg);
+
    color: var(--color-feedback-warning-text);
  }
</style>

<span
-
  class="global-counter badge"
+
  class="global-chip badge"
  class:public={type === "public"}
  class:private={type === "private"}>
-
  <Icon name={type === "public" ? "seedling" : "lock"} />
+
  <Icon name={type === "public" ? "seed" : "lock"} />
  {capitalize(type)}
</span>
modified src/lib/auth.svelte.ts
@@ -23,7 +23,7 @@ export async function checkAuth() {
      import.meta.env.VITE_AUTH_LONG_DELAY || 30_000,
    );
    if (get(router.activeRouteStore).resource === "booting") {
-
      void router.push({ resource: "home", activeTab: "all" });
+
      void router.push({ resource: "inbox" });
    }
  } catch (err) {
    const error = err as ErrorWrapper;
added src/lib/avatar.ts
@@ -0,0 +1,937 @@
+
/* eslint-disable @typescript-eslint/naming-convention */
+
import { cached } from "@app/lib/cached";
+

+
type AtomType = "A" | "B" | "C" | "D";
+
type ShapeModel =
+
  | "rose"
+
  | "starburst"
+
  | "ringed"
+
  | "tip"
+
  | "notched"
+
  | "hollow";
+
type AtomMode = "bands-ABC" | "angle-stripes" | "parity-ACB" | "balanced-rand";
+

+
const PALETTE = [
+
  "#00D4DA", // teal
+
  "#886BF2", // purple
+
  "#FFA5FF", // pink
+
  "#009F67", // green
+
  "#CCFF38", // lime
+
  "#585600", // olive
+
];
+

+
const REPO_CONFIG = {
+
  GRID_SIZE: 16,
+
  CELL_SIZE: 32,
+
  PIXEL_DENSITY: 2,
+
  GLYPH: {
+
    WIDTH: 5,
+
    HEIGHT: 7,
+
    SPACING: 2,
+
    SCALE_2X_WIDTH: 10,
+
    SCALE_2X_HEIGHT: 14,
+
  },
+
  ATOMS: {
+
    CIRCLE_B_RATIO: 0.55,
+
    CIRCLE_C_RATIO: 0.67,
+
  },
+
} as const;
+

+
const USER_CONFIG = {
+
  TILE_SIZE: 32,
+
  DEFAULT_GRID: 10,
+
  PIXEL_DENSITY: 2,
+
  ATOMS: {
+
    ELLIPSE_B_SIZE: 17,
+
    ELLIPSE_C_SIZE: 21,
+
  },
+
  TOLERANCE: {
+
    ANGLE_NEAR_BASE: Math.PI / 28,
+
    ANGLE_FAR_BASE: Math.PI / 7,
+
    NEAR_RANGE: { MIN: 0.7, MAX: 1.2 },
+
    FAR_RANGE: { MIN: 0.7, MAX: 1.2 },
+
  },
+
  SOFTNESS: {
+
    MIN: 1.2,
+
    MAX: 4.2,
+
  },
+
  RING_PHASE: {
+
    MIN: 0.2,
+
    MAX: 0.8,
+
  },
+
} as const;
+

+
function hash32(str: string): number {
+
  let h = 2166136261 >>> 0;
+
  for (let i = 0; i < str.length; i++) {
+
    h ^= str.charCodeAt(i);
+
    h = Math.imul(h, 16777619);
+
  }
+
  return h >>> 0;
+
}
+

+
function xmur3(str: string): () => number {
+
  let h = 1779033703 ^ str.length;
+
  for (let i = 0; i < str.length; i++) {
+
    h = Math.imul(h ^ str.charCodeAt(i), 3432918353);
+
    h = (h << 13) | (h >>> 19);
+
  }
+
  return function () {
+
    h = Math.imul(h ^ (h >>> 16), 2246822507);
+
    h = Math.imul(h ^ (h >>> 13), 3266489909);
+
    return (h ^= h >>> 16) >>> 0;
+
  };
+
}
+

+
function mulberry32(a: number): () => number {
+
  return function () {
+
    let t = (a += 0x6d2b79f5);
+
    t = Math.imul(t ^ (t >>> 15), t | 1);
+
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
+
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
+
  };
+
}
+

+
function makeRNG(key: string): () => number {
+
  return mulberry32(xmur3(key)());
+
}
+

+
function chooseK<T>(arr: T[], k: number, rnd: () => number): T[] {
+
  const pool = arr.slice();
+
  const out = [];
+
  for (let i = 0; i < k; i++) {
+
    const idx = Math.floor(rnd() * pool.length);
+
    out.push(pool.splice(idx, 1)[0]);
+
  }
+
  return out;
+
}
+

+
function pick<T>(rng: () => number, arr: T[]): T {
+
  return arr[Math.floor(rng() * arr.length)];
+
}
+

+
function createOffscreenCanvas(
+
  w: number,
+
  h: number,
+
  density: number = 2,
+
): { canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D } {
+
  const canvas = document.createElement("canvas");
+
  canvas.width = w * density;
+
  canvas.height = h * density;
+
  const ctx = canvas.getContext("2d")!;
+
  ctx.scale(density, density);
+
  ctx.imageSmoothingEnabled = false;
+
  return { canvas, ctx };
+
}
+

+
function fillCircle(
+
  ctx: CanvasRenderingContext2D,
+
  x: number,
+
  y: number,
+
  diameter: number,
+
  color: string,
+
): void {
+
  ctx.fillStyle = color;
+
  ctx.beginPath();
+
  ctx.arc(x, y, diameter / 2, 0, Math.PI * 2);
+
  ctx.fill();
+
}
+

+
function fillEllipse(
+
  ctx: CanvasRenderingContext2D,
+
  x: number,
+
  y: number,
+
  w: number,
+
  h: number,
+
  color: string,
+
): void {
+
  ctx.fillStyle = color;
+
  ctx.beginPath();
+
  ctx.ellipse(x, y, w / 2, h / 2, 0, 0, Math.PI * 2);
+
  ctx.fill();
+
}
+

+
interface AtomDrawConfig {
+
  cellSize: number;
+
  circleB: number;
+
  circleC: number;
+
  useEllipse?: boolean;
+
}
+

+
function createAtomRenderer(config: AtomDrawConfig) {
+
  const drawCircleOrEllipse = (
+
    ctx: CanvasRenderingContext2D,
+
    x: number,
+
    y: number,
+
    w: number,
+
    h: number,
+
    color: string,
+
  ) => {
+
    if (config.useEllipse) {
+
      fillEllipse(ctx, x, y, w, h, color);
+
    } else {
+
      fillCircle(ctx, x, y, w, color); // w is diameter for circles
+
    }
+
  };
+

+
  return {
+
    drawAtomA(
+
      ctx: CanvasRenderingContext2D,
+
      gx: number,
+
      gy: number,
+
      c1: string,
+
    ): void {
+
      const x = gx * config.cellSize;
+
      const y = gy * config.cellSize;
+
      ctx.fillStyle = c1;
+
      ctx.fillRect(x, y, config.cellSize, config.cellSize);
+
    },
+

+
    drawAtomB(
+
      ctx: CanvasRenderingContext2D,
+
      gx: number,
+
      gy: number,
+
      c1: string,
+
      c2: string,
+
    ): void {
+
      const x = gx * config.cellSize;
+
      const y = gy * config.cellSize;
+
      ctx.fillStyle = c1;
+
      ctx.fillRect(x, y, config.cellSize, config.cellSize);
+
      drawCircleOrEllipse(
+
        ctx,
+
        x + config.cellSize / 2,
+
        y + config.cellSize / 2,
+
        config.circleB,
+
        config.circleB,
+
        c2,
+
      );
+
    },
+

+
    drawAtomC(
+
      ctx: CanvasRenderingContext2D,
+
      gx: number,
+
      gy: number,
+
      c2: string,
+
      c3: string,
+
    ): void {
+
      const x = gx * config.cellSize;
+
      const y = gy * config.cellSize;
+
      ctx.fillStyle = c2;
+
      ctx.fillRect(x, y, config.cellSize, config.cellSize);
+
      drawCircleOrEllipse(
+
        ctx,
+
        x + config.cellSize / 2,
+
        y + config.cellSize / 2,
+
        config.circleC,
+
        config.circleC,
+
        c3,
+
      );
+
    },
+

+
    drawAtomD(
+
      ctx: CanvasRenderingContext2D,
+
      gx: number,
+
      gy: number,
+
      c3: string,
+
    ): void {
+
      const x = gx * config.cellSize;
+
      const y = gy * config.cellSize;
+
      ctx.fillStyle = c3;
+
      ctx.fillRect(x, y, config.cellSize, config.cellSize);
+
    },
+

+
    drawAtom(
+
      ctx: CanvasRenderingContext2D,
+
      atom: AtomType,
+
      gx: number,
+
      gy: number,
+
      c1: string,
+
      c2: string,
+
      c3: string,
+
    ): void {
+
      switch (atom) {
+
        case "A":
+
          this.drawAtomA(ctx, gx, gy, c1);
+
          break;
+
        case "B":
+
          this.drawAtomB(ctx, gx, gy, c1, c2);
+
          break;
+
        case "C":
+
          this.drawAtomC(ctx, gx, gy, c2, c3);
+
          break;
+
        case "D":
+
          this.drawAtomD(ctx, gx, gy, c3);
+
          break;
+
      }
+
    },
+
  };
+
}
+

+
// 5x7 pixel font glyphs (cached at module level for performance)
+
function createGlyphs5x7(): Record<string, number[][]> {
+
  const L: Record<string, number[][]> = {};
+
  const r = (s: string[]) =>
+
    s.map(row => row.split("").map(ch => (ch === "1" ? 1 : 0)));
+
  L["A"] = r(["01110", "10001", "10001", "11111", "10001", "10001", "10001"]);
+
  L["B"] = r(["11110", "10001", "10001", "11110", "10001", "10001", "11110"]);
+
  L["C"] = r(["01111", "10000", "10000", "10000", "10000", "10000", "01111"]);
+
  L["D"] = r(["11110", "10001", "10001", "10001", "10001", "10001", "11110"]);
+
  L["E"] = r(["11111", "10000", "10000", "11110", "10000", "10000", "11111"]);
+
  L["F"] = r(["11111", "10000", "10000", "11110", "10000", "10000", "10000"]);
+
  L["G"] = r(["01111", "10000", "10000", "10111", "10001", "10001", "01111"]);
+
  L["H"] = r(["10001", "10001", "10001", "11111", "10001", "10001", "10001"]);
+
  L["I"] = r(["11111", "00100", "00100", "00100", "00100", "00100", "11111"]);
+
  L["J"] = r(["11111", "00001", "00001", "00001", "10001", "10001", "01110"]);
+
  L["K"] = r(["10001", "10010", "10100", "11000", "10100", "10010", "10001"]);
+
  L["L"] = r(["10000", "10000", "10000", "10000", "10000", "10000", "11111"]);
+
  L["M"] = r(["10001", "11011", "10101", "10001", "10001", "10001", "10001"]);
+
  L["N"] = r(["10001", "11001", "10101", "10011", "10001", "10001", "10001"]);
+
  L["O"] = r(["01110", "10001", "10001", "10001", "10001", "10001", "01110"]);
+
  L["P"] = r(["11110", "10001", "10001", "11110", "10000", "10000", "10000"]);
+
  L["Q"] = r(["01110", "10001", "10001", "10001", "10101", "10010", "01101"]);
+
  L["R"] = r(["11110", "10001", "10001", "11110", "10100", "10010", "10001"]);
+
  L["S"] = r(["01111", "10000", "11110", "00001", "00001", "10001", "11110"]);
+
  L["T"] = r(["11111", "00100", "00100", "00100", "00100", "00100", "00100"]);
+
  L["U"] = r(["10001", "10001", "10001", "10001", "10001", "10001", "01110"]);
+
  L["V"] = r(["10001", "10001", "10001", "01010", "01010", "00100", "00100"]);
+
  L["W"] = r(["10001", "10001", "10001", "10101", "10101", "11011", "10001"]);
+
  L["X"] = r(["10001", "01010", "00100", "00100", "00100", "01010", "10001"]);
+
  L["Y"] = r(["10001", "01010", "00100", "00100", "00100", "00100", "00100"]);
+
  L["Z"] = r(["11111", "00001", "00010", "00100", "01000", "10000", "11111"]);
+
  L["0"] = r(["01110", "10001", "10011", "10101", "11001", "10001", "01110"]);
+
  L["1"] = r(["00100", "01100", "00100", "00100", "00100", "00100", "01110"]);
+
  L["2"] = r(["01110", "10001", "00001", "00110", "01000", "10000", "11111"]);
+
  L["3"] = r(["11110", "00001", "01110", "00001", "00001", "00001", "11110"]);
+
  L["4"] = r(["10010", "10010", "10010", "11111", "00010", "00010", "00010"]);
+
  L["5"] = r(["11111", "10000", "11110", "00001", "00001", "00001", "11110"]);
+
  L["6"] = r(["01110", "10000", "11110", "10001", "10001", "10001", "01110"]);
+
  L["7"] = r(["11111", "00001", "00010", "00100", "01000", "01000", "01000"]);
+
  L["8"] = r(["01110", "10001", "01110", "10001", "10001", "10001", "01110"]);
+
  L["9"] = r(["01110", "10001", "10001", "01111", "00001", "00001", "11110"]);
+
  L["?"] = r(["11111", "00001", "01110", "00000", "00100", "00000", "00100"]);
+
  return L;
+
}
+

+
const LETTER_5X7 = createGlyphs5x7();
+

+
function getInitials(name: string): string[] {
+
  if (!name || typeof name !== "string") return ["?"];
+
  const cleaned = name.trim().replace(/\s+/g, " ");
+
  const parts = cleaned.split(/[^A-Za-z0-9]+/).filter(Boolean);
+
  const first = parts[0] ? parts[0][0].toUpperCase() : "?";
+
  const second = parts[1] ? parts[1][0].toUpperCase() : null;
+
  return second ? [first, second] : [first];
+
}
+

+
function polarFromCell(
+
  gx: number,
+
  gy: number,
+
  cx: number,
+
  cy: number,
+
): { r: number; a: number } {
+
  const x = gx - cx + 0.5;
+
  const y = gy - cy + 0.5;
+
  const r = Math.hypot(x, y);
+
  let a = Math.atan2(y, x);
+
  if (a < 0) a += 2 * Math.PI;
+
  return { r, a };
+
}
+

+
function shapeRose(theta: number, petals: number, tol: number): boolean {
+
  const sector = Math.PI / petals;
+
  const nearest = Math.round(theta / sector) * sector;
+
  let diff = Math.abs(theta - nearest);
+
  diff = Math.min(diff, 2 * Math.PI - diff);
+
  return diff <= tol;
+
}
+

+
function shapeStarburst(
+
  theta: number,
+
  petals: number,
+
  softness: number,
+
): boolean {
+
  const period = (2 * Math.PI) / petals;
+
  const local = theta % period;
+
  const d = Math.min(local, period - local) / (period / 2);
+
  const response = Math.pow(Math.cos((d * Math.PI) / 2), softness);
+
  return response > 0.5;
+
}
+

+
function shapeRinged(
+
  theta: number,
+
  petals: number,
+
  ringPhase: number,
+
  tol: number,
+
): boolean {
+
  const sector = (2 * Math.PI) / petals;
+
  const k = Math.floor(theta / sector);
+
  const center = k * sector + sector * ringPhase;
+
  let diff = Math.abs(theta - center);
+
  diff = Math.min(diff, 2 * Math.PI - diff);
+
  return diff <= tol;
+
}
+

+
function shapeTip(
+
  theta: number,
+
  petals: number,
+
  tol: number,
+
  t: number,
+
): boolean {
+
  return shapeRose(theta, petals, tol * (0.5 + 1.0 * t)) && t > 0.45;
+
}
+

+
function shapeNotched(
+
  theta: number,
+
  petals: number,
+
  tol: number,
+
  notchDepth: number = 0.25,
+
): boolean {
+
  const sector = (2 * Math.PI) / petals;
+
  const local = (theta % sector) / sector;
+
  const notch = Math.abs(local - 0.5);
+
  return shapeRose(theta, petals, tol) && notch > notchDepth;
+
}
+

+
function shapeHollow(
+
  theta: number,
+
  petals: number,
+
  tol: number,
+
  t: number,
+
  inner: number = 0.28,
+
  outer: number = 0.9,
+
): boolean {
+
  return shapeRose(theta, petals, tol) && t > inner && t < outer;
+
}
+

+
function sectorGate(theta: number, petals: number, mask: boolean[]): boolean {
+
  const sector = (2 * Math.PI) / petals;
+
  const k = Math.floor(theta / sector);
+
  return mask[k % mask.length];
+
}
+

+
export const cachedRepoAvatar = cached(
+
  async (key: string) => renderRepoAvatar(key),
+
  (key: string) => key,
+
  { max: 2000 },
+
);
+

+
export const cachedUserAvatar = cached(
+
  async (key: string) => renderUserAvatar(key),
+
  (key: string) => key,
+
  { max: 2000 },
+
);
+

+
function renderRepoAvatar(key: string): string {
+
  if (typeof window === "undefined") {
+
    return "";
+
  }
+

+
  {
+
    // Color logic:
+
    //   A: square = Color1
+
    //   B: square = Color1, circle = Color2
+
    //   C: square = Color2, circle = Color3
+
    //   D: square = Color3
+
    // Letters: solid-only (A or D). Background: other three atoms.
+
    // Single initial: 2x2 expansion (10x14). Two initials: 5x7 each.
+

+
    const GRID = REPO_CONFIG.GRID_SIZE;
+
    const CELL = REPO_CONFIG.CELL_SIZE;
+
    const W = GRID * CELL;
+
    const H = GRID * CELL;
+

+
    const { canvas, ctx } = createOffscreenCanvas(
+
      W,
+
      H,
+
      REPO_CONFIG.PIXEL_DENSITY,
+
    );
+

+
    const atoms = createAtomRenderer({
+
      cellSize: CELL,
+
      circleB: CELL * REPO_CONFIG.ATOMS.CIRCLE_B_RATIO,
+
      circleC: CELL * REPO_CONFIG.ATOMS.CIRCLE_C_RATIO,
+
      useEllipse: false,
+
    });
+

+
    function renderInitialsAvatar(nameKey: string = "color bright") {
+
      const initials = getInitials(nameKey);
+
      const seed = hash32(nameKey.toLowerCase());
+
      const rnd = mulberry32(seed);
+

+
      // Select three distinct colors deterministically (Color1, Color2, Color3)
+
      const [c1, c2, c3] = chooseK(PALETTE, 3, rnd);
+

+
      // Choose letter solid atom: 'A' or 'D' (deterministic)
+
      const letterSolidAtom: AtomType = ((seed >>> 7) & 1) === 0 ? "A" : "D";
+

+
      const bgAtoms: AtomType[] = (["A", "B", "C", "D"] as AtomType[]).filter(
+
        a => a !== letterSolidAtom,
+
      );
+

+
      function pickBgAtom(gx: number, gy: number): AtomType {
+
        const k = (gy * 131 + gx * 197 + seed) >>> 0;
+
        return bgAtoms[k % bgAtoms.length];
+
      }
+

+
      // 1) Background: fill with bgAtoms using strict color mapping
+
      for (let gy = 0; gy < GRID; gy++) {
+
        for (let gx = 0; gx < GRID; gx++) {
+
          const atom = pickBgAtom(gx, gy);
+
          atoms.drawAtom(ctx, atom, gx, gy, c1, c2, c3);
+
        }
+
      }
+

+
      // 2) Letters: solid-only atom across glyph pixels, strict mapping (A uses c1, D uses c3)
+
      const glyphW = REPO_CONFIG.GLYPH.WIDTH;
+
      const glyphH = REPO_CONFIG.GLYPH.HEIGHT;
+

+
      function placeSolidLetter(
+
        glyph: number[][],
+
        startX: number,
+
        startY: number,
+
        scale2x: boolean,
+
      ) {
+
        if (scale2x) {
+
          // 2x2 expansion → 10x14
+
          for (let r = 0; r < glyphH; r++) {
+
            for (let c = 0; c < glyphW; c++) {
+
              if (!glyph[r][c]) continue;
+
              const gx = startX + c * 2;
+
              const gy = startY + r * 2;
+
              if (letterSolidAtom === "A") {
+
                atoms.drawAtomA(ctx, gx, gy, c1);
+
                atoms.drawAtomA(ctx, gx + 1, gy, c1);
+
                atoms.drawAtomA(ctx, gx, gy + 1, c1);
+
                atoms.drawAtomA(ctx, gx + 1, gy + 1, c1);
+
              } else {
+
                atoms.drawAtomD(ctx, gx, gy, c3);
+
                atoms.drawAtomD(ctx, gx + 1, gy, c3);
+
                atoms.drawAtomD(ctx, gx, gy + 1, c3);
+
                atoms.drawAtomD(ctx, gx + 1, gy + 1, c3);
+
              }
+
            }
+
          }
+
        } else {
+
          // 1x scale
+
          for (let r = 0; r < glyphH; r++) {
+
            for (let c = 0; c < glyphW; c++) {
+
              if (!glyph[r][c]) continue;
+
              const gx = startX + c;
+
              const gy = startY + r;
+
              if (letterSolidAtom === "A") {
+
                atoms.drawAtomA(ctx, gx, gy, c1);
+
              } else {
+
                atoms.drawAtomD(ctx, gx, gy, c3);
+
              }
+
            }
+
          }
+
        }
+
      }
+

+
      if (initials.length === 1) {
+
        // Single initial: 2x2 expansion (10x14), centered
+
        const glyph = LETTER_5X7[initials[0]] || LETTER_5X7["?"];
+
        const startX = Math.floor(
+
          (GRID - REPO_CONFIG.GLYPH.SCALE_2X_WIDTH) / 2,
+
        );
+
        const startY = Math.floor(
+
          (GRID - REPO_CONFIG.GLYPH.SCALE_2X_HEIGHT) / 2,
+
        );
+
        placeSolidLetter(glyph, startX, startY, true);
+
      } else {
+
        // Two initials: 5x7 each, side-by-side
+
        const leftGlyph = LETTER_5X7[initials[0]] || LETTER_5X7["?"];
+
        const rightGlyph = LETTER_5X7[initials[1]] || LETTER_5X7["?"];
+
        const spacing = REPO_CONFIG.GLYPH.SPACING;
+
        const totalW = REPO_CONFIG.GLYPH.WIDTH * 2 + spacing;
+
        const totalH = REPO_CONFIG.GLYPH.HEIGHT;
+
        const startX = Math.floor((GRID - totalW) / 2);
+
        const startY = Math.floor((GRID - totalH) / 2);
+
        placeSolidLetter(leftGlyph, startX, startY, false);
+
        placeSolidLetter(
+
          rightGlyph,
+
          startX + REPO_CONFIG.GLYPH.WIDTH + spacing,
+
          startY,
+
          false,
+
        );
+
      }
+

+
      return canvas.toDataURL();
+
    }
+

+
    return renderInitialsAvatar(key);
+
  }
+
}
+

+
function renderUserAvatar(key: string): string {
+
  if (typeof window === "undefined") {
+
    return "";
+
  }
+

+
  {
+
    const TILE = USER_CONFIG.TILE_SIZE;
+
    const DEFAULT_GRID = USER_CONFIG.DEFAULT_GRID;
+

+
    const atoms = createAtomRenderer({
+
      cellSize: TILE,
+
      circleB: USER_CONFIG.ATOMS.ELLIPSE_B_SIZE,
+
      circleC: USER_CONFIG.ATOMS.ELLIPSE_C_SIZE,
+
      useEllipse: true,
+
    });
+

+
    // Edge-to-edge integer placement
+
    function drawAt(
+
      fn: (x: number, y: number) => void,
+
      gx: number,
+
      gy: number,
+
    ): void {
+
      fn(gx * TILE, gy * TILE);
+
    }
+

+
    // Strict 4-way symmetry (quadrant mirroring)
+
    function drawQuad(
+
      fn: (x: number, y: number) => void,
+
      gx: number,
+
      gy: number,
+
      grid: number,
+
    ): void {
+
      const N = grid - 1;
+
      drawAt(fn, gx, gy);
+
      drawAt(fn, N - gx, gy);
+
      drawAt(fn, gx, N - gy);
+
      drawAt(fn, N - gx, N - gy);
+
    }
+

+
    // Draw by atom type (converts pixel coordinates to grid coordinates)
+
    function drawAtomByType(
+
      ctx: CanvasRenderingContext2D,
+
      type: AtomType,
+
      x: number,
+
      y: number,
+
      c1: string,
+
      c2: string,
+
      c3: string,
+
    ): void {
+
      const gx = x / TILE;
+
      const gy = y / TILE;
+
      atoms.drawAtom(ctx, type, gx, gy, c1, c2, c3);
+
    }
+

+
    // Make assigner among active petal atoms (exclude background atom)
+
    function makeAssigner(
+
      mode: AtomMode,
+
      activeAtoms: AtomType[],
+
    ): (rCell: number, theta?: number, sectorIdx?: number) => AtomType {
+
      if (mode === "bands-ABC")
+
        return (rCell: number) => activeAtoms[rCell % 3];
+
      if (mode === "angle-stripes")
+
        return (_rCell: number, _theta?: number, sectorIdx?: number) =>
+
          activeAtoms[(sectorIdx || 0) % activeAtoms.length];
+
      if (mode === "parity-ACB")
+
        return (rCell: number) => activeAtoms[rCell % 2 ? 1 : 0];
+
      if (mode === "balanced-rand")
+
        return (rCell: number, theta?: number, sectorIdx?: number) => {
+
          const v =
+
            (Math.sin(
+
              (theta || 0) * 13.37 + rCell * 2.17 + (sectorIdx || 0) * 0.73,
+
            ) +
+
              1) /
+
            2;
+
          if (v < 0.33) return activeAtoms[0];
+
          if (v < 0.66) return activeAtoms[1];
+
          return activeAtoms[2];
+
        };
+
      return (rCell: number) => activeAtoms[rCell % 3];
+
    }
+

+
    function generateFlower(
+
      ctx: CanvasRenderingContext2D,
+
      canvas: HTMLCanvasElement,
+
      key: string,
+
      grid: number = DEFAULT_GRID,
+
    ): void {
+
      ctx.clearRect(0, 0, canvas.width, canvas.height);
+

+
      const rng = makeRNG(key);
+

+
      const picked = PALETTE.slice().sort(() => rng() - 0.5);
+
      const [c1, c2, c3] = picked.slice(0, 3);
+

+
      const allAtoms: AtomType[] = ["A", "B", "C", "D"];
+
      const bgAtom: AtomType = pick(rng, allAtoms);
+
      const petalAtoms: AtomType[] = allAtoms
+
        .filter(a => a !== bgAtom)
+
        .sort(() => rng() - 0.5);
+

+
      const cx = Math.floor(grid / 2),
+
        cy = Math.floor(grid / 2);
+
      const maxR = Math.min(cx, cy);
+
      const petals = pick(rng, [5, 6, 7, 8, 9, 10]);
+
      const petalDepth = Math.max(5, Math.floor(maxR * (0.6 + 0.35 * rng())));
+
      const radialThickness = pick(rng, [1, 2, 2, 3, 3]);
+
      const shapeModel: ShapeModel = pick(rng, [
+
        "rose",
+
        "starburst",
+
        "ringed",
+
        "tip",
+
        "notched",
+
        "hollow",
+
      ]);
+
      const atomMode: AtomMode = pick(rng, [
+
        "bands-ABC",
+
        "angle-stripes",
+
        "parity-ACB",
+
        "balanced-rand",
+
      ]);
+
      const assignAtom = makeAssigner(atomMode, petalAtoms);
+

+
      // Angle tolerances
+
      const angleTolNear =
+
        USER_CONFIG.TOLERANCE.ANGLE_NEAR_BASE *
+
        (USER_CONFIG.TOLERANCE.NEAR_RANGE.MIN +
+
          rng() *
+
            (USER_CONFIG.TOLERANCE.NEAR_RANGE.MAX -
+
              USER_CONFIG.TOLERANCE.NEAR_RANGE.MIN));
+
      const angleTolFar =
+
        USER_CONFIG.TOLERANCE.ANGLE_FAR_BASE *
+
        (USER_CONFIG.TOLERANCE.FAR_RANGE.MIN +
+
          rng() *
+
            (USER_CONFIG.TOLERANCE.FAR_RANGE.MAX -
+
              USER_CONFIG.TOLERANCE.FAR_RANGE.MIN));
+
      const softness =
+
        USER_CONFIG.SOFTNESS.MIN +
+
        rng() * (USER_CONFIG.SOFTNESS.MAX - USER_CONFIG.SOFTNESS.MIN);
+
      const ringPhase =
+
        USER_CONFIG.RING_PHASE.MIN +
+
        rng() * (USER_CONFIG.RING_PHASE.MAX - USER_CONFIG.RING_PHASE.MIN);
+

+
      // Sector gating mask (~70% sectors on), ensures bold shapes
+
      const sectorMask = Array.from({ length: petals }, () => rng() > 0.3);
+
      if (sectorMask.every(v => !v)) sectorMask[Math.floor(petals / 2)] = true;
+

+
      const drawBgAtom = (gx: number, gy: number) => {
+
        drawAt(
+
          (x: number, y: number) =>
+
            drawAtomByType(ctx, bgAtom, x, y, c1, c2, c3),
+
          gx,
+
          gy,
+
        );
+
      };
+

+
      // 0) Base pass: paint every tile once with background atom to avoid gaps
+
      function renderBasePass() {
+
        for (let gy = 0; gy < grid; gy++) {
+
          for (let gx = 0; gx < grid; gx++) {
+
            drawBgAtom(gx, gy);
+
          }
+
        }
+
      }
+

+
      // 1) Outer edge circumference (1–2 tiles thick) reinforced in background atom
+
      function renderEdgeReinforcement() {
+
        const edgeThickness = pick(rng, [1, 1, 2]);
+
        for (let t = 0; t < edgeThickness; t++) {
+
          for (let i = 0; i < grid; i++) {
+
            drawBgAtom(i, t);
+
            drawBgAtom(i, grid - 1 - t);
+
            drawBgAtom(t, i);
+
            drawBgAtom(grid - 1 - t, i);
+
          }
+
        }
+
      }
+

+
      // 2) Background atom structural accents inside (deterministic)
+
      //    - mid ring (optional) and a few gated spokes to help define silhouette
+
      function renderStructuralAccents() {
+
        // Mid ring
+
        if (rng() < 0.7) {
+
          const midR = Math.floor(petalDepth * (0.5 + 0.2 * rng()));
+
          for (let i = 0; i < grid; i++) {
+
            const coords: [number, number][] = [
+
              [cx - midR, i],
+
              [cx + midR, i],
+
              [i, cy - midR],
+
              [i, cy + midR],
+
            ];
+
            coords.forEach(([gx, gy]) => {
+
              if (gx >= 0 && gy >= 0 && gx < grid && gy < grid) {
+
                drawQuad(
+
                  (x: number, y: number) =>
+
                    drawAtomByType(ctx, bgAtom, x, y, c1, c2, c3),
+
                  gx,
+
                  gy,
+
                  grid,
+
                );
+
              }
+
            });
+
          }
+
        }
+

+
        // Gated spokes
+
        if (rng() < 0.6) {
+
          const gateEvery = pick(rng, [2, 3, 4]);
+
          for (let s = 0; s < petals; s++) {
+
            if (s % gateEvery !== 0) continue;
+
            const theta = s * ((2 * Math.PI) / petals);
+
            for (let r = 1; r <= petalDepth; r++) {
+
              const gx = Math.round(cx + r * Math.cos(theta));
+
              const gy = Math.round(cy + r * Math.sin(theta));
+
              if (gx >= 0 && gy >= 0 && gx < grid && gy < grid) {
+
                drawQuad(
+
                  (x: number, y: number) =>
+
                    drawAtomByType(ctx, bgAtom, x, y, c1, c2, c3),
+
                  gx,
+
                  gy,
+
                  grid,
+
                );
+
              }
+
            }
+
          }
+
        }
+
      }
+

+
      // 3) Strong center cluster (guarantee coverage; seed form)
+
      function renderCenterCluster() {
+
        const centerCluster: [number, number, AtomType][] = [
+
          [0, 0, "D"],
+
          [0, -1, petalAtoms[0]],
+
          [1, 0, petalAtoms[1]],
+
          [0, 1, petalAtoms[2]],
+
          [-1, 0, petalAtoms[0]],
+
        ];
+
        centerCluster.forEach(([dx, dy, t]: [number, number, AtomType]) => {
+
          const gx = cx + dx,
+
            gy = cy + dy;
+
          if (gx < 0 || gy < 0 || gx >= grid || gy >= grid) return;
+
          drawQuad(
+
            (x: number, y: number) => drawAtomByType(ctx, t, x, y, c1, c2, c3),
+
            gx,
+
            gy,
+
            grid,
+
          );
+
        });
+
      }
+

+
      // 4) Petals (TL quadrant → mirror)
+
      function renderPetals() {
+
        const half = Math.ceil(grid / 2);
+
        for (let gy = 0; gy < half; gy++) {
+
          for (let gx = 0; gx < half; gx++) {
+
            const { r, a } = polarFromCell(gx, gy, cx, cy);
+
            const rCell = Math.floor(r);
+
            if (rCell === 0 || rCell > petalDepth) continue;
+

+
            const t = rCell / petalDepth;
+
            const tol = angleTolNear * (1 - t) + angleTolFar * t;
+

+
            const sectorIdx = Math.floor(a / ((2 * Math.PI) / petals));
+
            if (!sectorGate(a, petals, sectorMask)) continue;
+

+
            let inside = false;
+
            if (shapeModel === "rose") inside = shapeRose(a, petals, tol);
+
            else if (shapeModel === "starburst")
+
              inside = shapeStarburst(a, petals, softness);
+
            else if (shapeModel === "ringed")
+
              inside = shapeRinged(a, petals, ringPhase, tol * 0.7);
+
            else if (shapeModel === "tip") inside = shapeTip(a, petals, tol, t);
+
            else if (shapeModel === "notched")
+
              inside = shapeNotched(a, petals, tol, 0.24);
+
            else inside = shapeHollow(a, petals, tol, t, 0.28, 0.92);
+

+
            if (!inside) continue;
+

+
            const type = assignAtom(rCell, a, sectorIdx);
+

+
            // Draw with radial thickness and 4-way mirroring
+
            for (let dr = 0; dr < radialThickness; dr++) {
+
              const x1 = gx + dr,
+
                y1 = gy + dr;
+
              const coords: [number, number][] = [
+
                [x1, y1],
+
                [grid - 1 - x1, y1],
+
                [x1, grid - 1 - y1],
+
                [grid - 1 - x1, grid - 1 - y1],
+
              ];
+
              coords.forEach(([ix, iy]: [number, number]) => {
+
                if (ix < 0 || iy < 0 || ix >= grid || iy >= grid) return;
+
                drawAt(
+
                  (x: number, y: number) =>
+
                    drawAtomByType(ctx, type, x, y, c1, c2, c3),
+
                  ix,
+
                  iy,
+
                );
+
              });
+
            }
+
          }
+
        }
+
      }
+

+
      // 5) Ensure all three petal atoms appear at least once
+
      function renderAccents() {
+
        const accents: [number, number, AtomType][] = [
+
          [cx, cy - 2, petalAtoms[0]],
+
          [cx + 1, cy, petalAtoms[1]],
+
          [cx, cy + 2, petalAtoms[2]],
+
        ];
+
        accents.forEach(([gx, gy, t]: [number, number, AtomType]) => {
+
          if (gx < 0 || gy < 0 || gx >= grid || gy >= grid) return;
+
          drawQuad(
+
            (x: number, y: number) => drawAtomByType(ctx, t, x, y, c1, c2, c3),
+
            gx,
+
            gy,
+
            grid,
+
          );
+
        });
+
      }
+

+
      // 6) Final safety: ensure center 6×6 px is covered via 2×2 cluster
+
      function renderFinalCluster() {
+
        const cluster: [number, number, AtomType][] = [
+
          [0, 0, "D"],
+
          [1, 0, petalAtoms[1]],
+
          [0, 1, petalAtoms[2]],
+
          [1, 1, petalAtoms[0]],
+
        ];
+
        cluster.forEach(([dx, dy, t]: [number, number, AtomType]) => {
+
          const gx = cx + dx,
+
            gy = cy + dy;
+
          if (gx < 0 || gy < 0 || gx >= grid || gy >= grid) return;
+
          drawQuad(
+
            (x: number, y: number) => drawAtomByType(ctx, t, x, y, c1, c2, c3),
+
            gx,
+
            gy,
+
            grid,
+
          );
+
        });
+
      }
+

+
      renderBasePass();
+
      renderEdgeReinforcement();
+
      renderStructuralAccents();
+
      renderCenterCluster();
+
      renderPetals();
+
      renderAccents();
+
      renderFinalCluster();
+
    }
+

+
    function drawFlowerForKey(
+
      key: string,
+
      grid: number = DEFAULT_GRID,
+
    ): string {
+
      const canvasPx = grid * TILE;
+

+
      const { canvas, ctx } = createOffscreenCanvas(
+
        canvasPx,
+
        canvasPx,
+
        USER_CONFIG.PIXEL_DENSITY,
+
      );
+

+
      generateFlower(ctx, canvas, key, grid);
+

+
      return canvas.toDataURL();
+
    }
+

+
    return drawFlowerForKey(key, DEFAULT_GRID);
+
  }
+
}
deleted src/lib/blockies.ts
@@ -1,125 +0,0 @@
-
// 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;
-
}
added src/lib/modal.ts
@@ -0,0 +1,82 @@
+
import type { Component, ComponentProps } from "svelte";
+

+
import { derived, get, writable } from "svelte/store";
+

+
type HideCallback = () => void;
+

+
type Modal = {
+
  component: Component;
+
  props: Record<string, unknown>;
+
  hideCallback?: HideCallback;
+
  disableHide?: boolean;
+
  disableScrimClose?: boolean;
+
};
+

+
const store = writable<Modal | undefined>(undefined);
+
export const modalStore = derived(store, s => s);
+

+
export function enableHide() {
+
  store.update(s => {
+
    if (s) {
+
      return { ...s, disableHide: false };
+
    }
+
  });
+
}
+

+
export function disableHide() {
+
  store.update(s => {
+
    if (s) {
+
      return { ...s, disableHide: true };
+
    }
+
  });
+
}
+

+
export function forceHide(): void {
+
  const stored = get(modalStore);
+
  if (!stored) {
+
    return;
+
  }
+
  if (stored.hideCallback) {
+
    stored.hideCallback();
+
  }
+
  store.set(undefined);
+
}
+

+
export function hide(): void {
+
  const stored = get(modalStore);
+
  if (!stored || stored.disableHide) {
+
    return;
+
  }
+

+
  if (stored.hideCallback) {
+
    stored.hideCallback();
+
  }
+
  store.set(undefined);
+
}
+

+
interface ShowArgs<T extends Component> {
+
  component: T;
+
  props: ComponentProps<T>;
+
  hideCallback?: HideCallback;
+
  disableScrimClose?: boolean;
+
}
+

+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
+
export function show<T extends Component<any>>(args: ShowArgs<T>): void {
+
  if (document.activeElement instanceof HTMLElement) {
+
    document.activeElement.blur();
+
  }
+
  store.set({ ...args, disableScrimClose: args.disableScrimClose ?? false });
+
}
+

+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
+
export function toggle<T extends Component<any>>(args: ShowArgs<T>): void {
+
  const stored = get(modalStore);
+

+
  if (stored && stored.component === args.component) {
+
    hide();
+
    return;
+
  }
+

+
  show(args);
+
}
modified src/lib/notification.ts
@@ -8,7 +8,7 @@ export type Action =
  | ActionWithAuthor<IssueAction>
  | ActionWithAuthor<PatchAction>;

-
// N.b. I have taken the `%` char as indicator for a `global-oid` class
+
// N.b. I have taken the `%` char as indicator for a `txt-id` class
export function createSummary(
  a: Action[],
  kind: "issue" | "patch",
@@ -116,10 +116,7 @@ export function createSummary(
    summary = `redacted ${count > 1 ? count : "a"} ${pluralize("revision", count)}`;
  }

-
  return summary.replaceAll(
-
    /[%](\S+)[%]/g,
-
    '<span class="global-oid">$1</span>',
-
  );
+
  return summary.replaceAll(/[%](\S+)[%]/g, '<span class="txt-id">$1</span>');
}

export function compressActions(
added src/lib/notificationCount.svelte.ts
@@ -0,0 +1 @@
+
export const notificationCount = $state({ value: 0 });
added src/lib/portal.ts
@@ -0,0 +1,12 @@
+
export function portal(node: HTMLElement, target: HTMLElement = document.body) {
+
  const placeholder = document.createComment("portal");
+
  node.replaceWith(placeholder);
+
  target.appendChild(node);
+

+
  return {
+
    destroy() {
+
      node.remove();
+
      placeholder.remove();
+
    },
+
  };
+
}
modified src/lib/router.ts
@@ -1,7 +1,3 @@
-
import {
-
  homeRouteToPath as homeRouteToPath,
-
  homeUrlToRoute,
-
} from "@app/views/home/router";
import { repoRouteToPath, repoUrlToRoute } from "@app/views/repo/router";
import { on } from "svelte/events";
import { get, writable } from "svelte/store";
@@ -51,7 +47,7 @@ async function navigateToUrl(
    await navigate(action, route);
  } else {
    console.error("Could not resolve route for URL: ", url);
-
    await navigate(action, { resource: "home", activeTab: "all" });
+
    await navigate(action, { resource: "inbox" });
  }
}

@@ -121,13 +117,34 @@ export async function replace(newRoute: Route): Promise<void> {
  await navigate("replace", newRoute);
}

+
function inboxUrlToRoute(url: URL): { resource: "inbox" } | undefined {
+
  if (url.pathname === "/inbox") {
+
    return { resource: "inbox" };
+
  }
+
}
+

+
function guideUrlToRoute(url: URL): { resource: "guide" } | undefined {
+
  if (url.pathname === "/guide") {
+
    return { resource: "guide" };
+
  }
+
}
+

function urlToRoute(url: URL): Route | null {
  const segments = url.pathname.substring(1).split("/");
  const resource = segments.shift();

-
  const homeRoute = homeUrlToRoute(url);
-
  if (homeRoute) {
-
    return homeRoute;
+
  if (url.pathname === "/") {
+
    return { resource: "inbox" };
+
  }
+

+
  const inboxRoute = inboxUrlToRoute(url);
+
  if (inboxRoute) {
+
    return inboxRoute;
+
  }
+

+
  const guideRoute = guideUrlToRoute(url);
+
  if (guideRoute) {
+
    return guideRoute;
  }

  switch (resource) {
@@ -141,11 +158,12 @@ function urlToRoute(url: URL): Route | null {
}

export function routeToPath(route: Route): string {
-
  if (route.resource === "home") {
-
    return homeRouteToPath(route);
+
  if (route.resource === "inbox") {
+
    return "/inbox";
+
  } else if (route.resource === "guide") {
+
    return "/guide";
  } else if (
    route.resource === "repo.home" ||
-
    route.resource === "repo.createIssue" ||
    route.resource === "repo.issue" ||
    route.resource === "repo.issues" ||
    route.resource === "repo.patch" ||
modified src/lib/router/definitions.ts
@@ -1,9 +1,9 @@
-
import type { HomeRoute, LoadedHomeRoute } from "@app/views/home/router";
import type { LoadedRepoRoute, RepoRoute } from "@app/views/repo/router";
+
import type { NotificationsByRepo } from "@bindings/cob/inbox/NotificationsByRepo";
+
import type { Config } from "@bindings/config/Config";
+
import type { RepoSummary } from "@bindings/repo/RepoSummary";

-
import { loadHome } from "@app/views/home/router";
import {
-
  loadCreateIssue,
  loadIssue,
  loadIssues,
  loadPatch,
@@ -11,25 +11,88 @@ import {
  loadRepoHome,
} from "@app/views/repo/router";

+
import { invoke } from "@app/lib/invoke";
+

interface BootingRoute {
  resource: "booting";
}

-
export type Route = BootingRoute | HomeRoute | RepoRoute;
-
export type LoadedRoute = BootingRoute | LoadedHomeRoute | LoadedRepoRoute;
+
interface InboxRoute {
+
  resource: "inbox";
+
}
+

+
interface GuideRoute {
+
  resource: "guide";
+
}
+

+
export interface SidebarData {
+
  config: Config;
+
  repos: RepoSummary[];
+
  notificationCount: number;
+
  seededNotReplicated: string[];
+
}
+

+
export interface LoadedInboxRoute {
+
  resource: "inbox";
+
  params: {
+
    sidebarData: SidebarData;
+
    notificationsByRepo: NotificationsByRepo[];
+
  };
+
}
+

+
export interface LoadedGuideRoute {
+
  resource: "guide";
+
  params: { sidebarData: SidebarData };
+
}
+

+
export type Route = BootingRoute | RepoRoute | InboxRoute | GuideRoute;
+
export type LoadedRoute =
+
  | BootingRoute
+
  | LoadedRepoRoute
+
  | LoadedInboxRoute
+
  | LoadedGuideRoute;
+

+
export async function loadSidebarData(): Promise<SidebarData> {
+
  const [config, repos, notificationCount, seededNotReplicated] =
+
    await Promise.all([
+
      invoke<Config>("config"),
+
      invoke<RepoSummary[]>("list_repos_summary"),
+
      invoke<number>("notification_count"),
+
      invoke<string[]>("seeded_not_replicated"),
+
    ]);
+
  return { config, repos, notificationCount, seededNotReplicated };
+
}
+

+
export async function loadGuide(): Promise<LoadedGuideRoute> {
+
  const sidebarData = await loadSidebarData();
+
  return { resource: "guide", params: { sidebarData } };
+
}
+

+
export async function loadInbox(): Promise<LoadedInboxRoute> {
+
  const [sidebarData, notificationsByRepo] = await Promise.all([
+
    loadSidebarData(),
+
    invoke<NotificationsByRepo[]>("list_notifications", {
+
      params: { take: 100 },
+
    }),
+
  ]);
+
  return {
+
    resource: "inbox",
+
    params: { sidebarData, notificationsByRepo },
+
  };
+
}

export async function loadRoute(
  route: Route,
  _previousLoaded: LoadedRoute,
): Promise<LoadedRoute> {
-
  if (route.resource === "home") {
-
    return loadHome(route);
+
  if (route.resource === "inbox") {
+
    return loadInbox();
+
  } else if (route.resource === "guide") {
+
    return loadGuide();
  } else if (route.resource === "repo.home") {
    return loadRepoHome(route);
  } else if (route.resource === "repo.issue") {
    return loadIssue(route);
-
  } else if (route.resource === "repo.createIssue") {
-
    return loadCreateIssue(route);
  } else if (route.resource === "repo.issues") {
    return loadIssues(route);
  } else if (route.resource === "repo.patch") {
modified src/lib/utils.ts
@@ -146,33 +146,33 @@ export function scrollIntoView(id: string, options?: ScrollIntoViewOptions) {
}

export const issueStatusColor: Record<Issue["state"]["status"], string> = {
-
  open: "var(--color-fill-success)",
-
  closed: "var(--color-foreground-red)",
+
  open: "var(--color-text-open)",
+
  closed: "var(--color-text-closed)",
};

export const issueStatusBackgroundColor: Record<
  Issue["state"]["status"],
  string
> = {
-
  open: "var(--color-fill-diff-green)",
-
  closed: "var(--color-fill-diff-red)",
+
  open: "var(--color-surface-open)",
+
  closed: "var(--color-surface-closed)",
};

export const patchStatusColor: Record<Patch["state"]["status"], string> = {
-
  draft: "var(--color-fill-gray)",
-
  open: "var(--color-fill-success)",
-
  archived: "var(--color-foreground-yellow)",
-
  merged: "var(--color-fill-primary)",
+
  draft: "var(--color-text-draft)",
+
  open: "var(--color-text-open)",
+
  archived: "var(--color-text-archived)",
+
  merged: "var(--color-text-merged)",
};

export const patchStatusBackgroundColor: Record<
  Patch["state"]["status"],
  string
> = {
-
  draft: "var(--color-fill-ghost)",
-
  open: "var(--color-fill-diff-green)",
-
  archived: "var(--color-fill-private)",
-
  merged: "var(--color-fill-delegate)",
+
  draft: "var(--color-surface-draft)",
+
  open: "var(--color-surface-open)",
+
  archived: "var(--color-surface-archived)",
+
  merged: "var(--color-surface-merged)",
};

export function authorForNodeId(author: Author): ComponentProps<typeof NodeId> {
@@ -240,11 +240,11 @@ export function gravatarURL(email: string): string {

export function verdictIcon(verdict: Review["verdict"]) {
  if (verdict === "accept") {
-
    return "thumb-up";
+
    return "thumbs-up";
  } else if (verdict === "reject") {
    return "stop";
  } else {
-
    return "review";
+
    return "comment";
  }
}

added src/modals/CreateIssue.svelte
@@ -0,0 +1,224 @@
+
<script lang="ts">
+
  import type { Author } from "@bindings/cob/Author";
+
  import type { Issue } from "@bindings/cob/issue/Issue";
+
  import type { Embed } from "@bindings/cob/thread/Embed";
+
  import type { Config } from "@bindings/config/Config";
+
  import type { RepoInfo } from "@bindings/repo/RepoInfo";
+

+
  import { nodeRunning } from "@app/lib/events";
+
  import { invoke } from "@app/lib/invoke";
+
  import { disableHide, enableHide, forceHide, hide } from "@app/lib/modal";
+
  import * as roles from "@app/lib/roles";
+
  import * as router from "@app/lib/router";
+

+
  import { announce } from "@app/components/AnnounceSwitch.svelte";
+
  import AssigneeInput from "@app/components/AssigneeInput.svelte";
+
  import Button from "@app/components/Button.svelte";
+
  import ExtendedTextarea from "@app/components/ExtendedTextarea.svelte";
+
  import Icon from "@app/components/Icon.svelte";
+
  import LabelInput from "@app/components/LabelInput.svelte";
+
  import RepoAvatar from "@app/components/RepoAvatar.svelte";
+
  import TextInput from "@app/components/TextInput.svelte";
+

+
  interface Props {
+
    repo: RepoInfo;
+
  }
+

+
  const { repo }: Props = $props();
+

+
  let preview = $state(false);
+
  let title = $state("");
+
  let body = $state("");
+
  let assignees: Author[] = $state([]);
+
  let labels: string[] = $state([]);
+

+
  $effect(() => {
+
    const isDirty =
+
      title.trim() !== "" ||
+
      body.trim() !== "" ||
+
      labels.length > 0 ||
+
      assignees.length > 0;
+
    if (isDirty) {
+
      disableHide();
+
    } else {
+
      enableHide();
+
    }
+
  });
+

+
  const configPromise = invoke<Config>("config");
+

+
  async function createIssue(description: string, embeds: Embed[]) {
+
    return invoke<Issue>("create_issue", {
+
      rid: repo.rid,
+
      new: {
+
        title,
+
        description,
+
        labels: $state.snapshot(labels),
+
        assignees: $state.snapshot(assignees.map(a => a.did)),
+
        embeds,
+
      },
+
      opts: { announce: $nodeRunning && $announce },
+
    });
+
  }
+
</script>
+

+
<style>
+
  .modal {
+
    width: 56rem;
+
    display: flex;
+
    flex-direction: column;
+
    border: 1px solid var(--color-border-subtle);
+
    border-radius: var(--border-radius-lg);
+
    background-color: var(--color-surface-canvas);
+
    overflow: hidden;
+
  }
+
  .header {
+
    display: flex;
+
    align-items: center;
+
    justify-content: space-between;
+
    padding: 0 1.5rem;
+
    height: 3.25rem;
+
    flex-shrink: 0;
+
    border-bottom: 1px solid var(--color-border-subtle);
+
  }
+
  .header-left {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.5rem;
+
    font: var(--txt-body-m-regular);
+
    color: var(--color-text-secondary);
+
    min-width: 0;
+
  }
+
  .repo-name {
+
    font: var(--txt-body-m-semibold);
+
    color: var(--color-text-secondary);
+
    white-space: nowrap;
+
    overflow: hidden;
+
    text-overflow: ellipsis;
+
  }
+
  .title {
+
    font: var(--txt-body-m-regular);
+
    color: var(--color-text-primary);
+
    white-space: nowrap;
+
  }
+
  .body {
+
    padding: 1.5rem;
+
    display: flex;
+
    flex-direction: column;
+
    gap: 1rem;
+
  }
+
  .title-preview {
+
    font: var(--txt-heading-s);
+
    color: var(--color-text-primary);
+
    padding: 0.5rem 0;
+
  }
+
  .metadata-section {
+
    padding: 0.5rem;
+
    font: var(--txt-body-m-regular);
+
    display: flex;
+
    flex-direction: column;
+
    align-items: flex-start;
+
    height: 100%;
+
  }
+
</style>
+

+
<div class="modal">
+
  <div class="header">
+
    <div class="header-left">
+
      <RepoAvatar
+
        name={repo.payloads["xyz.radicle.project"]?.data.name ?? ""}
+
        rid={repo.rid}
+
        styleWidth="1rem" />
+
      <span class="repo-name">
+
        {repo.payloads["xyz.radicle.project"]?.data.name}
+
      </span>
+
      <Icon name="chevron-right" />
+
      <span class="title">New issue</span>
+
    </div>
+
    <Button variant="naked" onclick={forceHide}>
+
      <span style:color="var(--color-text-tertiary)">
+
        <Icon name="close" />
+
      </span>
+
    </Button>
+
  </div>
+
  <div class="body">
+
    {#if preview}
+
      <div class="title-preview">
+
        {#if title.trim().length === 0}
+
          <span class="txt-missing">No title.</span>
+
        {:else}
+
          {title}
+
        {/if}
+
      </div>
+
    {:else}
+
      <TextInput
+
        placeholder="Title"
+
        autofocus
+
        onDismiss={hide}
+
        bind:value={title} />
+
    {/if}
+

+
    <ExtendedTextarea
+
      textAreaSize="fixed-height"
+
      disableSubmit={title.trim() === ""}
+
      disallowEmptyBody
+
      styleMinHeight="20rem"
+
      submitCaption="Save"
+
      hideDiscard
+
      close={hide}
+
      submit={async ({ comment, embeds }) => {
+
        try {
+
          const response = await createIssue(
+
            comment,
+
            Array.from(embeds.values()),
+
          );
+
          await router.push({
+
            resource: "repo.issue",
+
            rid: repo.rid,
+
            issue: response.id,
+
            status: "open",
+
          });
+
          forceHide();
+
        } catch {
+
          console.error("Not able to create issue.");
+
        }
+
      }}
+
      rid={repo.rid}
+
      bind:preview
+
      bind:body
+
      borderVariant="ghost"
+
      placeholder="Description">
+
      {#snippet belowTextarea()}
+
        {#await configPromise then config}
+
          {#if !!roles.isDelegate( config.publicKey, repo.delegates.map(d => d.did), )}
+
            <div
+
              style:display="flex"
+
              style:align-items="center"
+
              style:width="100%">
+
              <div class="metadata-section" style:flex="1">
+
                <LabelInput
+
                  allowedToEdit={true}
+
                  {preview}
+
                  {labels}
+
                  submitInProgress={false}
+
                  save={newLabels => {
+
                    labels = newLabels;
+
                  }} />
+
              </div>
+
              <div class="metadata-section" style:flex="1">
+
                <AssigneeInput
+
                  allowedToEdit={true}
+
                  {preview}
+
                  bind:assignees
+
                  submitInProgress={false}
+
                  save={newAssignees => {
+
                    assignees = newAssignees;
+
                  }} />
+
              </div>
+
            </div>
+
          {/if}
+
        {/await}
+
      {/snippet}
+
    </ExtendedTextarea>
+
  </div>
+
</div>
added src/modals/Guide.svelte
@@ -0,0 +1,156 @@
+
<script lang="ts">
+
  import { radicleInstalled } from "@app/lib/checkRadicleCLI.svelte";
+
  import { nodeRunning } from "@app/lib/events";
+
  import { show } from "@app/lib/modal";
+
  import type { SidebarData } from "@app/lib/router/definitions";
+
  import { sleep } from "@app/lib/sleep";
+
  import { didFromPublicKey, truncateDid } from "@app/lib/utils";
+

+
  import { addRepoPopoverToggleId } from "@app/components/AddRepoButton.svelte";
+
  import Command from "@app/components/Command.svelte";
+
  import CopyableId from "@app/components/CopyableId.svelte";
+
  import Icon from "@app/components/Icon.svelte";
+
  import NodeId from "@app/components/NodeId.svelte";
+
  import SettingsView from "@app/modals/Settings.svelte";
+
  import Layout from "@app/views/repo/Layout.svelte";
+

+
  interface Props {
+
    sidebarData: SidebarData;
+
  }
+

+
  const { sidebarData }: Props = $props();
+

+
  const config = $derived(sidebarData.config);
+
</script>
+

+
<style>
+
  .page {
+
    display: flex;
+
    flex-direction: column;
+
    height: 100%;
+
  }
+
  .hero {
+
    width: 100%;
+
    flex-shrink: 0;
+
    overflow: hidden;
+
    max-height: 18rem;
+
  }
+
  .hero img {
+
    width: 100%;
+
    height: 100%;
+
    object-fit: cover;
+
    object-position: center 77%;
+
    display: block;
+
    -webkit-user-drag: none;
+
    user-select: none;
+
  }
+
  .content {
+
    flex: 1;
+
    overflow-y: auto;
+
    display: grid;
+
    place-items: center;
+
  }
+
  .inner {
+
    width: 100%;
+
    max-width: 48rem;
+
    display: flex;
+
    flex-direction: column;
+
    gap: 0.75rem;
+
    padding: 1rem;
+
    width: min(100%, 48rem);
+
    margin-bottom: 3rem;
+
  }
+
  .heading {
+
    font: var(--txt-heading-l);
+
    color: var(--color-text-primary);
+
    margin-bottom: 0.25rem;
+
  }
+
  .body {
+
    font: var(--txt-body-l-regular);
+
    color: var(--color-text-primary);
+
  }
+
  button {
+
    text-decoration: underline;
+
    border: 0;
+
    color: var(--color-text-primary);
+
    margin: 0;
+
    padding: 0;
+
    background-color: transparent;
+
    cursor: pointer;
+
    font: var(--txt-body-l-regular);
+
  }
+
</style>
+

+
<Layout {sidebarData}>
+
  <div class="page">
+
    <div class="hero">
+
      <img src="/flower.png" alt="" />
+
    </div>
+
    <div class="content">
+
      <div class="inner">
+
        <div class="heading">Getting started</div>
+

+
        <div class="body">
+
          Hello <NodeId
+
            inline
+
            styleFont="var(--txt-body-l-regular)"
+
            publicKey={config.publicKey}
+
            alias={config.alias} />, your identity has been created and stored
+
          on your machine.
+
        </div>
+
        <div class="body">
+
          Your public key is <CopyableId
+
            inline
+
            styleFont="var(--txt-body-l-regular)"
+
            id={didFromPublicKey(config.publicKey)}>
+
            {truncateDid(config.publicKey)}
+
          </CopyableId>
+
          you can share this with anyone to find you on the network.
+
        </div>
+
        <div class="body">
+
          We release a new version of the app every two weeks. To stay up to
+
          date, go to
+
          <button onclick={() => show({ component: SettingsView, props: {} })}>
+
            Settings
+
          </button>
+
          and enable 'Notify on new versions' to receive notifications about new
+
          releases.
+
        </div>
+

+
        {#if !radicleInstalled() && !$nodeRunning}
+
          <div class="body">
+
            <div class="global-flex" style:padding-bottom="1rem">
+
              <Icon name="warning" />Radicle CLI is not installed
+
            </div>
+
            <div style:padding-bottom="1rem">
+
              To interact with repos on the Radicle network, you'll need to
+
              install Radicle node along with its accompanying CLI tools. The
+
              node runs in the background, enabling seamless pushing and pulling
+
              of changes, while the CLI tools let you manage the node and
+
              provide interoperability between Git and Radicle.
+
            </div>
+
            <div style:padding-bottom="0.5rem">
+
              To install Radicle node and CLI tooling, run this in your shell:
+
            </div>
+
            <Command
+
              styleWidth="fit-content"
+
              command="curl -sSf https://radicle.xyz/install | sh" />
+
          </div>
+
        {/if}
+

+
        <div class="body">
+
          <!-- prettier-ignore -->
+
          To get started,
+

+
          <button
+
            onclick={async () => {
+
              await sleep(1);
+
              document.getElementById(addRepoPopoverToggleId)?.click();
+
            }}>
+
            try adding a repo!
+
          </button>
+
        </div>
+
      </div>
+
    </div>
+
  </div>
+
</Layout>
added src/modals/Settings.svelte
@@ -0,0 +1,148 @@
+
<script lang="ts">
+
  import { hide } from "@app/lib/modal";
+
  import { updateChecker } from "@app/lib/updateChecker.svelte";
+

+
  import AnnounceSwitch from "@app/components/AnnounceSwitch.svelte";
+
  import BadgeCounterSwitch from "@app/components/BadgeCounterSwitch.svelte";
+
  import Button from "@app/components/Button.svelte";
+
  import CodeFontSwitch from "@app/components/CodeFontSwitch.svelte";
+
  import ExternalLink from "@app/components/ExternalLink.svelte";
+
  import FontSizeSwitch from "@app/components/FontSizeSwitch.svelte";
+
  import Icon from "@app/components/Icon.svelte";
+
  import ThemeSwitch from "@app/components/ThemeSwitch.svelte";
+
  import UpdateSwitch from "@app/components/UpdateSwitch.svelte";
+
</script>
+

+
<style>
+
  .modal {
+
    width: 40rem;
+
    display: flex;
+
    flex-direction: column;
+
    border: 1px solid var(--color-border-subtle);
+
    border-radius: var(--border-radius-lg);
+
    background-color: var(--color-surface-canvas);
+
    overflow: hidden;
+
  }
+
  .header {
+
    display: flex;
+
    align-items: center;
+
    justify-content: space-between;
+
    padding: 0 1.5rem;
+
    height: 3.25rem;
+
    flex-shrink: 0;
+
    border-bottom: 1px solid var(--color-border-subtle);
+
  }
+
  .title {
+
    font: var(--txt-heading-s);
+
    color: var(--color-text-primary);
+
  }
+
  .rows {
+
    display: flex;
+
    flex-direction: column;
+
    padding: 1.5rem;
+
    gap: 1.5rem;
+
  }
+
  .row {
+
    display: flex;
+
    align-items: center;
+
    justify-content: space-between;
+
    gap: 1rem;
+
  }
+
  .row-label {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 0.125rem;
+
    min-width: 0;
+
  }
+
  .row-title {
+
    font: var(--txt-body-m-medium);
+
    color: var(--color-text-primary);
+
  }
+
  .row-description {
+
    font: var(--txt-body-m-regular);
+
    color: var(--color-text-secondary);
+
  }
+
  .footer {
+
    padding: 7rem 1.5rem 1.5rem;
+
    font: var(--txt-body-m-regular);
+
    color: var(--color-text-tertiary);
+
  }
+
</style>
+

+
<div class="modal">
+
  <div class="header">
+
    <span class="title">Settings</span>
+
    <Button variant="naked" onclick={hide}>
+
      <span style:color="var(--color-text-tertiary)">
+
        <Icon name="close" />
+
      </span>
+
    </Button>
+
  </div>
+
  <div class="rows">
+
    <div class="row">
+
      <div class="row-label">
+
        <span class="row-title">Appearance</span>
+
        <span class="row-description">Light, dark, or follow your system</span>
+
      </div>
+
      <ThemeSwitch />
+
    </div>
+
    <div class="row">
+
      <div class="row-label">
+
        <span class="row-title">Font size</span>
+
        <span class="row-description">
+
          Make the interface text larger or smaller
+
        </span>
+
      </div>
+
      <FontSizeSwitch />
+
    </div>
+
    <div class="row">
+
      <div class="row-label">
+
        <span class="row-title">Notification badge</span>
+
        <span class="row-description">Show unread count on the dock icon</span>
+
      </div>
+
      <BadgeCounterSwitch />
+
    </div>
+
    <div class="row">
+
      <div class="row-label">
+
        <span class="row-title">Announce changes</span>
+
        <span class="row-description">
+
          Broadcast your activity to the network right away or periodically
+
        </span>
+
      </div>
+
      <AnnounceSwitch />
+
    </div>
+
    <div class="row">
+
      <div class="row-label">
+
        <span class="row-title">Code font</span>
+
        <span class="row-description">Use a monospace font in code views</span>
+
      </div>
+
      <CodeFontSwitch />
+
    </div>
+
    <div class="row">
+
      <div class="row-label">
+
        <span class="row-title">Notify on new versions</span>
+
        <span class="row-description">
+
          Check for new versions in the background
+
        </span>
+
      </div>
+
      <UpdateSwitch
+
        active={updateChecker.isEnabled}
+
        disable={updateChecker.disable}
+
        enable={updateChecker.enable} />
+
    </div>
+
  </div>
+
  <div class="footer">
+
    {#if updateChecker.currentVersion}
+
      <span class="txt-selectable">
+
        Version {updateChecker.currentVersion}
+
      </span>
+
      {#if updateChecker.newVersion}
+
        · <ExternalLink href="https://radicle.xyz/desktop">
+
          Update to {updateChecker.newVersion}
+
        </ExternalLink>
+
      {:else}
+
        · Up to date
+
      {/if}
+
    {/if}
+
  </div>
+
</div>
added src/views/Inbox.svelte
@@ -0,0 +1,136 @@
+
<script lang="ts">
+
  import type { NotificationsByRepo } from "@bindings/cob/inbox/NotificationsByRepo";
+

+
  import { getCurrentWindow } from "@tauri-apps/api/window";
+

+
  import { invoke } from "@app/lib/invoke";
+
  import { notificationCount } from "@app/lib/notificationCount.svelte";
+
  import type { SidebarData } from "@app/lib/router/definitions";
+

+
  import { badgeCounter } from "@app/components/BadgeCounterSwitch.svelte";
+
  import InboxList from "@app/components/InboxList.svelte";
+
  import ScrollArea from "@app/components/ScrollArea.svelte";
+
  import Layout from "@app/views/repo/Layout.svelte";
+

+
  interface Props {
+
    sidebarData: SidebarData;
+
    notificationsByRepo: NotificationsByRepo[];
+
  }
+

+
  /* eslint-disable prefer-const */
+
  let { sidebarData, notificationsByRepo }: Props = $props();
+
  /* eslint-enable prefer-const */
+

+
  notificationCount.value = sidebarData.notificationCount;
+

+
  async function updateCount() {
+
    const count = await invoke<number>("notification_count");
+
    notificationCount.value = count;
+
    if (window.__TAURI_INTERNALS__ && $badgeCounter) {
+
      await getCurrentWindow().setBadgeCount(count === 0 ? undefined : count);
+
    } else if (window.__TAURI_INTERNALS__) {
+
      await getCurrentWindow().setBadgeCount(undefined);
+
    }
+
  }
+

+
  async function clearAll() {
+
    try {
+
      await invoke("clear_notifications", { params: { type: "all" } });
+
    } finally {
+
      await updateCount();
+
      await loadNotifications();
+
    }
+
  }
+

+
  async function clearByRepo(rid: string) {
+
    try {
+
      await invoke("clear_notifications", {
+
        params: { type: "repo", content: rid },
+
      });
+
    } finally {
+
      await updateCount();
+
      await loadNotifications();
+
    }
+
  }
+

+
  async function clearByIds(ids: string[]) {
+
    try {
+
      await invoke("clear_notifications", {
+
        params: { type: "ids", content: ids },
+
      });
+
    } finally {
+
      await updateCount();
+
      await loadNotifications();
+
    }
+
  }
+

+
  async function loadNotifications() {
+
    notificationsByRepo = await invoke<NotificationsByRepo[]>(
+
      "list_notifications",
+
      { params: { take: 100 } },
+
    );
+
  }
+

+
  async function showAll(rid: string) {
+
    const all = await invoke<NotificationsByRepo[]>("list_notifications", {
+
      params: { repos: [rid] },
+
    });
+
    notificationsByRepo = [
+
      ...notificationsByRepo.filter(r => r.rid !== rid),
+
      ...all,
+
    ];
+
  }
+
</script>
+

+
<style>
+
  .content {
+
    padding: 1rem;
+
    height: 100%;
+
    display: flex;
+
    flex-direction: column;
+
    box-sizing: border-box;
+
  }
+
  .inner {
+
    width: 100%;
+
    max-width: 75rem;
+
    margin: 0 auto;
+
    flex: 1;
+
    display: flex;
+
    flex-direction: column;
+
    min-height: 0;
+
  }
+
  .inbox-zero {
+
    display: flex;
+
    align-items: center;
+
    justify-content: center;
+
    height: 100%;
+
  }
+
</style>
+

+
<Layout {sidebarData} selfScroll>
+
  {#if notificationCount.value === 0}
+
    <div class="inbox-zero">
+
      <div
+
        class="txt-missing txt-body-m-regular global-flex"
+
        style:gap="0.25rem">
+
        You're all caught up
+
      </div>
+
    </div>
+
  {:else}
+
    <ScrollArea
+
      style="height: 100%; width: 100%; background-color: var(--color-surface-canvas);">
+
      <div class="content">
+
        <div class="inner">
+
          <InboxList
+
            {clearAll}
+
            {clearByIds}
+
            {clearByRepo}
+
            loadNew={loadNotifications}
+
            notificationCount={notificationCount.value}
+
            {notificationsByRepo}
+
            {showAll} />
+
        </div>
+
      </div>
+
    </ScrollArea>
+
  {/if}
+
</Layout>
added src/views/auth/Auth.svelte
@@ -0,0 +1,145 @@
+
<script lang="ts">
+
  import type { Author } from "@bindings/cob/Author";
+
  import type { ErrorWrapper } from "@bindings/error/ErrorWrapper";
+

+
  import { invoke } from "@app/lib/invoke";
+
  import * as router from "@app/lib/router";
+
  import {
+
    createEventEmittersOnce,
+
    setUnlistenNodeEvents,
+
  } from "@app/lib/startup.svelte";
+
  import { truncateDid } from "@app/lib/utils";
+

+
  import Button from "@app/components/Button.svelte";
+
  import Icon from "@app/components/Icon.svelte";
+
  import Spinner from "@app/components/Spinner.svelte";
+
  import TextInput from "@app/components/TextInput.svelte";
+
  import Layout from "@app/views/auth/Layout.svelte";
+

+
  interface Props {
+
    profile: Author;
+
  }
+

+
  let error = $state<ErrorWrapper>();
+
  let passphrase = $state("");
+
  let authInProgress = $state(false);
+
  const { profile }: Props = $props();
+

+
  async function authenticate() {
+
    if (passphrase.length > 0) {
+
      authInProgress = true;
+
      error = undefined;
+
      try {
+
        await invoke("authenticate", { passphrase });
+
        if (window.__TAURI_INTERNALS__) {
+
          setUnlistenNodeEvents(await createEventEmittersOnce());
+
        }
+
        passphrase = " ".repeat(passphrase.length);
+
        await router.push({ resource: "inbox" });
+
      } catch (err) {
+
        error = err as ErrorWrapper;
+
      } finally {
+
        authInProgress = false;
+
      }
+
    }
+
  }
+
</script>
+

+
<style>
+
  .header {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 0.375rem;
+
    align-items: center;
+
    text-align: center;
+
  }
+
  .form {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 1rem;
+
  }
+
  .field {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 0.375rem;
+
  }
+
  .label {
+
    font: var(--txt-body-s-semibold);
+
    color: var(--color-text-secondary);
+
  }
+
  .profile-card {
+
    border: 1px solid var(--color-border-subtle);
+
    border-radius: var(--border-radius-sm);
+
    background-color: var(--color-surface-canvas);
+
  }
+
  .profile-row {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 0.125rem;
+
    padding: 0.5rem 0.75rem;
+
  }
+
  .profile-row + .profile-row {
+
    border-top: 1px solid var(--color-border-subtle);
+
  }
+
  .hint {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.25rem;
+
    padding: 0.125rem 0 0 0.125rem;
+
  }
+
</style>
+

+
<Layout>
+
  <div class="header">
+
    <div class="txt-heading-l">Unlock keys</div>
+
    <div class="txt-body-m-regular" style:color="var(--color-text-secondary)">
+
      Your passphrase is needed to unlock your keys.
+
    </div>
+
  </div>
+

+
  <div class="form">
+
    <div class="profile-card">
+
      <div class="profile-row">
+
        <div class="label">Alias</div>
+
        <div class="txt-body-m-regular">{profile.alias}</div>
+
      </div>
+
      <div class="profile-row">
+
        <div class="label">DID</div>
+
        <div class="txt-body-m-regular">{truncateDid(profile.did)}</div>
+
      </div>
+
    </div>
+

+
    <div class="field">
+
      <div class="label">Passphrase</div>
+
      <TextInput
+
        autofocus
+
        onSubmit={authenticate}
+
        oninput={() => {
+
          error = undefined;
+
        }}
+
        placeholder="Enter passphrase to unlock your keys"
+
        type="password"
+
        bind:value={passphrase} />
+
      {#if error?.code === "PassphraseError.InvalidPassphrase"}
+
        <div
+
          style:color="var(--color-feedback-error-text)"
+
          class="hint txt-body-s-regular">
+
          <Icon name="warning" />
+
          <span>Not able to decrypt keys with provided passphrase.</span>
+
        </div>
+
      {/if}
+
    </div>
+

+
    <Button
+
      disabled={authInProgress || passphrase.length === 0}
+
      variant="secondary"
+
      onclick={authenticate}
+
      styleWidth="100%">
+
      {#if authInProgress}
+
        <Spinner /> Unlocking…
+
      {:else}
+
        <Icon name="lock" /> Unlock
+
      {/if}
+
    </Button>
+
  </div>
+
</Layout>
added src/views/auth/CreateIdentity.svelte
@@ -0,0 +1,218 @@
+
<script lang="ts">
+
  import type { ErrorWrapper } from "@bindings/error/ErrorWrapper";
+

+
  import debounce from "lodash/debounce";
+

+
  import { invoke } from "@app/lib/invoke";
+
  import * as router from "@app/lib/router";
+
  import { createEventEmittersOnce } from "@app/lib/startup.svelte";
+

+
  import Button from "@app/components/Button.svelte";
+
  import Icon from "@app/components/Icon.svelte";
+
  import TextInput from "@app/components/TextInput.svelte";
+
  import Layout from "@app/views/auth/Layout.svelte";
+

+
  let passphrase = $state("");
+
  let notMatchingPassphrases = $state<boolean>();
+
  let passphraseRepeat = $state("");
+
  let alias = $state("");
+
  const errors: { alias: ErrorWrapper[]; passphrase: ErrorWrapper[] } = {
+
    alias: [],
+
    passphrase: [],
+
  };
+

+
  const validatePassphraseRepeat = debounce(() => {
+
    if (passphrase !== passphraseRepeat && passphraseRepeat.length !== 0) {
+
      notMatchingPassphrases = true;
+
    }
+
  }, 400);
+

+
  function validateInput(field: "alias" | "passphrase") {
+
    if (field === "alias" && alias.length === 0) {
+
      errors.alias.push({ code: "AliasError.EmptyAlias" });
+
    }
+
    if (field === "alias" && alias.length > 32) {
+
      errors.alias.push({ code: "AliasError.TooLongAlias" });
+
    }
+
    if (field === "alias" && alias.includes(" ")) {
+
      errors.alias.push({ code: "AliasError.InvalidAlias" });
+
    }
+
    if (field === "passphrase" && passphrase.length === 0) {
+
      errors.passphrase.push({ code: "PassphraseError.InvalidPassphrase" });
+
    }
+
  }
+

+
  const validAlias = $derived(
+
    alias.length > 0 && alias.length <= 32 && !alias.includes(" "),
+
  );
+
  const validPassphrase = $derived(
+
    passphrase.length > 0 && passphrase === passphraseRepeat,
+
  );
+

+
  async function handleKeydown() {
+
    if (passphrase !== passphraseRepeat) {
+
      notMatchingPassphrases = true;
+

+
      return;
+
    }
+
    try {
+
      await invoke("init", { passphrase, alias });
+
      await invoke("startup");
+
      await invoke("authenticate", { passphrase });
+
      // Clearing the passphrases from memory.
+
      passphrase = "";
+
      passphraseRepeat = "";
+

+
      if (window.__TAURI_INTERNALS__) {
+
        await createEventEmittersOnce();
+
      }
+

+
      await router.push({ resource: "guide" });
+
    } catch (err) {
+
      const e = err as ErrorWrapper;
+
      if (e.code.startsWith("AliasError")) {
+
        errors.alias = [e];
+
      } else if (e.code.startsWith("PassphraseError")) {
+
        errors.passphrase = [e];
+
      }
+
      console.error(err);
+
    }
+

+
    return;
+
  }
+
</script>
+

+
<style>
+
  .header {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 0.375rem;
+
    align-items: center;
+
    text-align: center;
+
  }
+
  .form {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 1rem;
+
  }
+
  .field {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 0.375rem;
+
  }
+
  .label {
+
    font: var(--txt-body-s-semibold);
+
    color: var(--color-text-secondary);
+
  }
+
  .hint {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.25rem;
+
    padding: 0.125rem 0 0 0.125rem;
+
  }
+
</style>
+

+
<Layout>
+
  <div class="header">
+
    <div class="txt-heading-l">Create identity</div>
+
    <div class="txt-body-m-regular" style:color="var(--color-text-secondary)">
+
      Set up your Radicle identity to get started.
+
    </div>
+
  </div>
+

+
  <div class="form">
+
    <div class="field">
+
      <div class="label">Alias</div>
+
      <TextInput
+
        autofocus
+
        onSubmit={handleKeydown}
+
        oninput={() => {
+
          errors.alias = [];
+
          if (alias.length > 0) {
+
            validateInput("alias");
+
          }
+
        }}
+
        placeholder="Enter desired alias"
+
        type="text"
+
        bind:value={alias} />
+
      {#if errors.alias.some(e => e.code.startsWith("AliasError"))}
+
        {#each errors.alias as error}
+
          <div
+
            style:color="var(--color-feedback-error-text)"
+
            class="hint txt-body-s-regular">
+
            <Icon name="warning" />
+
            {#if error.code === "AliasError.EmptyAlias"}
+
              <span>Alias cannot be empty.</span>
+
            {:else if error.code === "AliasError.TooLongAlias"}
+
              <span>Alias is too long, max 32 characters.</span>
+
            {:else if error.code === "AliasError.InvalidAlias"}
+
              <span>Alias cannot contain whitespace.</span>
+
            {/if}
+
          </div>
+
        {/each}
+
      {:else}
+
        <div class="hint txt-body-s-regular txt-missing">
+
          <Icon name="guide" /> Max 32 characters, no whitespace.
+
        </div>
+
      {/if}
+
    </div>
+

+
    <div class="field">
+
      <div class="label">Passphrase</div>
+
      <TextInput
+
        onSubmit={handleKeydown}
+
        oninput={() => {
+
          errors.passphrase = [];
+
          notMatchingPassphrases = false;
+
          if (passphrase.length > 0) {
+
            validateInput("passphrase");
+
          }
+
        }}
+
        placeholder="Enter passphrase to protect your keys"
+
        type="password"
+
        bind:value={passphrase} />
+
      {#if errors.passphrase.some(e => e.code.startsWith("PassphraseError"))}
+
        {#each errors.passphrase as error}
+
          <div
+
            style:color="var(--color-feedback-error-text)"
+
            class="hint txt-body-s-regular">
+
            <Icon name="warning" />
+
            {#if error.code === "PassphraseError.InvalidPassphrase"}
+
              <span>Passphrase cannot be empty.</span>
+
            {:else}
+
              <span>{error.message}</span>
+
            {/if}
+
          </div>
+
        {/each}
+
      {/if}
+
    </div>
+

+
    <div class="field">
+
      <TextInput
+
        onSubmit={handleKeydown}
+
        oninput={() => {
+
          errors.passphrase = [];
+
          notMatchingPassphrases = false;
+
          validatePassphraseRepeat();
+
        }}
+
        placeholder="Repeat passphrase"
+
        type="password"
+
        bind:value={passphraseRepeat} />
+
      {#if notMatchingPassphrases}
+
        <div
+
          style:color="var(--color-feedback-error-text)"
+
          class="hint txt-body-s-regular">
+
          <Icon name="warning" /> Passphrases don't match.
+
        </div>
+
      {/if}
+
    </div>
+

+
    <Button
+
      disabled={!(validAlias && validPassphrase)}
+
      variant="secondary"
+
      onclick={handleKeydown}
+
      styleWidth="100%">
+
      <Icon name="seed" />Create new identity
+
    </Button>
+
  </div>
+
</Layout>
added src/views/auth/Layout.svelte
@@ -0,0 +1,90 @@
+
<script lang="ts">
+
  import type { Snippet } from "svelte";
+

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

+
  interface Props {
+
    children: Snippet;
+
  }
+

+
  const { children }: Props = $props();
+
</script>
+

+
<style>
+
  .window {
+
    display: flex;
+
    flex-direction: column;
+
    height: 100vh;
+
  }
+
  .titlebar {
+
    height: 2.5rem;
+
    flex-shrink: 0;
+
    display: flex;
+
    align-items: center;
+
    justify-content: center;
+
    -webkit-app-region: drag;
+
  }
+
  .titlebar :global(svg) {
+
    height: 1rem;
+
    width: auto;
+
    pointer-events: none;
+
  }
+
  .layout {
+
    flex: 1;
+
    display: grid;
+
    grid-template-columns: 1fr 1fr;
+
    min-height: 0;
+
  }
+
  .hero {
+
    overflow: hidden;
+
  }
+
  .hero img {
+
    width: 100%;
+
    height: 100%;
+
    object-fit: cover;
+
    object-position: center 77%;
+
    display: block;
+
  }
+
  .panel {
+
    display: grid;
+
    place-items: center;
+
    padding: 2rem;
+
    overflow-y: auto;
+
  }
+
  .inner {
+
    width: 100%;
+
    max-width: 22rem;
+
    display: flex;
+
    flex-direction: column;
+
    gap: 1.5rem;
+
    margin-bottom: 2rem;
+
  }
+
  .logo {
+
    display: flex;
+
    justify-content: center;
+
  }
+
  .logo :global(svg) {
+
    width: 6rem;
+
    height: auto;
+
  }
+
</style>
+

+
<div class="window">
+
  <div class="titlebar" data-tauri-drag-region>
+
    <RadicleWordmark />
+
  </div>
+
  <div class="layout">
+
    <div class="hero">
+
      <img src="/flower.png" alt="" />
+
    </div>
+
    <div class="panel">
+
      <div class="inner">
+
        <div class="logo">
+
          <Icon name="logo" size="32" />
+
        </div>
+
        {@render children()}
+
      </div>
+
    </div>
+
  </div>
+
</div>
deleted src/views/booting/Auth.svelte
@@ -1,139 +0,0 @@
-
<script lang="ts">
-
  import type { Author } from "@bindings/cob/Author";
-
  import type { ErrorWrapper } from "@bindings/error/ErrorWrapper";
-

-
  import logo from "/radicle.svg?url";
-

-
  import { invoke } from "@app/lib/invoke";
-
  import * as router from "@app/lib/router";
-
  import {
-
    createEventEmittersOnce,
-
    setUnlistenNodeEvents,
-
  } from "@app/lib/startup.svelte";
-
  import { truncateDid } from "@app/lib/utils";
-

-
  import Border from "@app/components/Border.svelte";
-
  import Button from "@app/components/Button.svelte";
-
  import Icon from "@app/components/Icon.svelte";
-
  import Spinner from "@app/components/Spinner.svelte";
-
  import TextInput from "@app/components/TextInput.svelte";
-

-
  interface Props {
-
    profile: Author;
-
  }
-

-
  let error = $state<ErrorWrapper>();
-
  let passphrase = $state("");
-
  let authInProgress = $state(false);
-
  const { profile }: Props = $props();
-

-
  async function authenticate() {
-
    if (passphrase.length > 0) {
-
      authInProgress = true;
-
      error = undefined;
-
      try {
-
        await invoke("authenticate", { passphrase });
-
        if (window.__TAURI_INTERNALS__) {
-
          setUnlistenNodeEvents(await createEventEmittersOnce());
-
        }
-
        passphrase = " ".repeat(passphrase.length);
-
        await router.push({ resource: "home", activeTab: "all" });
-
      } catch (err) {
-
        error = err as ErrorWrapper;
-
      } finally {
-
        authInProgress = false;
-
      }
-
    }
-
  }
-
</script>
-

-
<style>
-
  .container {
-
    display: flex;
-
    flex-direction: column;
-
    align-items: center;
-
    gap: 1rem;
-
    text-align: center;
-
    padding-top: 10rem;
-
  }
-
  .logo {
-
    height: 3rem;
-
  }
-
  .text-center {
-
    text-align: center;
-
    margin: auto;
-
  }
-
  .form {
-
    display: flex;
-
    flex-direction: column;
-
    gap: 1.5rem;
-
    margin-top: 1.5rem;
-
    width: 23rem;
-
  }
-
  .label {
-
    margin-bottom: 0.5rem;
-
  }
-
  .hint {
-
    padding: 0.25rem 0 0 0.25rem;
-
    gap: 0.25rem;
-
  }
-
</style>
-

-
<div class="container">
-
  <img src={logo} alt="Radicle Space Invader" class="logo" />
-

-
  <div class="txt-medium txt-bold">Unlock keys</div>
-
  <div class="txt-small">Your passphrase is needed to unlock your keys.</div>
-
  <div class="form">
-
    <div>
-
      <Border stylePadding="0.5rem 0.75rem" variant="ghost" flatBottom={true}>
-
        <div style:text-align="left">
-
          <div class="label txt-tiny">Alias</div>
-
          <div>
-
            {profile.alias}
-
          </div>
-
        </div>
-
      </Border>
-
      <Border stylePadding="0.5rem 0.75rem" variant="ghost" flatTop={true}>
-
        <div style:text-align="left">
-
          <div class="label txt-tiny">DID</div>
-
          <div>
-
            {truncateDid(profile.did)}
-
          </div>
-
        </div>
-
      </Border>
-
    </div>
-
    <div style:text-align="left">
-
      <div class="label txt-tiny">Passphrase</div>
-
      <TextInput
-
        autofocus
-
        onSubmit={authenticate}
-
        oninput={() => {
-
          error = undefined;
-
        }}
-
        placeholder="Enter passphrase to unlock your keys"
-
        type="password"
-
        bind:value={passphrase} />
-
      {#if error?.code === "PassphraseError.InvalidPassphrase"}
-
        <div
-
          style="color: var(--color-foreground-red);"
-
          class="hint txt-small global-flex">
-
          <Icon name="warning" />
-
          <span>Not able to decrypt keys with provided passphrase.</span>
-
        </div>
-
      {/if}
-
    </div>
-
    <Button
-
      disabled={authInProgress || passphrase.length === 0}
-
      variant="secondary"
-
      onclick={authenticate}>
-
      <div class="global-flex text-center">
-
        {#if authInProgress}
-
          <Spinner /> Unlocking…
-
        {:else}
-
          <Icon name="lock" /> Unlock
-
        {/if}
-
      </div>
-
    </Button>
-
  </div>
-
</div>
deleted src/views/booting/CreateIdentity.svelte
@@ -1,220 +0,0 @@
-
<script lang="ts">
-
  import type { ErrorWrapper } from "@bindings/error/ErrorWrapper";
-

-
  import logo from "/radicle.svg?url";
-
  import debounce from "lodash/debounce";
-

-
  import { invoke } from "@app/lib/invoke";
-
  import * as router from "@app/lib/router";
-
  import { createEventEmittersOnce } from "@app/lib/startup.svelte";
-

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

-
  let passphrase = $state("");
-
  let notMatchingPassphrases = $state<boolean>();
-
  let passphraseRepeat = $state("");
-
  let alias = $state("");
-
  const errors: { alias: ErrorWrapper[]; passphrase: ErrorWrapper[] } = {
-
    alias: [],
-
    passphrase: [],
-
  };
-

-
  const validatePassphraseRepeat = debounce(() => {
-
    if (passphrase !== passphraseRepeat && passphraseRepeat.length !== 0) {
-
      notMatchingPassphrases = true;
-
    }
-
  }, 400);
-

-
  function validateInput(field: "alias" | "passphrase") {
-
    if (field === "alias" && alias.length === 0) {
-
      errors.alias.push({ code: "AliasError.EmptyAlias" });
-
    }
-
    if (field === "alias" && alias.length > 32) {
-
      errors.alias.push({ code: "AliasError.TooLongAlias" });
-
    }
-
    if (field === "alias" && alias.includes(" ")) {
-
      errors.alias.push({ code: "AliasError.InvalidAlias" });
-
    }
-
    if (field === "passphrase" && passphrase.length === 0) {
-
      errors.passphrase.push({ code: "PassphraseError.InvalidPassphrase" });
-
    }
-
  }
-

-
  const validAlias = $derived(
-
    alias.length > 0 && alias.length <= 32 && !alias.includes(" "),
-
  );
-
  const validPassphrase = $derived(
-
    passphrase.length > 0 && passphrase === passphraseRepeat,
-
  );
-

-
  async function handleKeydown() {
-
    if (passphrase !== passphraseRepeat) {
-
      notMatchingPassphrases = true;
-

-
      return;
-
    }
-
    try {
-
      await invoke("init", { passphrase, alias });
-
      await invoke("startup");
-
      await invoke("authenticate", { passphrase });
-
      // Clearing the passphrases from memory.
-
      passphrase = "";
-
      passphraseRepeat = "";
-

-
      if (window.__TAURI_INTERNALS__) {
-
        await createEventEmittersOnce();
-
      }
-

-
      void router.loadFromLocation();
-
    } catch (err) {
-
      const e = err as ErrorWrapper;
-
      if (e.code.startsWith("AliasError")) {
-
        errors.alias = [e];
-
      } else if (e.code.startsWith("PassphraseError")) {
-
        errors.passphrase = [e];
-
      }
-
      console.error(err);
-
    }
-

-
    return;
-
  }
-
</script>
-

-
<style>
-
  .container {
-
    display: flex;
-
    flex-direction: column;
-
    align-items: center;
-
    gap: 1rem;
-
    text-align: center;
-
    padding-top: 10rem;
-
  }
-
  .logo {
-
    height: 3rem;
-
  }
-
  .text-center {
-
    text-align: center;
-
    margin: auto;
-
  }
-
  .form {
-
    display: flex;
-
    flex-direction: column;
-
    gap: 1.5rem;
-
    margin-top: 1.5rem;
-
    width: 23rem;
-
  }
-
  .label {
-
    margin-bottom: 0.5rem;
-
  }
-
  .hint {
-
    padding: 0.25rem 0 0 0.25rem;
-
    gap: 0.25rem;
-
  }
-
</style>
-

-
<div class="container">
-
  <img src={logo} alt="Radicle Space Invader" class="logo" />
-

-
  <div class="txt-medium txt-bold">Log in to Radicle Desktop</div>
-
  <div class="txt-small">
-
    Create a Radicle identity in order to use the app.
-
  </div>
-

-
  <div class="form">
-
    <div style:text-align="left">
-
      <div class="label txt-tiny">Alias (required)</div>
-
      <TextInput
-
        autofocus
-
        onSubmit={handleKeydown}
-
        oninput={() => {
-
          errors.alias = [];
-
          if (alias.length > 0) {
-
            validateInput("alias");
-
          }
-
        }}
-
        placeholder="Enter desired alias"
-
        type="text"
-
        bind:value={alias}></TextInput>
-
      {#if errors.alias.some(e => e.code.startsWith("AliasError"))}
-
        {#each errors.alias as error}
-
          <div
-
            style="color: var(--color-foreground-red);"
-
            class="hint txt-small global-flex">
-
            <Icon name="warning" />
-
            {#if error.code === "AliasError.EmptyAlias"}
-
              <span>Alias cannot be empty.</span>
-
            {:else if error.code === "AliasError.TooLongAlias"}
-
              <span>Alias is too long, make it less than 32 characters.</span>
-
            {:else if error.code === "AliasError.InvalidAlias"}
-
              <span>Alias cannot contain whitespace.</span>
-
            {/if}
-
          </div>
-
        {/each}
-
      {:else}
-
        <div class="hint txt-small txt-missing global-flex">
-
          <Icon name="info" /> Max 32 characters, no whitespace.
-
        </div>
-
      {/if}
-
    </div>
-
    <div>
-
      <div style:text-align="left" style:margin-bottom="0.5rem">
-
        <div class="label txt-tiny">Passphrase (required)</div>
-
        <TextInput
-
          onSubmit={handleKeydown}
-
          oninput={() => {
-
            errors.passphrase = [];
-
            notMatchingPassphrases = false;
-
            if (passphrase.length > 0) {
-
              validateInput("passphrase");
-
            }
-
          }}
-
          placeholder="Enter passphrase to protect your keys"
-
          type="password"
-
          bind:value={passphrase}></TextInput>
-
        {#if errors.passphrase.some(e => e.code.startsWith("PassphraseError"))}
-
          {#each errors.passphrase as error}
-
            <div
-
              style="color: var(--color-foreground-red);"
-
              class="hint txt-small global-flex">
-
              <Icon name="warning" />
-
              {#if error.code === "PassphraseError.InvalidPassphrase"}
-
                <span>Passphrase cannot be empty.</span>
-
              {:else}
-
                <span>{error.message}</span>
-
              {/if}
-
            </div>
-
          {/each}
-
        {/if}
-
      </div>
-
      <div style:text-align="left">
-
        <TextInput
-
          onSubmit={handleKeydown}
-
          oninput={() => {
-
            errors.passphrase = [];
-
            notMatchingPassphrases = false;
-
            validatePassphraseRepeat();
-
          }}
-
          placeholder="Repeat passphrase"
-
          type="password"
-
          bind:value={passphraseRepeat}></TextInput>
-
        {#if notMatchingPassphrases}
-
          <div
-
            style="color: var(--color-foreground-red);"
-
            class="hint txt-small global-flex">
-
            <Icon name="warning" /> Passphrases don't match
-
          </div>
-
        {/if}
-
      </div>
-
    </div>
-
    <Button
-
      disabled={!(validAlias && validPassphrase)}
-
      variant="secondary"
-
      onclick={handleKeydown}>
-
      <div class="global-flex text-center">
-
        <Icon name="seedling" />Create new identity
-
      </div>
-
    </Button>
-
  </div>
-
</div>
deleted src/views/home/Repos.svelte
@@ -1,273 +0,0 @@
-
<script lang="ts">
-
  import type { HomeReposTab } from "@app/views/home/router";
-
  import type { Config } from "@bindings/config/Config";
-
  import type { ErrorWrapper } from "@bindings/error/ErrorWrapper";
-
  import type { RepoCount } from "@bindings/repo/RepoCount";
-
  import type { RepoInfo } from "@bindings/repo/RepoInfo";
-

-
  import fuzzysort from "fuzzysort";
-
  import { onMount } from "svelte";
-

-
  import { dynamicInterval } from "@app/lib/interval";
-
  import { invoke } from "@app/lib/invoke";
-
  import * as router from "@app/lib/router";
-
  import { sleep } from "@app/lib/sleep";
-
  import { didFromPublicKey, modifierKey } from "@app/lib/utils";
-

-
  import AddRepoButton from "@app/components/AddRepoButton.svelte";
-
  import Border from "@app/components/Border.svelte";
-
  import { guidePopoverToggleId } from "@app/components/GuideButton.svelte";
-
  import HomeSidebar from "@app/components/HomeSidebar.svelte";
-
  import Icon from "@app/components/Icon.svelte";
-
  import NakedButton from "@app/components/NakedButton.svelte";
-
  import NodeBreadcrumb from "@app/components/NodeBreadcrumb.svelte";
-
  import RepoCard from "@app/components/RepoCard.svelte";
-
  import RepoCardPlaceholder from "@app/components/RepoCardPlaceholder.svelte";
-
  import TextInput from "@app/components/TextInput.svelte";
-
  import Layout from "@app/views/repo/Layout.svelte";
-

-
  interface Props {
-
    activeTab: HomeReposTab;
-
    config: Config;
-
    notificationCount: number;
-
    repoCount: RepoCount;
-
    repos: RepoInfo[];
-
    seededNotReplicated: string[];
-
  }
-

-
  /* eslint-disable prefer-const */
-
  let {
-
    activeTab,
-
    config,
-
    notificationCount,
-
    repoCount,
-
    repos,
-
    seededNotReplicated,
-
  }: Props =
-
    /* eslint-enable prefer-const */
-
    $props();
-

-
  const startup = $state<{ error?: ErrorWrapper }>({ error: undefined });
-
  let showFilters: boolean = $state(false);
-
  let searchInput = $state("");
-

-
  let lock = false;
-

-
  async function reloadRepoList() {
-
    try {
-
      if (lock) {
-
        return;
-
      }
-
      if (seededNotReplicated.length === 0 && repoCount.total > 0) {
-
        return;
-
      }
-
      if (searchInput !== "") {
-
        return;
-
      }
-
      lock = true;
-
      await reload();
-
    } catch (err) {
-
      const error = err as ErrorWrapper;
-
      startup.error = error;
-
    } finally {
-
      lock = false;
-
    }
-
  }
-

-
  onMount(() => {
-
    dynamicInterval("repos", reloadRepoList, 5_000);
-
  });
-

-
  async function reload() {
-
    [repos, repoCount, config, seededNotReplicated] = await Promise.all([
-
      invoke<RepoInfo[]>("list_repos", { show: activeTab ?? "all" }),
-
      invoke<RepoCount>("repo_count"),
-
      invoke<Config>("config"),
-
      invoke<string[]>("seeded_not_replicated"),
-
    ]);
-
  }
-

-
  const searchableRepos = $derived(
-
    repos
-
      .flatMap(r => {
-
        if (r.payloads["xyz.radicle.project"]) {
-
          return {
-
            repo: r,
-
            name: r.payloads["xyz.radicle.project"].data.name,
-
            description: r.payloads["xyz.radicle.project"].data.description,
-
          };
-
        }
-
      })
-
      .filter((item): item is NonNullable<typeof item> => item !== undefined),
-
  );
-

-
  const searchResults = $derived(
-
    fuzzysort.go(searchInput, searchableRepos, {
-
      keys: ["name", "description", "repo.rid"],
-
      threshold: 0.5,
-
      all: true,
-
    }),
-
  );
-
</script>
-

-
<style>
-
  .container {
-
    padding: 1rem 1rem 1rem 0;
-
  }
-
  .repo-grid {
-
    display: grid;
-
    grid-template-columns: repeat(auto-fill, minmax(21rem, 1fr));
-
    gap: 1rem;
-
  }
-
  .header {
-
    font-weight: var(--font-weight-medium);
-
    font-size: var(--font-size-medium);
-
    display: flex;
-
    justify-content: space-between;
-
    padding-right: 0.325rem;
-
    align-items: center;
-
    min-height: 2.5rem;
-
  }
-
  button {
-
    text-decoration: underline;
-
    border: 0;
-
    color: var(--color-foreground-dim);
-
    margin: 0;
-
    padding: 0;
-
    background-color: transparent;
-
    cursor: pointer;
-
  }
-
</style>
-

-
<Layout
-
  {notificationCount}
-
  hideSidebar
-
  styleSecondColumnOverflow="visible"
-
  {config}>
-
  {#snippet breadcrumbs()}
-
    <NodeBreadcrumb {config} />
-
  {/snippet}
-

-
  {#snippet secondColumn()}
-
    <HomeSidebar {activeTab} {repoCount} />
-
  {/snippet}
-
  <div class="container">
-
    <div class="global-flex" style:margin-bottom="1rem">
-
      <div class="global-flex">
-
        <div class="header">Repositories</div>
-
        {#if repos.length > 0}
-
          {#if !showFilters}
-
            <NakedButton
-
              styleHeight="2.5rem"
-
              keyShortcuts="ctrl+f"
-
              variant="ghost"
-
              active={showFilters}
-
              onclick={() => {
-
                if (showFilters) {
-
                  showFilters = false;
-
                  searchInput = "";
-
                } else {
-
                  showFilters = true;
-
                }
-
              }}>
-
              <Icon name="filter" />
-
            </NakedButton>
-
          {/if}
-
          {#if showFilters}
-
            <TextInput
-
              autofocus
-
              onSubmit={async () => {
-
                if (searchResults.length === 1) {
-
                  await router.push({
-
                    resource: "repo.home",
-
                    rid: searchResults[0].obj.repo.rid,
-
                  });
-
                }
-
              }}
-
              onDismiss={() => {
-
                searchInput = "";
-
                showFilters = false;
-
              }}
-
              onBlur={() => {
-
                if (searchInput.trim() === "") {
-
                  showFilters = false;
-
                }
-
              }}
-
              placeholder={`Fuzzy filter repositories ${modifierKey()} + f`}
-
              keyShortcuts="ctrl+f"
-
              bind:value={searchInput}>
-
              {#snippet left()}
-
                <div style:padding-left="0.5rem">
-
                  <Icon name="filter" />
-
                </div>
-
              {/snippet}
-
            </TextInput>
-
          {/if}
-
        {/if}
-
      </div>
-
      <div class="global-flex" style:margin-left="auto">
-
        <AddRepoButton
-
          {repos}
-
          {reload}
-
          {seededNotReplicated}
-
          onOpen={() => {
-
            searchInput = "";
-
            showFilters = false;
-
          }} />
-
      </div>
-
    </div>
-
    {#if repoCount.total > 0 || seededNotReplicated.length > 0}
-
      {#if searchResults.length > 0 || seededNotReplicated.length > 0}
-
        <div class="repo-grid">
-
          {#if !showFilters}
-
            {#each seededNotReplicated as rid}
-
              <RepoCardPlaceholder {rid} {reload} />
-
            {/each}
-
          {/if}
-
          {#each searchResults as result}
-
            <RepoCard
-
              focussed={searchResults.length === 1 && searchInput !== ""}
-
              repo={result.obj.repo}
-
              selfDid={didFromPublicKey(config.publicKey)} />
-
          {/each}
-
        </div>
-
      {:else}
-
        <Border
-
          variant="ghost"
-
          styleAlignItems="center"
-
          styleJustifyContent="center">
-
          <div
-
            class="global-flex"
-
            style:height="7.875rem"
-
            style:justify-content="center">
-
            <div class="txt-missing txt-small global-flex" style:gap="0.25rem">
-
              <Icon name="none" />
-
              {#if repos.length > 0}
-
                No matching repositories.
-
              {:else}
-
                No repositories.
-
              {/if}
-
            </div>
-
          </div>
-
        </Border>
-
      {/if}
-
    {:else}
-
      <div class="txt-missing txt-small">
-
        You don't have any repositories in your Radicle storage yet.
-
      </div>
-
      <!-- prettier-ignore -->
-
      <div class="txt-missing txt-small">
-
        To get started, check out
-
        <button
-
          class="txt-small"
-
          onclick={async () => {
-
                const guidePopoverButton = document.getElementById(guidePopoverToggleId);
-
                await sleep(1);
-
                guidePopoverButton?.click();
-
          }}>
-
          the guide
-
        </button>.
-
      </div>
-
    {/if}
-
  </div>
-
</Layout>
deleted src/views/home/router.ts
@@ -1,86 +0,0 @@
-
import type { Config } from "@bindings/config/Config";
-
import type { RepoCount } from "@bindings/repo/RepoCount";
-
import type { RepoInfo } from "@bindings/repo/RepoInfo";
-

-
import z from "zod";
-

-
import { invoke } from "@app/lib/invoke";
-

-
export type HomeReposTab = "all" | "delegate" | "private" | "contributor";
-

-
const homeReposTabSchema = z.union([
-
  z.literal("all"),
-
  z.literal("delegate"),
-
  z.literal("private"),
-
  z.literal("contributor"),
-
]);
-

-
export interface HomeRoute {
-
  resource: "home";
-
  activeTab: HomeReposTab;
-
}
-

-
export interface LoadedHomeRoute {
-
  resource: "home";
-
  params: {
-
    activeTab: HomeReposTab;
-
    repoCount: RepoCount;
-
    repos: RepoInfo[];
-
    config: Config;
-
    notificationCount: number;
-
    seededNotReplicated: string[];
-
  };
-
}
-

-
export async function loadHome(route: HomeRoute): Promise<LoadedHomeRoute> {
-
  let show = "all";
-

-
  if (route.resource === "home") {
-
    if (route.activeTab === "delegate") {
-
      show = "delegate";
-
    } else if (route.activeTab === "contributor") {
-
      show = "contributor";
-
    } else if (route.activeTab === "private") {
-
      show = "private";
-
    }
-
  }
-

-
  const [config, repoCount, repos, notificationCount, seededNotReplicated] =
-
    await Promise.all([
-
      invoke<Config>("config"),
-
      invoke<RepoCount>("repo_count"),
-
      invoke<RepoInfo[]>("list_repos", { show }),
-
      invoke<number>("notification_count"),
-
      invoke<string[]>("seeded_not_replicated"),
-
    ]);
-
  return {
-
    resource: "home",
-
    params: {
-
      activeTab: route.activeTab,
-
      repoCount,
-
      repos,
-
      config,
-
      notificationCount,
-
      seededNotReplicated,
-
    },
-
  };
-
}
-

-
export function homeUrlToRoute(url: URL): HomeRoute | undefined {
-
  if (url.pathname === "/") {
-
    return {
-
      resource: "home",
-
      activeTab: homeReposTabSchema
-
        .catch(() => "all" as const)
-
        .parse(url.searchParams.get("tab")),
-
    };
-
  }
-
}
-

-
export function homeRouteToPath(route: HomeRoute): string {
-
  const searchParams = new URLSearchParams();
-
  if (route.activeTab !== "all") {
-
    searchParams.append("tab", route.activeTab);
-
  }
-
  return `/?${searchParams.toString()}`;
-
}
deleted src/views/repo/BreadcrumbCopyButton.svelte
@@ -1,98 +0,0 @@
-
<script lang="ts">
-
  import type { ComponentProps } from "svelte";
-

-
  import debounce from "lodash/debounce";
-

-
  import { writeToClipboard } from "@app/lib/invoke";
-

-
  import Border from "@app/components/Border.svelte";
-
  import DropdownListItem from "@app/components/DropdownListItem.svelte";
-
  import Icon from "@app/components/Icon.svelte";
-
  import { closeFocused } from "@app/components/Popover.svelte";
-
  import Popover from "@app/components/Popover.svelte";
-

-
  interface Props {
-
    icon: ComponentProps<typeof Icon>["name"];
-
    id: string;
-
    url: string;
-
    id2?: string;
-
    icon2?: ComponentProps<typeof Icon>["name"];
-
  }
-

-
  const { icon, icon2, id, id2, url }: Props = $props();
-

-
  let popoverExpanded: boolean = $state(false);
-
  let triggerIcon: ComponentProps<typeof Icon>["name"] = $state("copy");
-
  const restoreIcon = debounce(() => {
-
    triggerIcon = "copy";
-
  }, 1000);
-
</script>
-

-
<Popover
-
  popoverPositionLeft="0"
-
  popoverPositionTop="2rem"
-
  bind:expanded={popoverExpanded}>
-
  {#snippet toggle(onclick)}
-
    <Icon name={triggerIcon} {onclick} />
-
  {/snippet}
-

-
  {#snippet popover()}
-
    <Border variant="ghost">
-
      <div
-
        class="global-flex txt-monospace"
-
        style:flex-direction="column"
-
        style:align-items="flex-start">
-
        <DropdownListItem
-
          styleGap="0.5rem"
-
          selected={false}
-
          onclick={async () => {
-
            await writeToClipboard(id);
-
            triggerIcon = "checkmark";
-
            restoreIcon();
-
            closeFocused();
-
          }}
-
          styleWidth="100%">
-
          <div class="global-flex">
-
            <Icon name={icon} />
-
            {id}
-
            <Icon name="copy" />
-
          </div>
-
        </DropdownListItem>
-
        {#if id2 && icon2}
-
          <DropdownListItem
-
            styleGap="0.5rem"
-
            selected={false}
-
            onclick={async () => {
-
              await writeToClipboard(id2);
-
              triggerIcon = "checkmark";
-
              restoreIcon();
-
              closeFocused();
-
            }}
-
            styleWidth="100%">
-
            <div class="global-flex" style:width="100%">
-
              <Icon name={icon2} />
-
              {id2}
-
              <Icon name="copy" />
-
            </div>
-
          </DropdownListItem>
-
        {/if}
-
        <a
-
          style:text-decoration="none"
-
          style:width="100%"
-
          onclick={closeFocused}
-
          href={url}
-
          target="_blank">
-
          <DropdownListItem styleGap="0.5rem" selected={false}>
-
            <div class="global-flex" style:width="100%">
-
              <Icon name="seedling" />
-
              view on seed.radicle.garden
-
              <div style:margin-left="auto">
-
                <Icon name="open-external" />
-
              </div>
-
            </div>
-
          </DropdownListItem>
-
        </a>
-
      </div>
-
    </Border>
-
  {/snippet}
-
</Popover>
deleted src/views/repo/CreateIssue.svelte
@@ -1,219 +0,0 @@
-
<script lang="ts">
-
  import type { IssueStatus } from "./router";
-
  import type { Author } from "@bindings/cob/Author";
-
  import type { Issue } from "@bindings/cob/issue/Issue";
-
  import type { Embed } from "@bindings/cob/thread/Embed";
-
  import type { Config } from "@bindings/config/Config";
-
  import type { RepoInfo } from "@bindings/repo/RepoInfo";
-

-
  import { nodeRunning } from "@app/lib/events";
-
  import { invoke } from "@app/lib/invoke";
-
  import * as roles from "@app/lib/roles";
-
  import * as router from "@app/lib/router";
-

-
  import { announce } from "@app/components/AnnounceSwitch.svelte";
-
  import AssigneeInput from "@app/components/AssigneeInput.svelte";
-
  import Border from "@app/components/Border.svelte";
-
  import ExtendedTextarea from "@app/components/ExtendedTextarea.svelte";
-
  import Icon from "@app/components/Icon.svelte";
-
  import InlineTitle from "@app/components/InlineTitle.svelte";
-
  import IssueSecondColumn from "@app/components/IssueSecondColumn.svelte";
-
  import LabelInput from "@app/components/LabelInput.svelte";
-
  import Sidebar from "@app/components/Sidebar.svelte";
-
  import TextInput from "@app/components/TextInput.svelte";
-

-
  import IssuesBreadcrumb from "./IssuesBreadcrumb.svelte";
-
  import Layout from "./Layout.svelte";
-
  import RepoBreadcrumb from "./RepoBreadcrumb.svelte";
-

-
  interface Props {
-
    repo: RepoInfo;
-
    issues: Issue[];
-
    config: Config;
-
    status: IssueStatus;
-
    notificationCount: number;
-
  }
-

-
  const {
-
    repo,
-
    issues: initialIssues,
-
    config,
-
    status: initialStatus,
-
    notificationCount,
-
  }: Props = $props();
-

-
  let preview: boolean = $state(false);
-
  let title: string = $state("");
-
  let status = $state(initialStatus);
-
  let issues = $state(initialIssues);
-

-
  let assignees: Author[] = $state([]);
-
  let labels: string[] = $state([]);
-

-
  const project = $derived(repo.payloads["xyz.radicle.project"]!);
-

-
  async function loadIssues(filter: IssueStatus) {
-
    try {
-
      issues = await invoke<Issue[]>("list_issues", {
-
        rid: repo.rid,
-
        status: filter,
-
      });
-
      status = filter;
-
    } catch (error) {
-
      console.error("Loading issue list failed", error);
-
    }
-
  }
-

-
  async function createIssue(
-
    description: string,
-
    embeds: Embed[],
-
  ): Promise<Issue> {
-
    return invoke("create_issue", {
-
      rid: repo.rid,
-
      new: {
-
        title,
-
        description,
-
        labels: $state.snapshot(labels),
-
        assignees: $state.snapshot(assignees.map(a => a.did)),
-
        embeds,
-
      },
-
      opts: { announce: $nodeRunning && $announce },
-
    });
-
  }
-
</script>
-

-
<style>
-
  .title {
-
    font-size: var(--font-size-medium);
-
    font-weight: var(--font-weight-medium);
-
    -webkit-user-select: text;
-
    user-select: text;
-
    display: flex;
-
    align-items: center;
-
    justify-content: space-between;
-
    word-break: break-word;
-
    min-height: 2.5rem;
-
    margin-bottom: 1rem;
-
  }
-
  .content {
-
    display: flex;
-
    flex-direction: column;
-
    min-height: 100%;
-
    padding: 1rem 1rem 1rem 0;
-
  }
-
  .metadata-divider {
-
    width: 2px;
-
    background-color: var(--color-fill-ghost);
-
    height: calc(100% + 4px);
-
    top: 0;
-
    position: relative;
-
  }
-
  .metadata-section {
-
    padding: 0.5rem;
-
    font-size: var(--font-size-small);
-
    display: flex;
-
    flex-direction: column;
-
    align-items: flex-start;
-
    height: 100%;
-
  }
-
</style>
-

-
<Layout {config} {notificationCount}>
-
  {#snippet breadcrumbs()}
-
    <RepoBreadcrumb name={project.data.name} rid={repo.rid} />
-
    <IssuesBreadcrumb rid={repo.rid} {status} />
-
    <Icon name="chevron-right" />
-
    New
-
  {/snippet}
-

-
  {#snippet sidebar()}
-
    <Sidebar activeTab="issues" rid={repo.rid} />
-
  {/snippet}
-

-
  {#snippet secondColumn()}
-
    <IssueSecondColumn
-
      {repo}
-
      {issues}
-
      {status}
-
      changeFilter={async filter => {
-
        await loadIssues(filter);
-
      }} />
-
  {/snippet}
-

-
  <div class="content">
-
    {#if preview}
-
      <div class="title">
-
        {#if title.trim().length === 0}
-
          <span class="txt-missing">No title</span>
-
        {:else}
-
          <InlineTitle content={title} fontSize="medium" />
-
        {/if}
-
      </div>
-
    {:else}
-
      <div style:margin-bottom="1rem">
-
        <TextInput placeholder="Title" autofocus bind:value={title} />
-
      </div>
-
    {/if}
-

-
    {#if !!roles.isDelegate( config.publicKey, repo.delegates.map(delegate => delegate.did), )}
-
      <div style:margin-bottom="1rem">
-
        <Border variant="ghost" styleGap="0">
-
          <div class="metadata-section" style:flex="1">
-
            <LabelInput
-
              allowedToEdit={!!roles.isDelegate(
-
                config.publicKey,
-
                repo.delegates.map(delegate => delegate.did),
-
              )}
-
              {labels}
-
              submitInProgress={false}
-
              save={newLabels => {
-
                labels = newLabels;
-
              }} />
-
          </div>
-

-
          <div class="metadata-divider"></div>
-

-
          <div class="metadata-section" style:flex="1">
-
            <AssigneeInput
-
              allowedToEdit={!!roles.isDelegate(
-
                config.publicKey,
-
                repo.delegates.map(delegate => delegate.did),
-
              )}
-
              bind:assignees
-
              submitInProgress={false}
-
              save={newAssignees => {
-
                assignees = newAssignees;
-
              }} />
-
          </div>
-
        </Border>
-
      </div>
-
    {/if}
-

-
    <ExtendedTextarea
-
      textAreaSize="fixed-height"
-
      disableSubmit={title.trim() === ""}
-
      disallowEmptyBody
-
      submitCaption="Save"
-
      close={() => window.history.back()}
-
      submit={async ({ comment, embeds }) => {
-
        try {
-
          const response = await createIssue(
-
            comment,
-
            Array.from(embeds.values()),
-
          );
-
          void router.push({
-
            resource: "repo.issue",
-
            rid: repo.rid,
-
            issue: response.id,
-
            status,
-
          });
-
        } catch {
-
          console.error("Not able to create issue.");
-
        }
-
      }}
-
      rid={repo.rid}
-
      bind:preview
-
      borderVariant="ghost"
-
      placeholder="Description" />
-
  </div>
-
</Layout>
modified src/views/repo/Issue.svelte
@@ -13,7 +13,10 @@

  import { nodeRunning } from "@app/lib/events";
  import { invoke } from "@app/lib/invoke";
+
  import { show } from "@app/lib/modal";
  import * as roles from "@app/lib/roles";
+
  import * as router from "@app/lib/router";
+
  import type { SidebarData } from "@app/lib/router/definitions";
  import {
    explorerUrl,
    issueStatusBackgroundColor,
@@ -23,26 +26,20 @@

  import { announce } from "@app/components/AnnounceSwitch.svelte";
  import AssigneeInput from "@app/components/AssigneeInput.svelte";
-
  import Border from "@app/components/Border.svelte";
+
  import Button from "@app/components/Button.svelte";
  import CommentComponent from "@app/components/Comment.svelte";
  import Discussion from "@app/components/Discussion.svelte";
-
  import DropdownListItem from "@app/components/DropdownListItem.svelte";
  import EditableTitle from "@app/components/EditableTitle.svelte";
+
  import ExternalLink from "@app/components/ExternalLink.svelte";
  import Icon from "@app/components/Icon.svelte";
-
  import InlineTitle from "@app/components/InlineTitle.svelte";
-
  import IssueSecondColumn from "@app/components/IssueSecondColumn.svelte";
+
  import Id from "@app/components/Id.svelte";
  import IssueStateButton from "@app/components/IssueStateButton.svelte";
  import IssueTimeline from "@app/components/IssueTimeline.svelte";
  import LabelInput from "@app/components/LabelInput.svelte";
-
  import MoreBreadcrumbsButton from "@app/components/MoreBreadcrumbsButton.svelte";
-
  import NakedButton from "@app/components/NakedButton.svelte";
-
  import NodeBreadcrumb from "@app/components/NodeBreadcrumb.svelte";
-
  import Sidebar from "@app/components/Sidebar.svelte";
+
  import ScrollArea from "@app/components/ScrollArea.svelte";
+
  import CreateIssueModal from "@app/modals/CreateIssue.svelte";

-
  import BreadcrumbCopyButton from "./BreadcrumbCopyButton.svelte";
-
  import IssuesBreadcrumb from "./IssuesBreadcrumb.svelte";
  import Layout from "./Layout.svelte";
-
  import RepoBreadcrumb from "./RepoBreadcrumb.svelte";

  interface Props {
    repo: RepoInfo;
@@ -52,7 +49,7 @@
    config: Config;
    threads: Thread[];
    status: IssueStatus;
-
    notificationCount: number;
+
    sidebarData: SidebarData;
  }

  /* eslint-disable prefer-const */
@@ -64,12 +61,12 @@
    config,
    threads,
    status: initialStatus,
-
    notificationCount,
+
    sidebarData,
  }: Props = $props();
  /* eslint-enable prefer-const */

  let issues = $state(initialIssues);
-
  let status = $state(initialStatus);
+
  const status = initialStatus;
  let labelSaveInProgress: boolean = $state(false);
  let assigneesSaveInProgress: boolean = $state(false);
  let hideTimeline = $state(true);
@@ -86,20 +83,6 @@
    hideTimeline = true;
  });

-
  const project = $derived(repo.payloads["xyz.radicle.project"]!);
-

-
  async function loadIssues(filter: IssueStatus) {
-
    try {
-
      issues = await invoke<Issue[]>("list_issues", {
-
        rid: repo.rid,
-
        status: filter,
-
      });
-
      status = filter;
-
    } catch (error) {
-
      console.error("Loading issue list failed", error);
-
    }
-
  }
-

  async function saveLabels(labels: string[]) {
    try {
      labelSaveInProgress = true;
@@ -280,207 +263,226 @@
</script>

<style>
-
  .status {
-
    padding: 0;
+
  .page {
+
    display: flex;
+
    flex-direction: column;
+
    height: 100%;
+
  }
+
  .topbar {
+
    display: flex;
+
    align-items: center;
+
    padding: 0 1rem;
    height: 2.5rem;
-
    width: 2.5rem;
+
    flex-shrink: 0;
+
    gap: 0.375rem;
+
    border-bottom: 1px solid var(--color-border-subtle);
+
    font: var(--txt-body-m-regular);
+
    color: var(--color-text-secondary);
  }
-
  .issue-body {
-
    margin: 1rem 0;
-
    position: relative;
+
  .topbar-link {
+
    cursor: pointer;
+
    background: none;
+
    border: none;
+
    padding: 0;
+
    font: var(--txt-body-m-regular);
+
    color: var(--color-text-secondary);
  }
-
  /* We put the background and clip-path in a separate element to prevent
-
     popovers being clipped in the main element. */
-
  .issue-body::after {
-
    position: absolute;
-
    z-index: -1;
-
    content: " ";
-
    background-color: var(--color-background-float);
-
    clip-path: var(--2px-corner-fill);
-
    width: 100%;
-
    height: 100%;
-
    top: 0;
+
  .topbar-link:hover {
+
    color: var(--color-text-primary);
  }
  .content {
-
    padding: 1rem 1rem 1rem 0;
+
    display: grid;
+
    grid-template-columns: 1fr 22rem;
+
  }
+
  .main {
+
    padding: 1.5rem 2rem;
+
    min-width: 0;
  }
-
  .metadata-divider {
-
    width: 2px;
-
    background-color: var(--color-fill-ghost);
-
    height: calc(100% + 4px);
-
    top: 0;
+
  .title {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.75rem;
+
    margin-bottom: 1rem;
+
  }
+
  .status-chip {
+
    padding: 0;
+
    height: 2rem;
+
    width: 2rem;
+
    flex-shrink: 0;
+
  }
+
  .issue-body {
+
    margin: 1rem 0;
    position: relative;
  }
-
  .metadata-section {
+
  .sidebar {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 0.5rem;
+
    border-left: 1px solid var(--color-border-subtle);
+
    height: 100%;
+
    padding: 1.5rem 1rem;
+
  }
+
  .sidebar-section {
    padding: 0.5rem;
-
    font-size: var(--font-size-small);
+
    font: var(--txt-body-m-regular);
    display: flex;
    flex-direction: column;
    align-items: flex-start;
-
    height: 100%;
  }
-
  .metadata-section-title {
-
    margin-bottom: 0.5rem;
-
    color: var(--color-foreground-dim);
+
  @media (max-width: 1349.98px) {
+
    .content {
+
      grid-template-columns: 1fr;
+
    }
+
    .sidebar {
+
      order: -1;
+
      border-left: none;
+
      border-bottom: 1px solid var(--color-border-subtle);
+
      flex-direction: row;
+
      align-items: flex-start;
+
    }
+
    .sidebar-section {
+
      flex: 1;
+
    }
  }
</style>

-
<Layout {config} {notificationCount}>
-
  {#snippet breadcrumbs()}
-
    <div
-
      class="global-flex global-hide-on-medium-desktop-down"
-
      style:gap="0.25rem">
-
      <NodeBreadcrumb {config} />
+
<Layout {sidebarData} activeRepo={repo}>
+
  <div class="page">
+
    <div class="topbar">
+
      <Icon name={issue.state.status === "open" ? "issue" : "issue-closed"} />
+
      <button
+
        class="topbar-link"
+
        onclick={() =>
+
          router.push({
+
            resource: "repo.issues",
+
            rid: repo.rid,
+
            status: "all",
+
          })}>
+
        All Issues
+
      </button>
      <Icon name="chevron-right" />
-
      <RepoBreadcrumb name={project.data.name} rid={repo.rid} />
-
      <Icon name="chevron-right" />
-
      <IssuesBreadcrumb rid={repo.rid} {status} />
-
      <Icon name="chevron-right" />
-
    </div>
-
    <div
-
      class="global-flex global-hide-on-desktop-up"
-
      style:gap="0.25rem"
-
      style:margin-right="0.5rem">
-
      <MoreBreadcrumbsButton>
-
        <DropdownListItem styleGap="0.5rem" selected={false} styleWidth="100%">
-
          <NodeBreadcrumb {config} />
-
        </DropdownListItem>
-
        <DropdownListItem styleGap="0.5rem" selected={false} styleWidth="100%">
-
          <Icon name="repo" />
-
          <RepoBreadcrumb name={project.data.name} rid={repo.rid} />
-
        </DropdownListItem>
-
        <DropdownListItem styleGap="0.5rem" selected={false} styleWidth="100%">
-
          <Icon name={status === "open" ? "issue" : "issue-closed"} />
-
          <IssuesBreadcrumb rid={repo.rid} {status} />
-
        </DropdownListItem>
-
      </MoreBreadcrumbsButton>
-
    </div>
-

-
    <span class="txt-overflow" style:max-width="16rem">
-
      <InlineTitle content={issue.title} fontSize="small" />
-
    </span>
-
    <BreadcrumbCopyButton
-
      url={explorerUrl(`${repo.rid}/issues/${issue.id}`)}
-
      icon={issue.state.status === "open" ? "issue" : "issue-closed"}
-
      id={issue.id} />
-
  {/snippet}
-

-
  {#snippet sidebar()}
-
    <Sidebar activeTab="issues" rid={repo.rid} />
-
  {/snippet}
-

-
  {#snippet secondColumn()}
-
    <IssueSecondColumn
-
      {repo}
-
      selectedIssueId={issue.id}
-
      {issues}
-
      {status}
-
      changeFilter={async filter => {
-
        await loadIssues(filter);
-
      }} />
-
  {/snippet}
-

-
  <div class="content">
-
    <div class="global-flex" style:margin-bottom="1rem" style:gap="0.75rem">
-
      <div
-
        class="global-counter status"
-
        style:color={issueStatusColor[issue.state.status]}
-
        style:background-color={issueStatusBackgroundColor[issue.state.status]}>
-
        {#if issue.state.status === "open"}
-
          <Icon name="issue" />
-
        {:else}
-
          <Icon name="issue-closed" />
-
        {/if}
+
      <Id id={issue.id} clipboard={issue.id} placement="bottom-start" />
+
      <ExternalLink
+
        href={explorerUrl(`${repo.rid}/issues/${issue.id}`)}
+
        title="Open in app.radicle.xyz" />
+
      <div style:margin-left="auto">
+
        <Button
+
          styleHeight="2rem"
+
          variant="ghost"
+
          onclick={() =>
+
            show({
+
              component: CreateIssueModal,
+
              props: { repo },
+
            })}>
+
          <Icon name="plus" />New issue
+
        </Button>
      </div>
-
      <EditableTitle
-
        {updateTitle}
-
        allowedToEdit={roles.isDelegateOrAuthor(
-
          config.publicKey,
-
          repo.delegates.map(delegate => delegate.did),
-
          issue.body.author.did,
-
        )}
-
        title={issue.title}
-
        cobId={issue.id} />
    </div>

-
    <Border variant="ghost" styleGap="0">
-
      <div class="metadata-section" style:min-width="8rem">
-
        <div class="metadata-section-title">Status</div>
-
        <IssueStateButton selectedState={issue.state} onSelect={saveState} />
-
      </div>
-

-
      <div class="metadata-divider"></div>
-

-
      <div class="metadata-section" style:flex="1">
-
        <LabelInput
-
          allowedToEdit={!!roles.isDelegateOrAuthor(
-
            config.publicKey,
-
            repo.delegates.map(delegate => delegate.did),
-
            issue.body.author.did,
-
          )}
-
          labels={issue.labels}
-
          submitInProgress={labelSaveInProgress}
-
          save={saveLabels} />
-
      </div>
-

-
      <div class="metadata-divider"></div>
-

-
      <div class="metadata-section" style:flex="1">
-
        <AssigneeInput
-
          allowedToEdit={!!roles.isDelegateOrAuthor(
-
            config.publicKey,
-
            repo.delegates.map(delegate => delegate.did),
-
            issue.body.author.did,
-
          )}
-
          assignees={issue.assignees}
-
          submitInProgress={assigneesSaveInProgress}
-
          save={saveAssignees} />
+
    <ScrollArea style="flex: 1; min-height: 0;">
+
      <div class="content">
+
        <div class="main">
+
          <div class="title">
+
            <div
+
              class="global-chip status-chip"
+
              style:color={issueStatusColor[issue.state.status]}
+
              style:background-color={issueStatusBackgroundColor[
+
                issue.state.status
+
              ]}>
+
              <Icon
+
                name={issue.state.status === "open"
+
                  ? "issue"
+
                  : "issue-closed"} />
+
            </div>
+
            <EditableTitle
+
              {updateTitle}
+
              allowedToEdit={roles.isDelegateOrAuthor(
+
                config.publicKey,
+
                repo.delegates.map(delegate => delegate.did),
+
                issue.body.author.did,
+
              )}
+
              title={issue.title}
+
              cobId={issue.id} />
+
          </div>
+

+
          <div class="issue-body">
+
            <CommentComponent
+
              rid={repo.rid}
+
              id={issue.id}
+
              lastEdit={issue.body.edits.length > 1
+
                ? issue.body.edits.at(-1)
+
                : undefined}
+
              author={issue.body.author}
+
              caption="opened"
+
              reactions={issue.body.reactions}
+
              timestamp={issue.body.edits.slice(-1)[0].timestamp}
+
              body={issue.body.edits.slice(-1)[0].body}
+
              editComment={roles.isDelegateOrAuthor(
+
                config.publicKey,
+
                repo.delegates.map(delegate => delegate.did),
+
                issue.body.author.did,
+
              ) && partial(editComment, issue.body.id)}
+
              reactOnComment={partial(reactOnComment, issue.body.id)}>
+
            </CommentComponent>
+
          </div>
+

+
          <Discussion
+
            cobId={issue.id}
+
            commentThreads={threads}
+
            {config}
+
            {createComment}
+
            {editComment}
+
            {reactOnComment}
+
            repoDelegates={repo.delegates}
+
            rid={repo.rid} />
+

+
          <div class="global-flex" style:margin-top="1rem">
+
            <Button
+
              variant="naked"
+
              onclick={() => (hideTimeline = !hideTimeline)}>
+
              <Icon name={hideTimeline ? "chevron-right" : "chevron-down"} />
+
            </Button>
+
            <div class="txt-body-m-regular global-flex">Timeline</div>
+
          </div>
+
          <div
+
            style:display={hideTimeline ? "none" : "revert"}
+
            style:margin-top="1rem">
+
            <IssueTimeline {activity} />
+
          </div>
+
        </div>
+

+
        <div class="sidebar">
+
          <div class="sidebar-section">
+
            <IssueStateButton
+
              selectedState={issue.state}
+
              onSelect={saveState}
+
              disabled={!roles.isDelegate(
+
                config.publicKey,
+
                repo.delegates.map(d => d.did),
+
              )} />
+
          </div>
+
          <div class="sidebar-section">
+
            <LabelInput
+
              allowedToEdit={!!roles.isDelegate(
+
                config.publicKey,
+
                repo.delegates.map(delegate => delegate.did),
+
              )}
+
              labels={issue.labels}
+
              submitInProgress={labelSaveInProgress}
+
              save={saveLabels} />
+
          </div>
+
          <div class="sidebar-section">
+
            <AssigneeInput
+
              allowedToEdit={!!roles.isDelegate(
+
                config.publicKey,
+
                repo.delegates.map(delegate => delegate.did),
+
              )}
+
              assignees={issue.assignees}
+
              submitInProgress={assigneesSaveInProgress}
+
              save={saveAssignees} />
+
          </div>
+
        </div>
      </div>
-
    </Border>
-

-
    <div class="issue-body">
-
      <CommentComponent
-
        rid={repo.rid}
-
        id={issue.id}
-
        lastEdit={issue.body.edits.length > 1
-
          ? issue.body.edits.at(-1)
-
          : undefined}
-
        author={issue.body.author}
-
        caption="opened"
-
        reactions={issue.body.reactions}
-
        timestamp={issue.body.edits.slice(-1)[0].timestamp}
-
        body={issue.body.edits.slice(-1)[0].body}
-
        editComment={roles.isDelegateOrAuthor(
-
          config.publicKey,
-
          repo.delegates.map(delegate => delegate.did),
-
          issue.body.author.did,
-
        ) && partial(editComment, issue.body.id)}
-
        reactOnComment={partial(reactOnComment, issue.body.id)}>
-
      </CommentComponent>
-
    </div>
-

-
    <Discussion
-
      cobId={issue.id}
-
      commentThreads={threads}
-
      {config}
-
      {createComment}
-
      {editComment}
-
      {reactOnComment}
-
      repoDelegates={repo.delegates}
-
      rid={repo.rid} />
-

-
    <div class="global-flex">
-
      <NakedButton
-
        variant="ghost"
-
        onclick={() => (hideTimeline = !hideTimeline)}>
-
        <Icon name={hideTimeline ? "chevron-right" : "chevron-down"} />
-
        <div class="txt-semibold global-flex txt-regular">Timeline</div>
-
      </NakedButton>
-
    </div>
-
    <div
-
      style:display={hideTimeline ? "none" : "revert"}
-
      style:margin-top="1rem">
-
      <IssueTimeline {activity} />
-
    </div>
+
    </ScrollArea>
  </div>
</Layout>
modified src/views/repo/Issues.svelte
@@ -2,7 +2,6 @@
  import type { IssueStatus } from "@app/views/repo/router";
  import type { CacheEvent } from "@bindings/cob/CacheEvent";
  import type { Issue } from "@bindings/cob/issue/Issue";
-
  import type { Config } from "@bindings/config/Config";
  import type { RepoInfo } from "@bindings/repo/RepoInfo";

  import { Channel } from "@tauri-apps/api/core";
@@ -14,40 +13,36 @@
    issueCountMismatch,
    resetIssueCounts,
  } from "@app/lib/issueCounts.svelte";
+
  import { show } from "@app/lib/modal";
  import * as router from "@app/lib/router";
-
  import { explorerUrl, modifierKey } from "@app/lib/utils";
+
  import type { SidebarData } from "@app/lib/router/definitions";
+
  import { modifierKey } from "@app/lib/utils";

-
  import Border from "@app/components/Border.svelte";
  import Button from "@app/components/Button.svelte";
+
  import CobCacheWarning from "@app/components/CobCacheWarning.svelte";
+
  import FuzzySearch from "@app/components/FuzzySearch.svelte";
  import Icon from "@app/components/Icon.svelte";
-
  import IssuesSecondColumn from "@app/components/IssuesSecondColumn.svelte";
  import IssueTeaser from "@app/components/IssueTeaser.svelte";
-
  import NodeBreadcrumb from "@app/components/NodeBreadcrumb.svelte";
-
  import Spinner from "@app/components/Spinner.svelte";
-
  import TextInput from "@app/components/TextInput.svelte";
+
  import ScrollArea from "@app/components/ScrollArea.svelte";
+
  import CreateIssueModal from "@app/modals/CreateIssue.svelte";

-
  import BreadcrumbCopyButton from "./BreadcrumbCopyButton.svelte";
-
  import IssuesBreadcrumb from "./IssuesBreadcrumb.svelte";
  import Layout from "./Layout.svelte";
-
  import RepoBreadcrumb from "./RepoBreadcrumb.svelte";

  interface Props {
    repo: RepoInfo;
    issues: Issue[];
-
    config: Config;
    status: IssueStatus;
-
    notificationCount: number;
+
    sidebarData: SidebarData;
  }

  /* eslint-disable prefer-const */
-
  let { notificationCount, repo, issues, config, status }: Props = $props();
+
  let { repo, issues, status, sidebarData }: Props = $props();
  /* eslint-enable prefer-const */

  let cacheState: CacheEvent | undefined = $state();

-
  const project = $derived(repo.payloads["xyz.radicle.project"]!);
-

  let searchInput = $state("");
+
  let showSearch = $state(false);

  async function rebuildIssueCache() {
    try {
@@ -75,6 +70,7 @@
    status;

    searchInput = "";
+
    showSearch = false;
  });

  const searchableIssues = $derived(
@@ -101,158 +97,170 @@
      all: true,
    }),
  );
+

+
  const project = $derived(repo.payloads["xyz.radicle.project"]!);
</script>

<style>
-
  .container {
-
    padding: 1rem 1rem 1rem 0;
-
  }
-
  .list {
+
  .page {
    display: flex;
    flex-direction: column;
-
    gap: 2px;
+
    height: 100%;
  }
-
  .header {
-
    font-weight: var(--font-weight-medium);
-
    font-size: var(--font-size-medium);
+
  .topbar {
    display: flex;
    align-items: center;
-
    min-height: 2.5rem;
-
    margin-bottom: 1rem;
+
    gap: 0.75rem;
+
    padding: 0 1rem;
+
    height: 2.75rem;
+
    flex-shrink: 0;
+
    border-bottom: 1px solid var(--color-border-subtle);
+
  }
+
  .topbar-title {
+
    font: var(--txt-body-m-semibold);
+
    color: var(--color-text-secondary);
+
    padding-right: 0.25rem;
+
  }
+
  .filters {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.25rem;
+
  }
+
  .filter {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.25rem;
+
    font: var(--txt-body-m-regular);
+
    color: var(--color-text-secondary);
+
    padding: 0.25rem 0.5rem;
+
    border-radius: var(--border-radius-sm);
+
    text-decoration: none;
+
    cursor: pointer;
+
    white-space: nowrap;
+
  }
+
  .filter:hover {
+
    background-color: var(--color-surface-subtle);
+
    color: var(--color-text-primary);
+
  }
+
  .filter.active {
+
    background-color: var(--color-surface-subtle);
+
  }
+
  .filter .global-counter-badge {
+
    margin-left: 0.25rem;
+
  }
+
  .list {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 1px;
+
    min-height: 100%;
  }
</style>

-
<Layout
-
  hideSidebar
-
  styleSecondColumnOverflow="visible"
-
  {config}
-
  {notificationCount}>
-
  {#snippet breadcrumbs()}
-
    <NodeBreadcrumb {config} />
-
    <Icon name="chevron-right" />
-
    <RepoBreadcrumb name={project.data.name} rid={repo.rid} />
-
    <Icon name="chevron-right" />
-
    <IssuesBreadcrumb rid={repo.rid} {status} />
-
    <BreadcrumbCopyButton
-
      url={explorerUrl(`${repo.rid}/issues`)}
-
      icon="repo"
-
      id={repo.rid} />
-
  {/snippet}
-

-
  {#snippet secondColumn()}
-
    <IssuesSecondColumn {status} {repo} />
-
  {/snippet}
-

-
  <div class="container">
-
    {#if issueCountMismatch(status)}
-
      <div style="margin-bottom: 1rem;">
-
        <Border
-
          styleOverflow="hidden"
-
          styleBackgroundColor="var(--color-fill-private)"
-
          stylePadding="0.25rem 0.5rem"
-
          styleGap="1rem"
-
          variant="outline">
-
          <div class="txt-overflow txt-small global-flex">
-
            <Icon name="warning" />
-
            <span class="txt-overflow">
-
              There’s a problem with your COB cache, so some issues may not be
-
              displayed. You can rebuild the cache to resolve this.
-
            </span>
-
          </div>
-
          <div style:margin-left="auto">
-
            <Button
-
              variant="ghost"
-
              onclick={rebuildIssueCache}
-
              disabled={cacheState !== undefined}>
-
              {#if cacheState?.event === "started" || cacheState?.event === "progress"}
-
                Rebuilding
-
                <Spinner />
-
              {:else if cacheState?.event === "finished"}
-
                Done
-
                <Icon name="checkmark" />
-
              {:else}
-
                Rebuild cache
-
              {/if}
-
            </Button>
-
          </div>
-
        </Border>
+
<Layout {sidebarData} activeRepo={repo} selfScroll>
+
  <div class="page">
+
    <div class="topbar">
+
      <span class="topbar-title">Issues</span>
+
      <div class="filters">
+
        <a
+
          class="filter"
+
          class:active={status === "all"}
+
          href={router.routeToPath({
+
            resource: "repo.issues",
+
            rid: repo.rid,
+
            status: "all",
+
          })}>
+
          <Icon name="issue" />All
+
          <span class="global-counter-badge">
+
            {project.meta.issues.open + project.meta.issues.closed}
+
          </span>
+
        </a>
+
        <a
+
          class="filter"
+
          class:active={status === "open"}
+
          href={router.routeToPath({
+
            resource: "repo.issues",
+
            rid: repo.rid,
+
            status: "open",
+
          })}>
+
          <Icon name="issue" />Open
+
          <span class="global-counter-badge">{project.meta.issues.open}</span>
+
        </a>
+
        <a
+
          class="filter"
+
          class:active={status === "closed"}
+
          href={router.routeToPath({
+
            resource: "repo.issues",
+
            rid: repo.rid,
+
            status: "closed",
+
          })}>
+
          <Icon name="issue-closed" />Closed
+
          <span class="global-counter-badge">{project.meta.issues.closed}</span>
+
        </a>
      </div>
-
    {/if}
-
    <div class="header">
-
      <div>Issues</div>
-
      <div class="global-flex" style:margin-left="auto" style:gap="0.75rem">
-
        {#if issues.length > 0}
-
          <TextInput
-
            onSubmit={async () => {
-
              if (searchResults.length === 1) {
-
                await router.push({
-
                  resource: "repo.issue",
-
                  rid: repo.rid,
-
                  issue: searchResults[0].obj.issue.id,
-
                  status,
-
                });
-
              }
-
            }}
-
            onDismiss={() => {
-
              searchInput = "";
-
            }}
-
            placeholder={`Fuzzy filter issues ${modifierKey()} + f`}
-
            keyShortcuts="ctrl+f"
-
            bind:value={searchInput}>
-
            {#snippet left()}
-
              <div
-
                style:color="var(--color-foreground-dim)"
-
                style:padding-left="0.5rem">
-
                <Icon name="filter" />
-
              </div>
-
            {/snippet}
-
          </TextInput>
-
        {/if}
+
      <div class="global-flex" style:margin-left="auto" style:gap="0.5rem">
+
        <FuzzySearch
+
          hasItems={issues.length > 0}
+
          placeholder={`Fuzzy filter issues ${modifierKey()} + f`}
+
          onSubmit={async () => {
+
            if (searchResults.length === 1) {
+
              await router.push({
+
                resource: "repo.issue",
+
                rid: repo.rid,
+
                issue: searchResults[0].obj.issue.id,
+
                status,
+
              });
+
            }
+
          }}
+
          bind:show={showSearch}
+
          bind:value={searchInput} />
        <Button
-
          styleHeight="2.5rem"
+
          styleHeight="2rem"
          variant="secondary"
-
          onclick={() => {
-
            void router.push({
-
              resource: "repo.createIssue",
-
              status,
-
              rid: repo.rid,
-
            });
-
          }}>
-
          <Icon name="add" />New issue
+
          onclick={() =>
+
            show({
+
              component: CreateIssueModal,
+
              props: { repo },
+
            })}>
+
          <Icon name="plus" />New issue
        </Button>
      </div>
    </div>

-
    <div class="list">
-
      {#each searchResults as result}
-
        <IssueTeaser
-
          focussed={searchResults.length === 1 && searchInput !== ""}
-
          issue={result.obj.issue}
-
          rid={repo.rid}
-
          {status} />
-
      {/each}
+
    <ScrollArea style="height: 100%; min-width: 0;">
+
      {#if issueCountMismatch(status)}
+
        <CobCacheWarning
+
          noun="issues"
+
          {cacheState}
+
          onRebuild={rebuildIssueCache} />
+
      {/if}
+

+
      <div class="list">
+
        {#each searchResults as result}
+
          <IssueTeaser
+
            focussed={searchResults.length === 1 && searchInput !== ""}
+
            issue={result.obj.issue}
+
            rid={repo.rid}
+
            {status} />
+
        {/each}

-
      {#if searchResults.length === 0}
-
        <Border
-
          variant="ghost"
-
          styleFlexDirection="column"
-
          styleAlignItems="center"
-
          styleJustifyContent="center">
+
        {#if searchResults.length === 0}
          <div
            class="global-flex"
-
            style:height="5.25rem"
-
            style:justify-content="center">
-
            <div class="txt-missing txt-small global-flex" style:gap="0.25rem">
-
              <Icon name="none" />
+
            style:flex="1"
+
            style:justify-content="center"
+
            style:align-items="center">
+
            <div
+
              class="txt-missing txt-body-m-regular global-flex"
+
              style:gap="0.25rem">
              {#if issues.length > 0 && searchResults.length === 0}
-
                No matching issues.
+
                No matching issues
              {:else}
-
                No {status === "all" ? "" : status} issues.
+
                No {status === "all" ? "" : status} issues
              {/if}
            </div>
          </div>
-
        </Border>
-
      {/if}
-
    </div>
+
        {/if}
+
      </div>
+
    </ScrollArea>
  </div>
</Layout>
deleted src/views/repo/IssuesBreadcrumb.svelte
@@ -1,20 +0,0 @@
-
<script lang="ts">
-
  import type { IssueStatus } from "./router";
-

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

-
  import Link from "@app/components/Link.svelte";
-

-
  interface Props {
-
    rid: string;
-
    status: IssueStatus;
-
  }
-

-
  const { rid, status }: Props = $props();
-
</script>
-

-
{#if $activeRouteStore.resource === "repo.issues"}
-
  Issues
-
{:else}
-
  <Link route={{ resource: "repo.issues", rid, status }}>Issues</Link>
-
{/if}
modified src/views/repo/Layout.svelte
@@ -1,168 +1,60 @@
-
<script lang="ts" module>
-
  type LayoutState = "one-column" | "two-column";
-

-
  const LAYOUT_KEY = "one-column-layout-enabled";
-

-
  let oneColumnLayout = $state(
-
    localStorage ? localStorage.getItem(LAYOUT_KEY) === "one-column" : false,
-
  );
-

-
  export function getLayout() {
-
    return oneColumnLayout;
-
  }
-

-
  export function storeLayout(newValue: LayoutState): void {
-
    oneColumnLayout = newValue === "one-column";
-
    if (localStorage) {
-
      localStorage.setItem(LAYOUT_KEY, newValue);
-
    } else {
-
      console.warn(
-
        "localStorage isn't available, not able to persist the selected layout settings without it.",
-
      );
-
    }
-
  }
-
</script>
-

<script lang="ts">
-
  import type { Config } from "@bindings/config/Config";
+
  import type { RepoInfo } from "@bindings/repo/RepoInfo";
  import type { Snippet } from "svelte";

-
  import { OverlayScrollbarsComponent } from "overlayscrollbars-svelte";
+
  import type { SidebarData } from "@app/lib/router/definitions";

-
  import Header from "@app/components/Header.svelte";
+
  import AppSidebar from "@app/components/AppSidebar.svelte";
+
  import ScrollArea from "@app/components/ScrollArea.svelte";

  interface Props {
    children: Snippet;
-
    config: Config;
-
    secondColumn: Snippet;
-
    sidebar?: Snippet;
+
    sidebarData: SidebarData;
+
    activeRepo?: RepoInfo;
    loadMoreContent?: () => Promise<void>;
-
    loadMoreSecondColumn?: () => Promise<void>;
-
    notificationCount: number;
-
    hideSidebar?: boolean;
-
    styleSecondColumnOverflow?: string;
-
    breadcrumbs?: Snippet;
+
    selfScroll?: boolean;
  }

  const {
    children,
-
    config,
-
    secondColumn,
-
    sidebar = undefined,
+
    sidebarData,
+
    activeRepo = undefined,
    loadMoreContent = undefined,
-
    loadMoreSecondColumn = undefined,
-
    notificationCount,
-
    hideSidebar = false,
-
    styleSecondColumnOverflow = "scroll",
-
    breadcrumbs,
+
    selfScroll = false,
  }: Props = $props();

  let loadingContent = false;
-
  let loadingSecondColumn = false;
</script>

<style>
  .layout {
    display: grid;
-
    grid-template-columns: auto auto 1fr auto;
-
    grid-template-rows: auto 1fr auto;
+
    grid-template-columns: auto 1fr;
+
    grid-template-rows: 100%;
    height: 100%;
-
  }
-

-
  .header {
-
    grid-column: 1 / 4;
-
    border-bottom: 2px solid var(--color-background-default);
-
    z-index: 100;
-
  }
-

-
  .sidebar {
-
    grid-column: 1 / 2;
-
    padding: 1rem;
-
    display: flex;
-
    flex-direction: column;
-
    align-items: center;
-
    justify-content: space-between;
-
  }
-

-
  :global(.secondColumn) {
-
    z-index: 10;
-
    grid-column: 2 / 3;
-
    max-width: 29rem;
-
    min-width: 14rem;
-
    padding: 1rem 1rem 1rem 0;
+
    overflow: hidden;
  }
</style>

<div class="layout">
-
  <div class="header">
-
    <Header {breadcrumbs} {config} {notificationCount}></Header>
-
  </div>
-

-
  {#if sidebar}
+
  <AppSidebar {sidebarData} {activeRepo} />
+
  {#if selfScroll}
    <div
-
      class="sidebar"
-
      style:display={hideSidebar ? "none" : "flex"}
-
      style:padding-right="1rem">
-
      {@render sidebar()}
+
      style="height: 100%; overflow: hidden; min-width: 0; background-color: var(--color-surface-canvas);">
+
      {@render children()}
    </div>
+
  {:else}
+
    <ScrollArea
+
      style="height: 100%; width: 100%; background-color: var(--color-surface-canvas);"
+
      onScrollHalf={loadMoreContent
+
        ? () => {
+
            if (!loadingContent) {
+
              loadingContent = true;
+
              void loadMoreContent().finally(() => (loadingContent = false));
+
            }
+
          }
+
        : undefined}>
+
      {@render children()}
+
    </ScrollArea>
  {/if}
-

-
  <OverlayScrollbarsComponent
-
    element="div"
-
    class="secondColumn"
-
    style={`padding-left: ${hideSidebar ? "1rem" : "0"}; ${oneColumnLayout && !hideSidebar ? "display: none;" : ""}; overflow: ${styleSecondColumnOverflow}`}
-
    events={{
-
      scroll: instance => {
-
        const secondColumnContainer = instance.elements().target;
-
        if (
-
          loadMoreSecondColumn &&
-
          secondColumnContainer.scrollTop +
-
            secondColumnContainer.clientHeight >=
-
            secondColumnContainer.scrollHeight / 2 &&
-
          loadingSecondColumn === false
-
        ) {
-
          loadingSecondColumn = true;
-
          void loadMoreSecondColumn().finally(
-
            () => (loadingSecondColumn = false),
-
          );
-
        }
-
      },
-
    }}
-
    options={{
-
      overflow: { x: "visible" },
-
      scrollbars: {
-
        theme: "global-os-theme-radicle",
-
        autoHide: "scroll",
-
      },
-
    }}
-
    defer>
-
    {@render secondColumn()}
-
  </OverlayScrollbarsComponent>
-

-
  <OverlayScrollbarsComponent
-
    element="div"
-
    events={{
-
      scroll: instance => {
-
        const contentContainer = instance.elements().target;
-
        if (
-
          loadMoreContent &&
-
          contentContainer.scrollTop + contentContainer.clientHeight >=
-
            contentContainer.scrollHeight / 2 &&
-
          loadingContent === false
-
        ) {
-
          loadingContent = true;
-
          void loadMoreContent().finally(() => (loadingContent = false));
-
        }
-
      },
-
    }}
-
    style="grid-column: 3/4; width: 100%;"
-
    options={{
-
      scrollbars: {
-
        theme: "global-os-theme-radicle",
-
        autoHide: "scroll",
-
      },
-
    }}
-
    defer>
-
    {@render children()}
-
  </OverlayScrollbarsComponent>
</div>
modified src/views/repo/Patch.svelte
@@ -1,7 +1,6 @@
<script lang="ts">
  import type { PatchStatus } from "./router";
  import type { Operation } from "@bindings/cob/Operation";
-
  import type { PaginatedQuery } from "@bindings/cob/PaginatedQuery";
  import type { Action } from "@bindings/cob/patch/Action";
  import type { Patch } from "@bindings/cob/patch/Patch";
  import type { Review } from "@bindings/cob/patch/Review";
@@ -9,85 +8,62 @@
  import type { Config } from "@bindings/config/Config";
  import type { RepoInfo } from "@bindings/repo/RepoInfo";

-
  import fuzzysort from "fuzzysort";
-

  import type { DraftReview } from "@app/lib/draftReviewStorage";
  import { draftReviewStorage } from "@app/lib/draftReviewStorage";
  import { nodeRunning } from "@app/lib/events";
  import { invoke } from "@app/lib/invoke";
  import * as router from "@app/lib/router";
-
  import { push } from "@app/lib/router";
+
  import type { SidebarData } from "@app/lib/router/definitions";
  import {
    didFromPublicKey,
    explorerUrl,
-
    formatOid,
-
    verdictIcon,
+
    patchStatusBackgroundColor,
+
    patchStatusColor,
  } from "@app/lib/utils";
-
  import { modifierKey } from "@app/lib/utils";

  import { announce } from "@app/components/AnnounceSwitch.svelte";
-
  import Border from "@app/components/Border.svelte";
  import Button from "@app/components/Button.svelte";
  import CheckoutPatchButton from "@app/components/CheckoutPatchButton.svelte";
-
  import DropdownListItem from "@app/components/DropdownListItem.svelte";
  import EditableTitle from "@app/components/EditableTitle.svelte";
+
  import ExternalLink from "@app/components/ExternalLink.svelte";
  import Icon from "@app/components/Icon.svelte";
-
  import InlineTitle from "@app/components/InlineTitle.svelte";
-
  import Link from "@app/components/Link.svelte";
-
  import MoreBreadcrumbsButton from "@app/components/MoreBreadcrumbsButton.svelte";
-
  import NakedButton from "@app/components/NakedButton.svelte";
+
  import Id from "@app/components/Id.svelte";
  import NewPatchButton from "@app/components/NewPatchButton.svelte";
-
  import NodeBreadcrumb from "@app/components/NodeBreadcrumb.svelte";
  import PatchMetadata from "@app/components/PatchMetadata.svelte";
-
  import PatchStateButtonCompact from "@app/components/PatchStateButtonCompact.svelte";
-
  import PatchStateFilterButton from "@app/components/PatchStateFilterButton.svelte";
-
  import PatchTeaser from "@app/components/PatchTeaser.svelte";
  import PatchTimeline from "@app/components/PatchTimeline.svelte";
  import ReviewComponent from "@app/components/Review.svelte";
  import RevisionComponent from "@app/components/Revision.svelte";
  import Revisions from "@app/components/Revisions.svelte";
-
  import Sidebar from "@app/components/Sidebar.svelte";
-
  import TextInput from "@app/components/TextInput.svelte";
+
  import ScrollArea from "@app/components/ScrollArea.svelte";

-
  import BreadcrumbCopyButton from "./BreadcrumbCopyButton.svelte";
  import Layout from "./Layout.svelte";
-
  import PatchesBreadcrumb from "./PatchesBreadcrumb.svelte";
-
  import RepoBreadcrumb from "./RepoBreadcrumb.svelte";
-
  import { DEFAULT_TAKE } from "./router";

  interface Props {
    repo: RepoInfo;
    patch: Patch;
-
    patches: PaginatedQuery<Patch[]>;
    revisions: Revision[];
    config: Config;
    activity: Operation<Action>[];
    status: PatchStatus | undefined;
    review: Review | DraftReview | undefined;
-
    notificationCount: number;
+
    sidebarData: SidebarData;
  }

  /* eslint-disable prefer-const */
  let {
    repo,
    patch,
-
    patches: initialPatches,
    revisions,
    config,
    status: initialStatus,
    activity,
    review,
-
    notificationCount,
+
    sidebarData,
  }: Props = $props();
  /* eslint-enable prefer-const */

-
  let cursor: number = $state(0);
-
  let more: boolean = $state(false);
-
  let patchTeasers: Patch[] = $state([]);
-

  let hideTimeline = $state(true);
-
  let patches = $state(initialPatches);
-
  let status = $state(initialStatus);
+
  const status = initialStatus;
  let tab: "patch" | "revisions" | "timeline" = $state(
    revisions.length > 1 ? "revisions" : "patch",
  );
@@ -101,14 +77,6 @@
    selectedRevision = revisions.slice(-1)[0];
  });

-
  $effect(() => {
-
    patchTeasers = patches.content;
-
    cursor = patches.cursor;
-
    more = patches.more;
-
  });
-

-
  const project = $derived(repo.payloads["xyz.radicle.project"]!);
-

  async function saveState(newState: Patch["state"]) {
    try {
      await invoke("edit_patch", {
@@ -147,27 +115,8 @@
    }
  }

-
  async function loadMoreTeasers(all: boolean = false) {
-
    if (more) {
-
      const p = await invoke<PaginatedQuery<Patch[]>>("list_patches", {
-
        rid: repo.rid,
-
        status,
-
        skip: cursor + DEFAULT_TAKE,
-
        take: all ? undefined : DEFAULT_TAKE,
-
      });
-

-
      cursor = p.cursor;
-
      more = p.more;
-
      if (all) {
-
        patchTeasers = p.content;
-
      } else {
-
        patchTeasers = [...patchTeasers, ...p.content];
-
      }
-
    }
-
  }
-

  async function loadPatch(patchId: string = patch.id) {
-
    [patch, revisions, activity, patches] = await Promise.all([
+
    [patch, revisions, activity] = await Promise.all([
      invoke<Patch>("patch_by_id", {
        rid: repo.rid,
        id: patchId,
@@ -180,11 +129,6 @@
        rid: repo.rid,
        id: patchId,
      }),
-
      invoke<PaginatedQuery<Patch[]>>("list_patches", {
-
        rid: repo.rid,
-
        status,
-
        take: DEFAULT_TAKE,
-
      }),
    ]);
  }

@@ -205,19 +149,6 @@
    }
  }

-
  async function loadPatches(filter: PatchStatus | undefined) {
-
    try {
-
      patches = await invoke<PaginatedQuery<Patch[]>>("list_patches", {
-
        rid: repo.rid,
-
        status: filter,
-
        take: DEFAULT_TAKE,
-
      });
-
      status = filter;
-
    } catch (error) {
-
      console.error("Loading patch list failed", error);
-
    }
-
  }
-

  function findReviewRevision(review: Review | DraftReview): Revision {
    // Every review is guaranteed to have a revision according to the protocol
    // model, so using type assertions here is safe.
@@ -232,49 +163,6 @@
    }
  }

-
  let showFilters: boolean = $state(false);
-
  let loading: boolean = $state(false);
-
  let searchInput = $state("");
-

-
  const searchablePatches = $derived(
-
    patchTeasers
-
      .flatMap(i => {
-
        return {
-
          patch: i,
-
          labels: i.labels.join(" "),
-
          assignees: i.assignees
-
            .map(a => {
-
              return a.alias ?? "";
-
            })
-
            .join(" "),
-
          author: i.author.alias ?? "",
-
        };
-
      })
-
      .filter((item): item is NonNullable<typeof item> => item !== undefined),
-
  );
-

-
  const searchResults = $derived(
-
    fuzzysort.go(searchInput, searchablePatches, {
-
      keys: ["patch.title", "labels", "assignees", "author", "patch.id"],
-
      threshold: 0.5,
-
      all: true,
-
    }),
-
  );
-
  function breadcrumbTitle() {
-
    if (tab === "patch") {
-
      if (revisions[0].description.slice(-1)[0].body.trim() === "") {
-
        return formatOid(revisions[0].id);
-
      } else {
-
        return revisions[0].description.slice(-1)[0].body.trim();
-
      }
-
    } else {
-
      if (selectedRevision.description.slice(-1)[0].body.trim() === "") {
-
        return formatOid(selectedRevision.id);
-
      } else {
-
        return selectedRevision.description.slice(-1)[0].body.trim();
-
      }
-
    }
-
  }
  const reviewsOfSelectedRevision: Array<Review | DraftReview> = $derived(
    [
      draftReviewStorage.getForRevision(selectedRevision.id, {
@@ -292,241 +180,76 @@
</script>

<style>
-
  .content {
-
    padding: 1rem 1rem 1rem 0;
+
  .page {
+
    display: flex;
+
    flex-direction: column;
+
    height: 100%;
+
  }
+
  .topbar {
+
    display: flex;
+
    align-items: center;
+
    padding: 0 1rem;
+
    height: 2.5rem;
+
    flex-shrink: 0;
+
    gap: 0.375rem;
+
    border-bottom: 1px solid var(--color-border-subtle);
+
    font: var(--txt-body-m-regular);
+
    color: var(--color-text-secondary);
  }
-
  .container {
+
  .topbar-link {
+
    cursor: pointer;
+
    background: none;
+
    border: none;
+
    padding: 0;
+
    font: var(--txt-body-m-regular);
+
    color: var(--color-text-secondary);
+
  }
+
  .topbar-link:hover {
+
    color: var(--color-text-primary);
+
  }
+
  .content {
    display: grid;
-
    grid-template-columns: 1fr min-content;
-
    grid-template-areas: "main-content right-sidebar";
+
    grid-template-columns: 1fr 22rem;
+
  }
+
  @media (max-width: 1349.98px) {
+
    .content {
+
      grid-template-columns: 1fr;
+
    }
  }
-
  .list {
+
  .main {
+
    padding: 1.5rem 2rem;
+
    min-width: 0;
+
  }
+
  .title {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.75rem;
+
    margin-bottom: 1rem;
+
  }
+
  .sidebar {
    display: flex;
    flex-direction: column;
-
    gap: 2px;
+
    border-left: 1px solid var(--color-border-subtle);
+
    height: 100%;
+
    padding: 1.5rem 1rem;
+
    gap: 0.5rem;
+
  }
+
  @media (max-width: 1349.98px) {
+
    .sidebar {
+
      display: none;
+
    }
+
    .sidebar-inline {
+
      display: block;
+
    }
+
  }
+
  @media (min-width: 1350px) {
+
    .sidebar-inline {
+
      display: none;
+
    }
  }
</style>

-
<Layout {config} loadMoreSecondColumn={loadMoreTeasers} {notificationCount}>
-
  {#snippet breadcrumbs()}
-
    <div
-
      class="global-flex global-hide-on-medium-desktop-down"
-
      style:gap="0.25rem">
-
      <NodeBreadcrumb {config} />
-
      <Icon name="chevron-right" />
-
      <RepoBreadcrumb name={project.data.name} rid={repo.rid} />
-
      <Icon name="chevron-right" />
-
      <PatchesBreadcrumb rid={repo.rid} {status} />
-
      <Icon name="chevron-right" />
-
    </div>
-
    <div
-
      class="global-flex global-hide-on-desktop-up"
-
      style:gap="0.25rem"
-
      style:margin-right="0.5rem">
-
      <MoreBreadcrumbsButton>
-
        <DropdownListItem styleGap="0.5rem" selected={false} styleWidth="100%">
-
          <NodeBreadcrumb {config} />
-
        </DropdownListItem>
-
        <DropdownListItem styleGap="0.5rem" selected={false} styleWidth="100%">
-
          <Icon name="repo" />
-
          <RepoBreadcrumb name={project.data.name} rid={repo.rid} />
-
        </DropdownListItem>
-
        <DropdownListItem styleGap="0.5rem" selected={false} styleWidth="100%">
-
          <Icon
-
            name={status === "open" || status === undefined
-
              ? "patch"
-
              : `patch-${status}`} />
-
          <PatchesBreadcrumb rid={repo.rid} {status} />
-
        </DropdownListItem>
-
      </MoreBreadcrumbsButton>
-
    </div>
-
    <span class="txt-overflow" style:max-width="8rem">
-
      {#if review || selectedRevision.id !== revisions.slice(-1)[0].id}
-
        <Link
-
          route={{
-
            resource: "repo.patch",
-
            rid: repo.rid,
-
            patch: patch.id,
-
            status,
-
            reviewId: undefined,
-
          }}>
-
          <InlineTitle content={patch.title} fontSize="small" />
-
        </Link>
-
      {:else}
-
        <InlineTitle content={patch.title} fontSize="small" />
-
      {/if}
-
    </span>
-
    <Icon name="chevron-right" />
-
    {#if review}
-
      <span class="txt-overflow" style:max-width="8rem">
-
        <Link
-
          route={{
-
            resource: "repo.patch",
-
            rid: repo.rid,
-
            patch: patch.id,
-
            status,
-
            reviewId: undefined,
-
          }}>
-
          <span class="txt-overflow" style:max-width="8rem">
-
            {#if selectedRevision.description.slice(-1)[0].body.trim() === ""}
-
              {formatOid(selectedRevision.id)}
-
            {:else}
-
              <InlineTitle
-
                content={selectedRevision.description.slice(-1)[0].body}
-
                fontSize="small" />
-
            {/if}
-
          </span>
-
        </Link>
-
      </span>
-
      <Icon name="chevron-right" />
-
      {review.author.alias}'s review
-
      {#if !("draft" in review)}
-
        <BreadcrumbCopyButton
-
          url={explorerUrl(`${repo.rid}/patches/${patch.id}`)}
-
          icon={verdictIcon(review.verdict)}
-
          id={review.id} />
-
      {/if}
-
    {:else}
-
      <span class="txt-overflow" style:max-width="8rem">
-
        <InlineTitle content={breadcrumbTitle()} fontSize="small" />
-
      </span>
-
      <BreadcrumbCopyButton
-
        url={explorerUrl(`${repo.rid}/patches/${patch.id}`)}
-
        icon={patch.state.status === "open"
-
          ? "patch"
-
          : `patch-${patch.state.status}`}
-
        id={patch.id}
-
        icon2={revisions.length > 1 ? "revision" : undefined}
-
        id2={revisions.length > 1 &&
-
        selectedRevision.id !== revisions[0].id &&
-
        tab !== "patch"
-
          ? selectedRevision.id
-
          : undefined} />
-
    {/if}
-
  {/snippet}
-

-
  {#snippet sidebar()}
-
    <Sidebar activeTab="patches" rid={repo.rid} />
-
  {/snippet}
-

-
  {#snippet secondColumn()}
-
    <div
-
      class="txt-medium global-flex"
-
      style:font-weight="var(--font-weight-medium)"
-
      style:min-width="28rem"
-
      style:min-height="2.5rem"
-
      style:margin-bottom="1rem">
-
      <PatchStateFilterButton
-
        counters={project.meta.patches}
-
        {status}
-
        select={async selectedState => {
-
          await loadPatches(selectedState);
-
        }} />
-
      <NakedButton
-
        styleHeight="2.5rem"
-
        keyShortcuts="ctrl+f"
-
        variant="ghost"
-
        active={showFilters}
-
        onclick={() => {
-
          if (showFilters) {
-
            showFilters = false;
-
            searchInput = "";
-
          } else {
-
            showFilters = true;
-
          }
-
        }}>
-
        <Icon name="filter" />
-
      </NakedButton>
-
      <div class="global-flex" style:margin-left="auto">
-
        <NewPatchButton rid={repo.rid} outline />
-
      </div>
-
    </div>
-
    {#if showFilters}
-
      <div class="global-flex" style:margin="1rem 0">
-
        {#if patchTeasers.length > 0}
-
          <TextInput
-
            onFocus={async () => {
-
              try {
-
                loading = true;
-
                await loadMoreTeasers(true);
-
              } catch (e) {
-
                console.error("Loading all patches failed: ", e);
-
              } finally {
-
                loading = false;
-
              }
-
            }}
-
            onSubmit={async () => {
-
              if (searchResults.length === 1) {
-
                await router.push({
-
                  patch: searchResults[0].obj.patch.id,
-
                  resource: "repo.patch",
-
                  reviewId: undefined,
-
                  rid: repo.rid,
-
                  status,
-
                });
-
              }
-
            }}
-
            onDismiss={() => {
-
              showFilters = false;
-
              searchInput = "";
-
            }}
-
            placeholder={`Fuzzy filter patches ${modifierKey()} + f`}
-
            autofocus
-
            bind:value={searchInput}>
-
            {#snippet left()}
-
              <div
-
                style:color="var(--color-foreground-dim)"
-
                style:padding-left="0.5rem">
-
                <Icon name={loading ? "clock" : "filter"} />
-
              </div>
-
            {/snippet}
-
          </TextInput>
-
        {/if}
-
      </div>
-
    {/if}
-

-
    {#if searchResults.length > 0}
-
      <div class="list">
-
        {#each searchResults as teaser}
-
          <PatchTeaser
-
            selected={teaser.obj.patch.id === patch.id}
-
            focussed={searchResults.length === 1 && searchInput !== ""}
-
            compact
-
            loadPatch={async (id: string) => {
-
              review = undefined;
-
              await loadPatch(id);
-
            }}
-
            patch={teaser.obj.patch}
-
            rid={repo.rid}
-
            {status} />
-
        {/each}
-
      </div>
-
    {/if}
-

-
    {#if searchResults.length === 0}
-
      <Border
-
        variant="ghost"
-
        styleFlexDirection="column"
-
        styleOverflow="hidden"
-
        styleAlignItems="center"
-
        styleJustifyContent="center">
-
        <div
-
          class="global-flex"
-
          style:height="84px"
-
          style:justify-content="center">
-
          <div class="txt-missing txt-small global-flex" style:gap="0.25rem">
-
            <Icon name="none" />
-
            {#if patchTeasers.length > 0 && searchResults.length === 0}
-
              No matching patches.
-
            {:else}
-
              No {status === undefined ? "" : status} patches.
-
            {/if}
-
          </div>
-
        </div>
-
      </Border>
-
    {/if}
-
  {/snippet}
-

+
<Layout {sidebarData} activeRepo={repo}>
  {#if review}
    <ReviewComponent
      {config}
@@ -539,137 +262,168 @@
        review = undefined;
      }} />
  {:else}
-
    <div class="content">
-
      <div class="global-flex" style:margin-bottom="1rem" style:gap="0.75rem">
-
        <PatchStateButtonCompact
-
          selectedState={patch.state}
-
          onSelect={newState => {
-
            void saveState(newState);
-
          }} />
-
        <EditableTitle
-
          {updateTitle}
-
          allowedToEdit={true}
-
          title={patch.title}
-
          cobId={patch.id} />
-
        <div
-
          class="global-flex"
-
          style:margin-left="auto"
-
          style:z-index="40"
-
          style:gap="1rem">
-
          <CheckoutPatchButton
-
            {tab}
-
            selectedRevisionId={selectedRevision.id}
-
            patchId={patch.id} />
-
          <Button
-
            variant="secondary"
-
            styleHeight="2.5rem"
-
            disabled={hasOwnReview}
-
            onclick={() => {
-
              const id = draftReviewStorage.create(
-
                repo.rid,
-
                selectedRevision.id,
-
              );
-
              void push({
-
                resource: "repo.patch",
-
                rid: repo.rid,
-
                patch: patch.id,
-
                reviewId: id,
-
                status,
-
              });
-
            }}
-
            title={hasOwnReview
-
              ? "You already created a review for this revision"
-
              : "Review revision"}>
-
            <Icon name="review" />
-
            <span class="txt-small global-hide-on-medium-desktop-down">
-
              Review revision
-
            </span>
-
          </Button>
-
        </div>
-
      </div>
-
      <div class="global-hide-on-desktop-up" style:margin-top="1rem">
-
        <PatchMetadata
-
          {config}
-
          {loadPatch}
-
          {patch}
-
          {repo}
-
          {saveState}
-
          horizontal />
-
      </div>
-
      <div
-
        class="global-hide-on-desktop-up"
-
        style:padding="0.5rem"
-
        style:margin-bottom="2rem">
-
        <div
-
          class="txt-small"
-
          style:margin-bottom="1rem"
-
          style:color="var(--color-foreground-dim)">
-
          Revisions
+
    <div class="page">
+
      <div class="topbar">
+
        <Icon
+
          name={patch.state.status === "open"
+
            ? "patch"
+
            : `patch-${patch.state.status}`} />
+
        <button
+
          class="topbar-link"
+
          onclick={() =>
+
            router.push({
+
              resource: "repo.patches",
+
              rid: repo.rid,
+
              status: undefined,
+
            })}>
+
          All Patches
+
        </button>
+
        <Icon name="chevron-right" />
+
        <Id id={patch.id} clipboard={patch.id} placement="bottom-start" />
+
        <ExternalLink
+
          href={explorerUrl(`${repo.rid}/patches/${patch.id}`)}
+
          title="Open in app.radicle.xyz" />
+
        <div style:margin-left="auto">
+
          <NewPatchButton rid={repo.rid} ghost />
        </div>
-
        <Revisions
-
          {config}
-
          rid={repo.rid}
-
          selectRevision={rev => {
-
            selectedRevision = rev;
-
            tab = "revisions";
-
          }}
-
          {patch}
-
          {revisions}
-
          {selectedRevision}
-
          {status} />
      </div>
-
      <div class="container">
-
        <div style:grid-area="main-content" style:min-width="0">
-
          <RevisionComponent
-
            rid={repo.rid}
-
            repoDelegates={repo.delegates}
-
            patchId={patch.id}
-
            {loadPatch}
-
            revision={selectedRevision}
-
            {config} />
-
          <div class="global-flex" style:margin-top="1.5rem">
-
            <NakedButton
-
              variant="ghost"
-
              onclick={() => (hideTimeline = !hideTimeline)}
-
              stylePadding="0 4px">
-
              <Icon name={hideTimeline ? "chevron-right" : "chevron-down"} />
-
            </NakedButton>
-
            <div class="txt-semibold global-flex txt-regular">Timeline</div>
-
          </div>
-
          <div
-
            style:display={hideTimeline ? "none" : "revert"}
-
            style:margin-top="1rem">
-
            <PatchTimeline {activity} patchId={patch.id} />
-
          </div>
-
        </div>

-
        <div
-
          class="global-hide-on-medium-desktop-down"
-
          style:grid-area="right-sidebar"
-
          style:margin-left="1rem"
-
          style:width="22rem">
-
          <PatchMetadata {config} {loadPatch} {patch} {repo} {saveState} />
-
          <div style:margin-top="0.5rem" style:padding="0.5rem">
-
            <div
-
              class="txt-small"
-
              style:margin-bottom="1rem"
-
              style:color="var(--color-foreground-dim)">
-
              Revisions
+
      <ScrollArea style="flex: 1; min-height: 0;">
+
        <div class="content">
+
          <div class="main">
+
            <div class="title">
+
              <div
+
                class="global-chip"
+
                style:color={patchStatusColor[patch.state.status]}
+
                style:background-color={patchStatusBackgroundColor[
+
                  patch.state.status
+
                ]}
+
                style:height="2rem"
+
                style:width="2rem"
+
                style:padding="0">
+
                <Icon
+
                  name={patch.state.status === "open"
+
                    ? "patch"
+
                    : `patch-${patch.state.status}`} />
+
              </div>
+
              <EditableTitle
+
                {updateTitle}
+
                allowedToEdit={true}
+
                title={patch.title}
+
                cobId={patch.id} />
+
              <div
+
                class="global-flex"
+
                style:margin-left="auto"
+
                style:z-index="40"
+
                style:gap="1rem">
+
                <CheckoutPatchButton
+
                  {tab}
+
                  selectedRevisionId={selectedRevision.id}
+
                  patchId={patch.id} />
+
                <Button
+
                  variant="secondary"
+
                  disabled={hasOwnReview}
+
                  onclick={() => {
+
                    const id = draftReviewStorage.create(
+
                      repo.rid,
+
                      selectedRevision.id,
+
                    );
+
                    void router.push({
+
                      resource: "repo.patch",
+
                      rid: repo.rid,
+
                      patch: patch.id,
+
                      reviewId: id,
+
                      status,
+
                    });
+
                  }}
+
                  title={hasOwnReview
+
                    ? "You already created a review for this revision"
+
                    : "Review revision"}>
+
                  <Icon name="comment" />
+
                  <span
+
                    class="txt-body-m-regular global-hide-on-medium-desktop-down">
+
                    Review revision
+
                  </span>
+
                </Button>
+
              </div>
+
            </div>
+

+
            <div class="sidebar-inline">
+
              <PatchMetadata
+
                {config}
+
                {loadPatch}
+
                {patch}
+
                {repo}
+
                {saveState}
+
                horizontal />
+
              <div style:padding="0.5rem" style:margin-bottom="1rem">
+
                <div
+
                  class="txt-body-m-regular"
+
                  style:margin-bottom="1rem"
+
                  style:color="var(--color-text-secondary)">
+
                  Revisions
+
                </div>
+
                <Revisions
+
                  {config}
+
                  rid={repo.rid}
+
                  selectRevision={rev => {
+
                    selectedRevision = rev;
+
                    tab = "revisions";
+
                  }}
+
                  {patch}
+
                  {revisions}
+
                  {selectedRevision}
+
                  {status} />
+
              </div>
            </div>
-
            <Revisions
-
              {config}
+

+
            <RevisionComponent
              rid={repo.rid}
-
              selectRevision={rev => {
-
                selectedRevision = rev;
-
                tab = "revisions";
-
              }}
-
              {patch}
-
              {revisions}
-
              {selectedRevision}
-
              {status} />
+
              repoDelegates={repo.delegates}
+
              patchId={patch.id}
+
              {loadPatch}
+
              revision={selectedRevision}
+
              {config} />
+

+
            <div class="global-flex" style:margin-top="1.5rem">
+
              <Button
+
                variant="naked"
+
                onclick={() => (hideTimeline = !hideTimeline)}>
+
                <Icon name={hideTimeline ? "chevron-right" : "chevron-down"} />
+
              </Button>
+
              <div class="txt-body-m-regular global-flex">Timeline</div>
+
            </div>
+
            <div
+
              style:display={hideTimeline ? "none" : "revert"}
+
              style:margin-top="1rem">
+
              <PatchTimeline {activity} patchId={patch.id} />
+
            </div>
+
          </div>
+

+
          <div class="sidebar">
+
            <PatchMetadata {config} {loadPatch} {patch} {repo} {saveState} />
+
            <div style:padding="0.5rem" style:margin-top="0.5rem">
+
              <div
+
                class="txt-body-m-regular"
+
                style:margin-bottom="1rem"
+
                style:color="var(--color-text-secondary)">
+
                Revisions
+
              </div>
+
              <Revisions
+
                {config}
+
                rid={repo.rid}
+
                selectRevision={rev => {
+
                  selectedRevision = rev;
+
                  tab = "revisions";
+
                }}
+
                {patch}
+
                {revisions}
+
                {selectedRevision}
+
                {status} />
+
            </div>
          </div>
        </div>
-
      </div>
+
      </ScrollArea>
    </div>
  {/if}
</Layout>
modified src/views/repo/Patches.svelte
@@ -3,7 +3,6 @@
  import type { CacheEvent } from "@bindings/cob/CacheEvent";
  import type { PaginatedQuery } from "@bindings/cob/PaginatedQuery";
  import type { Patch } from "@bindings/cob/patch/Patch";
-
  import type { Config } from "@bindings/config/Config";
  import type { RepoInfo } from "@bindings/repo/RepoInfo";

  import { DEFAULT_TAKE } from "@app/views/repo/router";
@@ -18,32 +17,26 @@
    updatePatchCounts,
  } from "@app/lib/patchCounts.svelte";
  import * as router from "@app/lib/router";
-
  import { explorerUrl, modifierKey } from "@app/lib/utils";
+
  import type { SidebarData } from "@app/lib/router/definitions";
+
  import { modifierKey } from "@app/lib/utils";

-
  import Border from "@app/components/Border.svelte";
-
  import Button from "@app/components/Button.svelte";
+
  import CobCacheWarning from "@app/components/CobCacheWarning.svelte";
+
  import FuzzySearch from "@app/components/FuzzySearch.svelte";
  import Icon from "@app/components/Icon.svelte";
  import NewPatchButton from "@app/components/NewPatchButton.svelte";
-
  import NodeBreadcrumb from "@app/components/NodeBreadcrumb.svelte";
-
  import PatchesSecondColumn from "@app/components/PatchesSecondColumn.svelte";
  import PatchTeaser from "@app/components/PatchTeaser.svelte";
-
  import Spinner from "@app/components/Spinner.svelte";
-
  import TextInput from "@app/components/TextInput.svelte";
+
  import ScrollArea from "@app/components/ScrollArea.svelte";

-
  import BreadcrumbCopyButton from "./BreadcrumbCopyButton.svelte";
  import Layout from "./Layout.svelte";
-
  import PatchesBreadcrumb from "./PatchesBreadcrumb.svelte";
-
  import RepoBreadcrumb from "./RepoBreadcrumb.svelte";

  interface Props {
    repo: RepoInfo;
    patches: PaginatedQuery<Patch[]>;
-
    config: Config;
    status: PatchStatus | undefined;
-
    notificationCount: number;
+
    sidebarData: SidebarData;
  }

-
  const { repo, patches, config, status, notificationCount }: Props = $props();
+
  const { repo, patches, status, sidebarData }: Props = $props();

  let items = $state(patches.content);
  let cursor = patches.cursor;
@@ -56,7 +49,6 @@
  $effect(() => {
    items = patches.content;
    cursor = patches.cursor;
-
    // If the first page is not full, we know there are no more patches.
    if (patches.more === true && patches.content.length < DEFAULT_TAKE) {
      more = false;
    } else {
@@ -75,6 +67,7 @@
    status;

    searchInput = "";
+
    showSearch = false;
  });

  async function rebuildPatchCache() {
@@ -125,7 +118,6 @@
        items = [...items, ...p.content];
      }

-
      // If the newly fetched patches are empty, there is no more to fetch.
      if (p.content.length === 0) {
        more = false;
      }
@@ -136,8 +128,10 @@
    }
  }

+
  let loadingMore: boolean = $state(false);
  let loading: boolean = $state(false);
  let searchInput = $state("");
+
  let showSearch = $state(false);

  const searchablePatches = $derived(
    items
@@ -166,159 +160,220 @@
</script>

<style>
-
  .container {
-
    padding: 1rem 1rem 1rem 0;
-
  }
-
  .list {
+
  .page {
    display: flex;
    flex-direction: column;
-
    gap: 2px;
+
    height: 100%;
  }
-
  .header {
-
    font-weight: var(--font-weight-medium);
-
    font-size: var(--font-size-medium);
+
  .topbar {
    display: flex;
    align-items: center;
-
    min-height: 2.5rem;
-
    margin-bottom: 1rem;
    gap: 0.75rem;
+
    padding: 0 1rem;
+
    height: 2.75rem;
+
    flex-shrink: 0;
+
    border-bottom: 1px solid var(--color-border-subtle);
+
  }
+
  .topbar-title {
+
    font: var(--txt-body-m-semibold);
+
    color: var(--color-text-secondary);
+
    padding-right: 0.25rem;
+
  }
+
  .filters {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.25rem;
+
  }
+
  .filter {
+
    display: flex;
+
    align-items: center;
+
    gap: 0.25rem;
+
    font: var(--txt-body-m-regular);
+
    color: var(--color-text-secondary);
+
    padding: 0.25rem 0.5rem;
+
    border-radius: var(--border-radius-sm);
+
    text-decoration: none;
+
    cursor: pointer;
+
    white-space: nowrap;
+
  }
+
  .filter:hover {
+
    background-color: var(--color-surface-subtle);
+
    color: var(--color-text-primary);
+
  }
+
  .filter.active {
+
    background-color: var(--color-surface-subtle);
+
  }
+
  .filter .global-counter-badge {
+
    margin-left: 0.25rem;
+
  }
+
  .filter-label {
+
    display: none;
+
  }
+
  .filter.active .filter-label {
+
    display: inline;
+
  }
+
  @media (min-width: 1011px) {
+
    .filter-label {
+
      display: inline;
+
    }
+
  }
+
  .list {
+
    display: flex;
+
    flex-direction: column;
+
    gap: 1px;
+
    min-height: 100%;
  }
</style>

-
<Layout
-
  {notificationCount}
-
  {loadMoreContent}
-
  hideSidebar
-
  styleSecondColumnOverflow="visible"
-
  {config}>
-
  {#snippet breadcrumbs()}
-
    <NodeBreadcrumb {config} />
-
    <Icon name="chevron-right" />
-
    <RepoBreadcrumb name={project.data.name} rid={repo.rid} />
-
    <Icon name="chevron-right" />
-
    <PatchesBreadcrumb rid={repo.rid} {status} />
-
    <BreadcrumbCopyButton
-
      url={explorerUrl(`${repo.rid}/patches`)}
-
      icon="repo"
-
      id={repo.rid} />
-
  {/snippet}
-

-
  {#snippet secondColumn()}
-
    <PatchesSecondColumn {project} {status} {repo} />
-
  {/snippet}
-

-
  <div class="container">
-
    {#if patchCountMismatch(status)}
-
      <div style="margin-bottom: 1rem;">
-
        <Border
-
          styleOverflow="hidden"
-
          styleBackgroundColor="var(--color-fill-private)"
-
          stylePadding="0.25rem 0.5rem"
-
          styleGap="1rem"
-
          variant="outline">
-
          <div class="txt-overflow txt-small global-flex">
-
            <Icon name="warning" />
-
            <span class="txt-overflow">
-
              There’s a problem with your COB cache, so some patches may not be
-
              displayed. You can rebuild the cache to resolve this.
-
            </span>
-
          </div>
-
          <div style:margin-left="auto">
-
            <Button
-
              variant="ghost"
-
              onclick={rebuildPatchCache}
-
              disabled={cacheState !== undefined}>
-
              {#if cacheState?.event === "started" || cacheState?.event === "progress"}
-
                Rebuilding
-
                <Spinner />
-
              {:else if cacheState?.event === "finished"}
-
                Done
-
                <Icon name="checkmark" />
-
              {:else}
-
                Rebuild cache
-
              {/if}
-
            </Button>
-
          </div>
-
        </Border>
+
<Layout {sidebarData} activeRepo={repo} selfScroll>
+
  <div class="page">
+
    <div class="topbar">
+
      <span class="topbar-title">Patches</span>
+
      <div class="filters">
+
        <a
+
          class="filter"
+
          class:active={status === undefined}
+
          href={router.routeToPath({
+
            resource: "repo.patches",
+
            rid: repo.rid,
+
            status: undefined,
+
          })}>
+
          <Icon name="patch" />
+
          <span class="filter-label">All</span>
+
          <span class="global-counter-badge">
+
            {project.meta.patches.open +
+
              project.meta.patches.draft +
+
              project.meta.patches.archived +
+
              project.meta.patches.merged}
+
          </span>
+
        </a>
+
        <a
+
          class="filter"
+
          class:active={status === "open"}
+
          href={router.routeToPath({
+
            resource: "repo.patches",
+
            rid: repo.rid,
+
            status: "open",
+
          })}>
+
          <Icon name="patch" />
+
          <span class="filter-label">Open</span>
+
          <span class="global-counter-badge">{project.meta.patches.open}</span>
+
        </a>
+
        <a
+
          class="filter"
+
          class:active={status === "merged"}
+
          href={router.routeToPath({
+
            resource: "repo.patches",
+
            rid: repo.rid,
+
            status: "merged",
+
          })}>
+
          <Icon name="patch-merged" />
+
          <span class="filter-label">Merged</span>
+
          <span class="global-counter-badge">
+
            {project.meta.patches.merged}
+
          </span>
+
        </a>
+
        <a
+
          class="filter"
+
          class:active={status === "archived"}
+
          href={router.routeToPath({
+
            resource: "repo.patches",
+
            rid: repo.rid,
+
            status: "archived",
+
          })}>
+
          <Icon name="patch-archived" />
+
          <span class="filter-label">Archived</span>
+
          <span class="global-counter-badge">
+
            {project.meta.patches.archived}
+
          </span>
+
        </a>
+
        <a
+
          class="filter"
+
          class:active={status === "draft"}
+
          href={router.routeToPath({
+
            resource: "repo.patches",
+
            rid: repo.rid,
+
            status: "draft",
+
          })}>
+
          <Icon name="patch-draft" />
+
          <span class="filter-label">Drafts</span>
+
          <span class="global-counter-badge">{project.meta.patches.draft}</span>
+
        </a>
      </div>
-
    {/if}
-
    <div class="header">
-
      Patches
-

-
      <div class="global-flex" style:margin-left="auto" style:gap="0.75rem">
-
        {#if items.length > 0}
-
          <TextInput
-
            onFocus={async () => {
-
              try {
-
                loading = true;
-
                // Load all patches.
-
                await loadMoreContent(true);
-
              } catch (e) {
-
                console.error("Loading all patches failed: ", e);
-
              } finally {
-
                loading = false;
-
              }
-
            }}
-
            onSubmit={async () => {
-
              if (searchResults.length === 1) {
-
                await router.push({
-
                  patch: searchResults[0].obj.patch.id,
-
                  resource: "repo.patch",
-
                  reviewId: undefined,
-
                  rid: repo.rid,
-
                  status,
-
                });
-
              }
-
            }}
-
            onDismiss={() => {
-
              searchInput = "";
-
            }}
-
            placeholder={`Fuzzy filter patches ${modifierKey()} + f`}
-
            keyShortcuts="ctrl+f"
-
            bind:value={searchInput}>
-
            {#snippet left()}
-
              <div
-
                style:color="var(--color-foreground-dim)"
-
                style:padding-left="0.5rem">
-
                <Icon name={loading ? "clock" : "filter"} />
-
              </div>
-
            {/snippet}
-
          </TextInput>
-
        {/if}
+
      <div class="global-flex" style:margin-left="auto" style:gap="0.5rem">
+
        <FuzzySearch
+
          hasItems={items.length > 0}
+
          placeholder={`Fuzzy filter patches ${modifierKey()} + f`}
+
          icon={loading ? "clock" : "filter"}
+
          onFocus={async () => {
+
            try {
+
              loading = true;
+
              await loadMoreContent(true);
+
            } catch (e) {
+
              console.error("Loading all patches failed: ", e);
+
            } finally {
+
              loading = false;
+
            }
+
          }}
+
          onSubmit={async () => {
+
            if (searchResults.length === 1) {
+
              await router.push({
+
                patch: searchResults[0].obj.patch.id,
+
                resource: "repo.patch",
+
                reviewId: undefined,
+
                rid: repo.rid,
+
                status,
+
              });
+
            }
+
          }}
+
          bind:show={showSearch}
+
          bind:value={searchInput} />
        <NewPatchButton rid={repo.rid} />
      </div>
    </div>

-
    <div class="list">
-
      {#each searchResults as result}
-
        <PatchTeaser
-
          focussed={searchResults.length === 1 && searchInput !== ""}
-
          patch={result.obj.patch}
-
          rid={repo.rid}
-
          {status} />
-
      {/each}
+
    <ScrollArea
+
      style="height: 100%; min-width: 0;"
+
      onScrollHalf={() => {
+
        if (!loadingMore) {
+
          loadingMore = true;
+
          void loadMoreContent().finally(() => (loadingMore = false));
+
        }
+
      }}>
+
      {#if patchCountMismatch(status)}
+
        <CobCacheWarning
+
          noun="patches"
+
          {cacheState}
+
          onRebuild={rebuildPatchCache} />
+
      {/if}
+

+
      <div class="list">
+
        {#each searchResults as result}
+
          <PatchTeaser
+
            focussed={searchResults.length === 1 && searchInput !== ""}
+
            patch={result.obj.patch}
+
            rid={repo.rid}
+
            {status} />
+
        {/each}

-
      {#if searchResults.length === 0}
-
        <Border
-
          variant="ghost"
-
          styleFlexDirection="column"
-
          styleAlignItems="center"
-
          styleJustifyContent="center">
+
        {#if searchResults.length === 0}
          <div
            class="global-flex"
-
            style:height="84px"
-
            style:justify-content="center">
-
            <div class="txt-missing txt-small global-flex" style:gap="0.25rem">
-
              <Icon name="none" />
+
            style:flex="1"
+
            style:justify-content="center"
+
            style:align-items="center">
+
            <div
+
              class="txt-missing txt-body-m-regular global-flex"
+
              style:gap="0.25rem">
              {#if items.length > 0 && searchResults.length === 0}
-
                No matching patches.
+
                No matching patches
              {:else}
-
                No {status === undefined ? "" : status} patches.
+
                No {status === undefined ? "" : status} patches
              {/if}
            </div>
          </div>
-
        </Border>
-
      {/if}
-
    </div>
+
        {/if}
+
      </div>
+
    </ScrollArea>
  </div>
</Layout>
deleted src/views/repo/PatchesBreadcrumb.svelte
@@ -1,20 +0,0 @@
-
<script lang="ts">
-
  import type { PatchStatus } from "./router";
-

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

-
  import Link from "@app/components/Link.svelte";
-

-
  interface Props {
-
    rid: string;
-
    status: PatchStatus | undefined;
-
  }
-

-
  const { rid, status }: Props = $props();
-
</script>
-

-
{#if $activeRouteStore.resource === "repo.patches"}
-
  Patches
-
{:else}
-
  <Link route={{ resource: "repo.patches", rid, status }}>Patches</Link>
-
{/if}
deleted src/views/repo/RepoBreadcrumb.svelte
@@ -1,24 +0,0 @@
-
<script lang="ts">
-
  import { activeRouteStore } from "@app/lib/router";
-
  import { explorerUrl } from "@app/lib/utils";
-

-
  import Link from "@app/components/Link.svelte";
-

-
  import BreadcrumbCopyButton from "./BreadcrumbCopyButton.svelte";
-

-
  interface Props {
-
    name: string;
-
    rid: string;
-
  }
-

-
  const { name, rid }: Props = $props();
-
</script>
-

-
{#if $activeRouteStore.resource === "repo.home"}
-
  {name}
-
  <BreadcrumbCopyButton icon="repo" id={rid} url={explorerUrl(`${rid}`)} />
-
{:else}
-
  <Link route={{ resource: "repo.home", rid }}>
-
    {name}
-
  </Link>
-
{/if}
modified src/views/repo/RepoHome.svelte
@@ -1,13 +1,4 @@
-
<script lang="ts" module>
-
  let currentPath = $state("");
-

-
  export function getCurrentPath() {
-
    return currentPath;
-
  }
-
</script>
-

<script lang="ts">
-
  import type { Config } from "@bindings/config/Config";
  import type { Readme } from "@bindings/repo/Readme";
  import type { RepoInfo } from "@bindings/repo/RepoInfo";
  import type { Blob } from "@bindings/source/Blob";
@@ -18,39 +9,38 @@
  import { useOverlayScrollbars } from "overlayscrollbars-svelte";

  import { invoke, InvokeError } from "@app/lib/invoke";
+
  import type { SidebarData } from "@app/lib/router/definitions";
  import { highlight } from "@app/lib/syntax";

-
  import Border from "@app/components/Border.svelte";
-
  import CheckoutRepoButton from "@app/components/CheckoutRepoButton.svelte";
-
  import File from "@app/components/File.svelte";
+
  import FileBlock from "@app/components/FileBlock.svelte";
  import Icon from "@app/components/Icon.svelte";
  import Id from "@app/components/Id.svelte";
  import Markdown from "@app/components/Markdown.svelte";
-
  import NodeBreadcrumb from "@app/components/NodeBreadcrumb.svelte";
  import Path from "@app/components/Path.svelte";
  import PreviewSwitch from "@app/components/PreviewSwitch.svelte";
-
  import RepoHomeSecondColumn from "@app/components/RepoHomeSecondColumn.svelte";
-
  import RepoMetadata from "@app/components/RepoMetadata.svelte";
+
  import RepoHeader from "@app/components/RepoHeader.svelte";
+
  import ScrollArea from "@app/components/ScrollArea.svelte";
+
  import TreeComponent from "@app/components/Tree.svelte";
  import Layout from "@app/views/repo/Layout.svelte";
-
  import RepoBreadcrumb from "@app/views/repo/RepoBreadcrumb.svelte";

  interface Props {
-
    config: Config;
    tree: Tree;
    repo: RepoInfo;
    readme: Readme | null;
-
    notificationCount: number;
+
    sidebarData: SidebarData;
  }

  /* eslint-disable prefer-const */
-
  let { config, tree, readme, repo, notificationCount }: Props = $props();
+
  let { tree, readme, repo, sidebarData }: Props = $props();
  /* eslint-enable prefer-const */

+
  let currentPath = $state("");
  let codeElement: HTMLElement | undefined = $state();
  let preview = $state(true);
  let error: InvokeError | undefined = $state();

  $effect(() => {
+
    blob = readme;
    currentPath = readme?.path || "";
  });

@@ -93,7 +83,6 @@
    preview = isMarkdownPath(currentPath);
  });

-
  const project = $derived(repo.payloads["xyz.radicle.project"]!);
  let blob: Blob | Readme | null = $state(readme);
  const showLineNumbers = $derived(
    blob && !blob.binary && blob.content.trim() !== "" && !preview && !error,
@@ -102,13 +91,7 @@

<style>
  .content {
-
    padding: 1rem 1rem 1rem 0;
-
  }
-
  .container {
-
    display: grid;
-
    grid-template-columns: 1fr min-content;
-
    grid-template-areas: "main-content right-sidebar";
-
    margin-top: 2rem;
+
    height: 100%;
  }
  .line-column {
    display: flex;
@@ -125,7 +108,8 @@
    display: flex;
    flex-direction: column;
    align-items: center;
-
    padding: 1rem 0;
+
    justify-content: center;
+
    min-height: calc(100dvh - 7rem);
  }
  .code,
  .commit-msg {
@@ -136,156 +120,146 @@
  }
</style>

-
<Layout
-
  {config}
-
  hideSidebar
-
  styleSecondColumnOverflow="visible"
-
  {notificationCount}>
-
  {#snippet breadcrumbs()}
-
    <NodeBreadcrumb {config} />
-
    <Icon name="chevron-right" />
-
    <RepoBreadcrumb name={project.data.name} rid={repo.rid} />
-
  {/snippet}
-

-
  {#snippet secondColumn()}
-
    <RepoHomeSecondColumn {repo} {tree} {fetchBlob} {fetchTree} />
-
  {/snippet}
-

-
  <div class="content">
-
    <div class="global-flex">
+
<Layout {sidebarData} activeRepo={repo} selfScroll>
+
  <div
+
    class="content"
+
    style:display="flex"
+
    style:flex-direction="column"
+
    style:height="100%">
+
    <RepoHeader {repo} />
+
    <div
+
      style:display="grid"
+
      style:grid-template-columns="16.5rem 1fr"
+
      style:grid-template-rows="100%"
+
      style:flex="1"
+
      style:min-height="0">
      <div
-
        class="txt-medium txt-selectable"
-
        style:font-weight="var(--font-weight-medium)">
-
        {project.data.name}
-
      </div>
-
      <div class="global-flex" style:margin-left="auto">
-
        <CheckoutRepoButton rid={repo.rid} />
+
        style:display="flex"
+
        style:flex-direction="column"
+
        style:height="100%"
+
        style:min-height="0"
+
        style:overflow="hidden">
+
        {#if tree.entries.length > 0}
+
          <ScrollArea
+
            style="border-right: 1px solid var(--color-border-subtle); flex: 1; min-height: 0; width: 100%; padding: 0.5rem;">
+
            <TreeComponent {tree} {currentPath} {fetchTree} {fetchBlob} />
+
          </ScrollArea>
+
        {/if}
      </div>
-
    </div>
-

-
    {#if project.data.description !== ""}
-
      <Markdown rid={repo.rid} breaks content={project.data.description} />
-
    {/if}
-

-
    <div class="global-hide-on-desktop-up" style:margin-top="1rem">
-
      <RepoMetadata {repo} horizontal />
-
    </div>
-

-
    <div class="container">
-
      <div style:grid-area="main-content" style:min-width="0">
-
        {#if blob === null}
-
          <Border
-
            variant="ghost"
-
            stylePadding="1rem"
-
            styleAlignItems="center"
-
            styleMinHeight="10rem">
-
            <div
-
              class="global-flex txt-missing"
-
              style:width="100%"
-
              style:justify-content="center">
-
              <Icon name="none" />README not found
-
            </div>
-
          </Border>
-
        {:else}
-
          <File expandable={false} sticky={false}>
-
            {#snippet leftHeader()}
-
              <div style:margin-left="0.5rem">
-
                <Path fullPath={currentPath} />
+
      <ScrollArea style="height: 100%; min-width: 0;">
+
        <div class="container">
+
          <div style:min-width="0">
+
            {#if blob === null}
+
              <div
+
                style:display="flex"
+
                style:min-height="calc(100dvh - 7rem)"
+
                style:gap="0.5rem"
+
                style:align-items="center"
+
                style:background-color="var(--color-surface-canvas)"
+
                style:padding="1rem">
+
                <div
+
                  class="global-flex txt-missing txt-body-m-regular"
+
                  style:width="100%"
+
                  style:justify-content="center">
+
                  No README.md
+
                </div>
              </div>
-
            {/snippet}
-

-
            {#snippet rightHeader()}
-
              {#if blob}
-
                <Border
-
                  styleMaxWidth="fit-content"
-
                  variant="float"
-
                  styleBackgroundColor="var(--color-background-float)"
-
                  stylePadding="0 0.5rem"
-
                  styleAlignItems="center"
-
                  styleAlignSelf="flex-end">
-
                  <Id
-
                    variant="commit"
-
                    id={blob.commit.id}
-
                    clipboard={blob.commit.id} />
-
                  <span class="commit-msg txt-overflow" style:max-width="20rem">
-
                    {blob.commit.message}
-
                  </span>
-
                </Border>
-
              {/if}
-

-
              {#if isMarkdownPath(currentPath)}
-
                <PreviewSwitch bind:preview />
-
              {/if}
-
            {/snippet}
+
            {:else}
+
              <FileBlock expandable={false} sticky={false} border={false}>
+
                {#snippet leftHeader()}
+
                  <div style:margin-left="0.5rem">
+
                    <Path fullPath={currentPath} />
+
                  </div>
+
                {/snippet}

-
            <div class="blob">
-
              <div class="line-column">
-
                {#if showLineNumbers}
-
                  {#each blob.content
-
                    .trimEnd()
-
                    .split("\n")
-
                    .map((_, index) => index) as line}
-
                    <div class="txt-missing txt-monospace txt-small">
-
                      {line + 1}
-
                    </div>
-
                  {/each}
-
                {/if}
-
              </div>
-
              <div style:width="100%" bind:this={codeElement}>
-
                {#if blob.binary}
-
                  {#if blob.mimeType.startsWith("image")}
-
                    <img
-
                      src={`data:${blob.mimeType};base64,${blob.content}`}
-
                      alt={`Preview of ${blob.id}`} />
-
                  {:else}
-
                    <div class="txt-small blob-placeholder txt-missing">
-
                      <Icon name="file" size="32" />
-
                      <span>Binary file</span>
+
                {#snippet rightHeader()}
+
                  {#if blob}
+
                    <div
+
                      style:display="flex"
+
                      style:gap="0.5rem"
+
                      style:align-items="center"
+
                      style:justify-content="center"
+
                      style:max-width="fit-content">
+
                      <Id
+
                        id={blob.commit.id}
+
                        clipboard={blob.commit.id}
+
                        placement="bottom-start" />
+
                      <span
+
                        class="commit-msg txt-overflow"
+
                        style:max-width="20rem">
+
                        {blob.commit.message}
+
                      </span>
                    </div>
                  {/if}
-
                {:else if preview}
-
                  <div style:margin-top="1rem">
-
                    <Markdown content={blob.content} />
-
                  </div>
-
                {:else if blob.content.trim() === ""}
-
                  <div class="txt-small blob-placeholder txt-missing">
-
                    <Icon name="none" size="32" />
-
                    <span>Empty file</span>
+

+
                  {#if isMarkdownPath(currentPath)}
+
                    <PreviewSwitch bind:preview />
+
                  {/if}
+
                {/snippet}
+

+
                <div class="blob">
+
                  <div class="line-column">
+
                    {#if showLineNumbers}
+
                      {#each blob.content
+
                        .trimEnd()
+
                        .split("\n")
+
                        .map((_, index) => index) as line}
+
                        <div class="txt-missing txt-code-regular">
+
                          {line + 1}
+
                        </div>
+
                      {/each}
+
                    {/if}
                  </div>
-
                {:else if error}
-
                  <div class="txt-small blob-placeholder txt-missing">
-
                    <Icon name="warning" size="32" />
-
                    {#if error.code === "PayloadError.TooLarge"}
-
                      <span>File size exceeds limit of 10 MB.</span>
+
                  <div style:width="100%" bind:this={codeElement}>
+
                    {#if blob.binary}
+
                      {#if blob.mimeType.startsWith("image")}
+
                        <img
+
                          src={`data:${blob.mimeType};base64,${blob.content}`}
+
                          alt={`Preview of ${blob.id}`} />
+
                      {:else}
+
                        <div
+
                          class="txt-body-m-regular blob-placeholder txt-missing">
+
                          <span>Binary file</span>
+
                        </div>
+
                      {/if}
+
                    {:else if preview}
+
                      <div style:margin-top="1rem">
+
                        <Markdown content={blob.content} />
+
                      </div>
+
                    {:else if blob.content.trim() === ""}
+
                      <div
+
                        class="txt-body-m-regular blob-placeholder txt-missing">
+
                        <span>Empty file</span>
+
                      </div>
+
                    {:else if error}
+
                      <div
+
                        class="txt-body-m-regular blob-placeholder txt-missing">
+
                        <Icon name="warning" size="32" />
+
                        {#if error.code === "PayloadError.TooLarge"}
+
                          <span>File size exceeds limit of 10 MB.</span>
+
                        {:else}
+
                          <span>{capitalize(error.message)}</span>
+
                        {/if}
+
                      </div>
                    {:else}
-
                      <span>{capitalize(error.message)}</span>
+
                      <code>
+
                        <pre
+
                          class="code txt-code-regular"
+
                          style:margin="0"
+
                          style:padding="0">{#await highlight(blob.content, currentPath
+
                              .split(".")
+
                              .at(-1) || "raw")}{blob.content}{:then tree}{@html toHtml(
+
                              tree,
+
                            )}{/await}</pre>
+
                      </code>
                    {/if}
                  </div>
-
                {:else}
-
                  <code>
-
                    <pre
-
                      class="code txt-small"
-
                      style:margin="0"
-
                      style:padding="0">{#await highlight(blob.content, currentPath
-
                          .split(".")
-
                          .at(-1) || "raw")}{blob.content}{:then tree}{@html toHtml(
-
                          tree,
-
                        )}{/await}</pre>
-
                  </code>
-
                {/if}
-
              </div>
-
            </div>
-
          </File>
-
        {/if}
-
      </div>
-

-
      <div
-
        class="global-hide-on-medium-desktop-down"
-
        style:grid-area="right-sidebar"
-
        style:margin-left="1rem"
-
        style:min-width="20rem">
-
        <RepoMetadata {repo} />
-
      </div>
+
                </div>
+
              </FileBlock>
+
            {/if}
+
          </div>
+
        </div>
+
      </ScrollArea>
    </div>
  </div>
</Layout>
modified src/views/repo/router.ts
@@ -15,6 +15,8 @@ import type { Tree } from "@bindings/source/Tree";
import type { DraftReview } from "@app/lib/draftReviewStorage";
import { draftReviewStorage } from "@app/lib/draftReviewStorage";
import { invoke } from "@app/lib/invoke";
+
import type { SidebarData } from "@app/lib/router/definitions";
+
import { loadSidebarData } from "@app/lib/router/definitions";
import { didFromPublicKey, unreachable } from "@app/lib/utils";

export type IssueStatus = "all" | Issue["state"]["status"];
@@ -34,12 +36,6 @@ export interface RepoIssueRoute {
  status: IssueStatus;
}

-
export interface RepoCreateIssueRoute {
-
  resource: "repo.createIssue";
-
  rid: string;
-
  status: IssueStatus;
-
}
-

export interface LoadedRepoHomeRoute {
  resource: "repo.home";
  params: {
@@ -48,7 +44,7 @@ export interface LoadedRepoHomeRoute {
    tree: Tree;
    config: Config;
    readme: Readme | null;
-
    notificationCount: number;
+
    sidebarData: SidebarData;
  };
}

@@ -62,18 +58,7 @@ export interface LoadedRepoIssueRoute {
    status: IssueStatus;
    activity: Operation<IssueAction>[];
    threads: Thread[];
-
    notificationCount: number;
-
  };
-
}
-

-
export interface LoadedRepoCreateIssueRoute {
-
  resource: "repo.createIssue";
-
  params: {
-
    repo: RepoInfo;
-
    config: Config;
-
    issues: Issue[];
-
    status: IssueStatus;
-
    notificationCount: number;
+
    sidebarData: SidebarData;
  };
}

@@ -90,7 +75,7 @@ export interface LoadedRepoIssuesRoute {
    config: Config;
    issues: Issue[];
    status: IssueStatus;
-
    notificationCount: number;
+
    sidebarData: SidebarData;
  };
}

@@ -115,7 +100,7 @@ export interface LoadedRepoPatchRoute {
    review: Review | DraftReview | undefined;
    revisions: Revision[];
    activity: Operation<PatchAction>[];
-
    notificationCount: number;
+
    sidebarData: SidebarData;
  };
}

@@ -132,20 +117,18 @@ export interface LoadedRepoPatchesRoute {
    config: Config;
    patches: PaginatedQuery<Patch[]>;
    status: PatchStatus | undefined;
-
    notificationCount: number;
+
    sidebarData: SidebarData;
  };
}

export type RepoRoute =
  | RepoHomeRoute
-
  | RepoCreateIssueRoute
  | RepoIssueRoute
  | RepoIssuesRoute
  | RepoPatchRoute
  | RepoPatchesRoute;
export type LoadedRepoRoute =
  | LoadedRepoHomeRoute
-
  | LoadedRepoCreateIssueRoute
  | LoadedRepoIssueRoute
  | LoadedRepoIssuesRoute
  | LoadedRepoPatchRoute
@@ -154,9 +137,9 @@ export type LoadedRepoRoute =
export async function loadPatch(
  route: RepoPatchRoute,
): Promise<LoadedRepoPatchRoute> {
-
  const [notificationCount, config, repo, patches, patch, revisions, activity] =
+
  const [sidebarData, config, repo, patches, patch, revisions, activity] =
    await Promise.all([
-
      invoke<number>("notification_count"),
+
      loadSidebarData(),
      invoke<Config>("config"),
      invoke<RepoInfo>("repo_by_id", {
        rid: route.rid,
@@ -204,7 +187,7 @@ export async function loadPatch(
      status: route.status,
      review,
      activity,
-
      notificationCount,
+
      sidebarData,
    },
  };
}
@@ -212,8 +195,8 @@ export async function loadPatch(
export async function loadPatches(
  route: RepoPatchesRoute,
): Promise<LoadedRepoPatchesRoute> {
-
  const [notificationCount, config, repo, patches] = await Promise.all([
-
    invoke<number>("notification_count"),
+
  const [sidebarData, config, repo, patches] = await Promise.all([
+
    loadSidebarData(),
    invoke<Config>("config"),
    invoke<RepoInfo>("repo_by_id", {
      rid: route.rid,
@@ -227,15 +210,15 @@ export async function loadPatches(

  return {
    resource: "repo.patches",
-
    params: { notificationCount, repo, config, patches, status: route.status },
+
    params: { sidebarData, repo, config, patches, status: route.status },
  };
}

export async function loadRepoHome(
  route: RepoHomeRoute,
): Promise<LoadedRepoHomeRoute> {
-
  const [notificationCount, config, repo, readme, tree] = await Promise.all([
-
    invoke<number>("notification_count"),
+
  const [sidebarData, config, repo, readme, tree] = await Promise.all([
+
    loadSidebarData(),
    invoke<Config>("config"),
    invoke<RepoInfo>("repo_by_id", {
      rid: route.rid,
@@ -252,37 +235,16 @@ export async function loadRepoHome(

  return {
    resource: "repo.home",
-
    params: { notificationCount, repo, sha: route.sha, config, readme, tree },
-
  };
-
}
-

-
export async function loadCreateIssue(
-
  route: RepoCreateIssueRoute,
-
): Promise<LoadedRepoCreateIssueRoute> {
-
  const [notificationCount, config, repo, issues] = await Promise.all([
-
    invoke<number>("notification_count"),
-
    invoke<Config>("config"),
-
    invoke<RepoInfo>("repo_by_id", {
-
      rid: route.rid,
-
    }),
-
    invoke<Issue[]>("list_issues", {
-
      rid: route.rid,
-
      status: route.status,
-
    }),
-
  ]);
-

-
  return {
-
    resource: "repo.createIssue",
-
    params: { notificationCount, repo, config, issues, status: route.status },
+
    params: { sidebarData, repo, sha: route.sha, config, readme, tree },
  };
}

export async function loadIssue(
  route: RepoIssueRoute,
): Promise<LoadedRepoIssueRoute> {
-
  const [notificationCount, config, repo, issue, activity, issues, threads] =
+
  const [sidebarData, config, repo, issue, activity, issues, threads] =
    await Promise.all([
-
      invoke<number>("notification_count"),
+
      loadSidebarData(),
      invoke<Config>("config"),
      invoke<RepoInfo>("repo_by_id", {
        rid: route.rid,
@@ -308,7 +270,7 @@ export async function loadIssue(
  return {
    resource: "repo.issue",
    params: {
-
      notificationCount,
+
      sidebarData,
      repo,
      config,
      issue,
@@ -323,8 +285,8 @@ export async function loadIssue(
export async function loadIssues(
  route: RepoIssuesRoute,
): Promise<LoadedRepoIssuesRoute> {
-
  const [notificationCount, config, repo, issues] = await Promise.all([
-
    invoke<number>("notification_count"),
+
  const [sidebarData, config, repo, issues] = await Promise.all([
+
    loadSidebarData(),
    invoke<Config>("config"),
    invoke<RepoInfo>("repo_by_id", {
      rid: route.rid,
@@ -337,7 +299,7 @@ export async function loadIssues(

  return {
    resource: "repo.issues",
-
    params: { notificationCount, repo, config, issues, status: route.status },
+
    params: { sidebarData, repo, config, issues, status: route.status },
  };
}

@@ -353,11 +315,6 @@ export function repoRouteToPath(route: RepoRoute): string {
    searchParams.set("status", route.status);
    url += `?${searchParams}`;
    return url;
-
  } else if (route.resource === "repo.createIssue") {
-
    let url = [...pathSegments, "issues", "create"].join("/");
-
    searchParams.set("status", route.status);
-
    url += `?${searchParams}`;
-
    return url;
  } else if (route.resource === "repo.issues") {
    let url = [...pathSegments, "issues"].join("/");
    searchParams.set("status", route.status);
@@ -401,9 +358,7 @@ export function repoUrlToRoute(
      const idOrAction = segments.shift();
      if (idOrAction) {
        const status = (searchParams.get("status") ?? "all") as IssueStatus;
-
        if (idOrAction === "create") {
-
          return { resource: "repo.createIssue", rid, status };
-
        } else {
+
        if (idOrAction !== "create") {
          return {
            resource: "repo.issue",
            rid,
@@ -411,6 +366,7 @@ export function repoUrlToRoute(
            status,
          };
        }
+
        return null;
      } else {
        const status = searchParams.get("status");
        if (status === "open" || status === "closed") {
modified tests/e2e/clipboard.spec.ts
@@ -1,8 +1,6 @@
import { expect, markdownRid, test } from "@tests/support/fixtures.js";
import { chromium } from "playwright";

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

// We explicitly run all clipboard tests withing the context of a single test
// so that we don't run into race conditions, because there is no way to isolate
// the clipboard in Playwright yet.
@@ -19,7 +17,9 @@ test("copy to clipboard", async () => {

  // Repo ID.
  {
-
    await page.getByText(formatRepositoryId(markdownRid)).click();
+
    const repoLink = page.getByRole("link", { name: "markdown" });
+
    await repoLink.hover();
+
    await repoLink.getByRole("button", { name: "icon-copy" }).click();
    const clipboardContent = await page.evaluate<string>(
      "navigator.clipboard.readText()",
    );
modified tests/e2e/repo/issue.spec.ts
@@ -9,8 +9,8 @@ test("navigate single issue", async ({ page }) => {

test("correct order of threads", async ({ page }) => {
  await page.goto("/repos");
-
  await page.getByRole("button", { name: "cobs" }).click();
-
  await page.getByRole("link", { name: "icon-issue Issues" }).click();
+
  await page.getByRole("link", { name: "cobs" }).click();
+
  await page.getByRole("link", { name: "Issues" }).click();
  await page.getByText("This title has **markdown**").click();
  const body = page.locator(".issue-body");
  await expect(body.getByText("This is a description")).toBeVisible();
@@ -30,8 +30,8 @@ test("correct order of threads", async ({ page }) => {

test("creation of top level comments", async ({ page }) => {
  await page.goto("/repos");
-
  await page.getByRole("button", { name: "cobs" }).click();
-
  await page.getByRole("link", { name: "icon-issue Issues" }).click();
+
  await page.getByRole("link", { name: "cobs" }).click();
+
  await page.getByRole("link", { name: "Issues" }).click();
  await page.getByRole("button", { name: "New" }).click();
  await page
    .getByPlaceholder("Title")
@@ -43,14 +43,12 @@ test("creation of top level comments", async ({ page }) => {
    );
  await page.getByRole("button", { name: "icon-checkmark" }).click();
  await expect(
-
    page.getByRole("button", { name: "icon-issue Make sure that" }),
+
    page.getByText("Make sure that comment creation is working"),
  ).toBeVisible();
  await expect(
-
    page
-
      .getByText(
-
        "It's important for us that the comment creation flow works as expected.",
-
      )
-
      .last(),
+
    page.getByText(
+
      "It's important for us that the comment creation flow works as expected.",
+
    ),
  ).toBeVisible();

  await page.getByRole("button", { name: "icon-comment Comment" }).click();
modified tests/e2e/repo/issues.spec.ts
@@ -1,7 +1,7 @@
import { cobRid, expect, test } from "@tests/support/fixtures.js";

test("navigate issues listing", async ({ page }) => {
-
  await page.goto(`/repos/${cobRid}/issues?show=all`);
+
  await page.goto(`/repos/${cobRid}/issues?status=all`);
  await page.getByRole("link", { name: "Closed" }).click();
  await expect(page.locator(".issue-teaser")).toHaveCount(2);
  await expect(page).toHaveURL(`/repos/${cobRid}/issues?status=closed`);
modified tests/e2e/repos.spec.ts
@@ -2,10 +2,8 @@ import { expect, test } from "@tests/support/fixtures.js";

test("navigate to repo issues", async ({ page }) => {
  await page.goto("/repos");
-
  await page.getByRole("button", { name: "cobs" }).click();
-
  await page.getByRole("link", { name: "icon-issue Issues" }).click();
+
  await page.getByRole("link", { name: "cobs" }).click();
+
  await page.getByRole("link", { name: "Issues" }).click();
  await page.getByText("This title has **markdown**").click();
-
  await expect(
-
    page.getByText("This title has **markdown**").nth(1),
-
  ).toBeVisible();
+
  await expect(page).toHaveURL(/\/issues\/[0-9a-f]{40}/);
});
modified tests/e2e/theme.spec.ts
@@ -8,12 +8,10 @@ test("default theme", async ({ page }) => {

test("theme persistence", async ({ page }) => {
  await page.goto("/repos");
-
  await expect(page.getByRole("button", { name: "markdown" })).toBeVisible();
-
  await page.getByRole("button", { name: "Settings", exact: true }).click();
+
  await expect(page.getByRole("link", { name: "markdown" })).toBeVisible();
+
  await page.getByRole("button", { name: "Settings" }).click();

-
  await page
-
    .getByRole("button", { name: "icon-sun Light", exact: true })
-
    .click();
+
  await page.getByRole("button", { name: "Light", exact: true }).click();
  await expect(page.locator("html")).toHaveAttribute("data-theme", "light");

  await page.reload();
@@ -23,16 +21,12 @@ test("theme persistence", async ({ page }) => {

test("change theme", async ({ page }) => {
  await page.goto("/repos");
-
  await expect(page.getByRole("button", { name: "markdown" })).toBeVisible();
-
  await page.getByRole("button", { name: "Settings", exact: true }).click();
+
  await expect(page.getByRole("link", { name: "markdown" })).toBeVisible();
+
  await page.getByRole("button", { name: "Settings" }).click();

-
  await page
-
    .getByRole("button", { name: "icon-sun Light", exact: true })
-
    .click();
+
  await page.getByRole("button", { name: "Light", exact: true }).click();
  await expect(page.locator("html")).toHaveAttribute("data-theme", "light");

-
  await page
-
    .getByRole("button", { name: "icon-moon Dark", exact: true })
-
    .click();
+
  await page.getByRole("button", { name: "Dark", exact: true }).click();
  await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
});
modified tests/support/peerManager.ts
@@ -270,7 +270,7 @@ export class RadiclePeer {
      resources: [
        `tcp:${this.#httpdBaseUrl.hostname}:${this.#httpdBaseUrl.port}`,
      ],
-
      timeout: 20_000,
+
      timeout: 120_000,
    });
  }

modified tests/unit/notifications.test.ts
@@ -31,17 +31,17 @@ describe("Action summaries", () => {
    {
      summary: "Review without verdict",
      input: [createAction({ type: "review", revision })],
-
      output: `left a review with a comment on revision <span class="global-oid">${formatOid(revision)}</span>`,
+
      output: `left a review with a comment on revision <span class="txt-id">${formatOid(revision)}</span>`,
    },
    {
      summary: "Review with accepted verdict",
      input: [createAction({ type: "review", verdict: "accept", revision })],
-
      output: `accepted revision <span class="global-oid">${formatOid(revision)}</span> with a review`,
+
      output: `accepted revision <span class="txt-id">${formatOid(revision)}</span> with a review`,
    },
    {
      summary: "Review with rejected verdict",
      input: [createAction({ type: "review", verdict: "reject", revision })],
-
      output: `rejected revision <span class="global-oid">${formatOid(revision)}</span> with a review`,
+
      output: `rejected revision <span class="txt-id">${formatOid(revision)}</span> with a review`,
    },
    {
      summary: "Add multiple labels",
@@ -91,7 +91,7 @@ describe("Action summaries", () => {
          base: oid,
        }),
      ],
-
      output: `created revision <span class="global-oid">${formatOid(revision)}</span>`,
+
      output: `created revision <span class="txt-id">${formatOid(revision)}</span>`,
    },
    {
      summary: "Merge revision",
@@ -102,7 +102,7 @@ describe("Action summaries", () => {
          commit: actionOid,
        }),
      ],
-
      output: `merged revision <span class="global-oid">${formatOid(revision)}</span>`,
+
      output: `merged revision <span class="txt-id">${formatOid(revision)}</span>`,
    },
  ])(
    "$summary => $output",
@@ -172,13 +172,13 @@ describe("Action summaries", () => {
        createAction({ type: "edit", title: "Lorem ipsum" }, oid),
        createAction({ type: "comment", body: "A patch title" }, oid),
      ],
-
      output: `opened patch <span class="global-oid">${formatOid(oid)}</span>`,
+
      output: `opened patch <span class="txt-id">${formatOid(oid)}</span>`,
    },
    {
      summary: "Open issue where the action has the same oid than the cob",
      type: "issue" as const,
      input: [createAction({ type: "edit", title: "Lorem ipsum" }, oid)],
-
      output: `opened issue <span class="global-oid">${formatOid(oid)}</span>`,
+
      output: `opened issue <span class="txt-id">${formatOid(oid)}</span>`,
    },
    {
      summary: "Leave two comments in one operation",