Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
Add our own router
Open rudolfs opened 1 year ago

check

๐Ÿ‘‰ Workflow runs ๐Ÿ‘‰ Branch on GitHub

14 files changed +684 -332 484d0d58 โ†’ 8179ed0f
modified package-lock.json
@@ -18,6 +18,7 @@
        "@sveltejs/vite-plugin-svelte": "^3.1.2",
        "@tauri-apps/cli": "^2.0.0-rc.1",
        "@tsconfig/svelte": "^5.0.4",
+
        "baconjs": "^3.0.19",
        "eslint": "^9.9.1",
        "eslint-config-prettier": "^9.1.0",
        "eslint-plugin-svelte": "^2.43.0",
@@ -26,7 +27,6 @@
        "svelte": "^4.2.19",
        "svelte-check": "^4.0.0",
        "svelte-eslint-parser": "^0.41.0",
-
        "svelte-routing": "^2.13.0",
        "tslib": "^2.7.0",
        "typescript": "^5.2.2",
        "typescript-eslint": "^8.4.0",
@@ -432,6 +432,18 @@
        "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
      }
    },
+
    "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+
      "version": "3.4.3",
+
      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+
      "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+
      "dev": true,
+
      "engines": {
+
        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+
      },
+
      "funding": {
+
        "url": "https://opencollective.com/eslint"
+
      }
+
    },
    "node_modules/@eslint-community/regexpp": {
      "version": "4.11.0",
      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz",
@@ -478,35 +490,6 @@
        "url": "https://opencollective.com/eslint"
      }
    },
-
    "node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": {
-
      "version": "4.0.0",
-
      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
-
      "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
-
      "dev": true,
-
      "engines": {
-
        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
-
      },
-
      "funding": {
-
        "url": "https://opencollective.com/eslint"
-
      }
-
    },
-
    "node_modules/@eslint/eslintrc/node_modules/espree": {
-
      "version": "10.1.0",
-
      "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz",
-
      "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==",
-
      "dev": true,
-
      "dependencies": {
-
        "acorn": "^8.12.0",
-
        "acorn-jsx": "^5.3.2",
-
        "eslint-visitor-keys": "^4.0.0"
-
      },
-
      "engines": {
-
        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
-
      },
-
      "funding": {
-
        "url": "https://opencollective.com/eslint"
-
      }
-
    },
    "node_modules/@eslint/js": {
      "version": "9.9.1",
      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz",
@@ -882,23 +865,18 @@
      }
    },
    "node_modules/@tauri-apps/api": {
-
      "version": "2.0.0-beta.15",
-
      "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.0-beta.15.tgz",
-
      "integrity": "sha512-H9w6iISmR+NvH4XuyCZB4zDN10tf9RFt6i/9JHEjaRhAowdAaJ+oiXq/3kedizNClHMtbTQ5j0oqDVPkZDAI8g==",
-
      "engines": {
-
        "node": ">= 18.18",
-
        "npm": ">= 6.6.0",
-
        "yarn": ">= 1.19.1"
-
      },
+
      "version": "2.0.0-rc.4",
+
      "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.0-rc.4.tgz",
+
      "integrity": "sha512-UNiIhhKG08j4ooss2oEEVexffmWkgkYlC2M3GcX3VPtNsqFgVNL8Mcw/4Y7rO9M9S+ffAMnLOF5ypzyuyb8tyg==",
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/tauri"
      }
    },
    "node_modules/@tauri-apps/cli": {
-
      "version": "2.0.0-rc.1",
-
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.0.0-rc.1.tgz",
-
      "integrity": "sha512-9AzVrUMdb6EZ/Lwtdqt03XqqG6d/3gTJPOw2E9zmCHprJWEwqEp4JIVHYYfrqkkZyKclD3m5ggXwfYwclcYLdw==",
+
      "version": "2.0.0-rc.10",
+
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.0.0-rc.10.tgz",
+
      "integrity": "sha512-mnoMyeD65DoVWzrLiLRW8Ns5Aktn9Ua7eKTOUEPq+r+1sQtWKxfnYTBEbEWnivduLhJCEDqGP5tyJaPcVXcEzA==",
      "dev": true,
      "bin": {
        "tauri": "tauri.js"
@@ -911,22 +889,22 @@
        "url": "https://opencollective.com/tauri"
      },
      "optionalDependencies": {
-
        "@tauri-apps/cli-darwin-arm64": "2.0.0-rc.1",
-
        "@tauri-apps/cli-darwin-x64": "2.0.0-rc.1",
-
        "@tauri-apps/cli-linux-arm-gnueabihf": "2.0.0-rc.1",
-
        "@tauri-apps/cli-linux-arm64-gnu": "2.0.0-rc.1",
-
        "@tauri-apps/cli-linux-arm64-musl": "2.0.0-rc.1",
-
        "@tauri-apps/cli-linux-x64-gnu": "2.0.0-rc.1",
-
        "@tauri-apps/cli-linux-x64-musl": "2.0.0-rc.1",
-
        "@tauri-apps/cli-win32-arm64-msvc": "2.0.0-rc.1",
-
        "@tauri-apps/cli-win32-ia32-msvc": "2.0.0-rc.1",
-
        "@tauri-apps/cli-win32-x64-msvc": "2.0.0-rc.1"
+
        "@tauri-apps/cli-darwin-arm64": "2.0.0-rc.10",
+
        "@tauri-apps/cli-darwin-x64": "2.0.0-rc.10",
+
        "@tauri-apps/cli-linux-arm-gnueabihf": "2.0.0-rc.10",
+
        "@tauri-apps/cli-linux-arm64-gnu": "2.0.0-rc.10",
+
        "@tauri-apps/cli-linux-arm64-musl": "2.0.0-rc.10",
+
        "@tauri-apps/cli-linux-x64-gnu": "2.0.0-rc.10",
+
        "@tauri-apps/cli-linux-x64-musl": "2.0.0-rc.10",
+
        "@tauri-apps/cli-win32-arm64-msvc": "2.0.0-rc.10",
+
        "@tauri-apps/cli-win32-ia32-msvc": "2.0.0-rc.10",
+
        "@tauri-apps/cli-win32-x64-msvc": "2.0.0-rc.10"
      }
    },
    "node_modules/@tauri-apps/cli-darwin-arm64": {
-
      "version": "2.0.0-rc.1",
-
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.0-rc.1.tgz",
-
      "integrity": "sha512-dJxyAi4P9fOkklBZXuwUVnHgdM/20fgM4zYdfejQfju5+q9GUqnMbrrIUqlJbQGf8EnrIdSWnieO8wU8GOwT0g==",
+
      "version": "2.0.0-rc.10",
+
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.0-rc.10.tgz",
+
      "integrity": "sha512-oAuG3n/dIqK5ZedknF1QOgVDlpEepAaaIFHpUi+eIdG1MFp82jgyHqplveVZ95F16j7RhjIMaEhiTF6cGR/baA==",
      "cpu": [
        "arm64"
      ],
@@ -940,9 +918,9 @@
      }
    },
    "node_modules/@tauri-apps/cli-darwin-x64": {
-
      "version": "2.0.0-rc.1",
-
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.0.0-rc.1.tgz",
-
      "integrity": "sha512-dwrqLzNIFk8a1Vf2YI8axHm7uvLfo4M4TSWCw2ZkgeSGWWK6Y6CYVZbBEjOEGIOf+GFAa9rVOSZRuMwpiufNng==",
+
      "version": "2.0.0-rc.10",
+
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.0.0-rc.10.tgz",
+
      "integrity": "sha512-n4ul0XUBqrA7KbNY6Vo52EBNqTXogYuV2qi5RWR0bIJF/A/vYjZ3LcC1TXXo/X57sDN55LWORrBe4c4Ds8MZrA==",
      "cpu": [
        "x64"
      ],
@@ -956,9 +934,9 @@
      }
    },
    "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
-
      "version": "2.0.0-rc.1",
-
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.0.0-rc.1.tgz",
-
      "integrity": "sha512-jvMF4UNc3Jr/xHnw+4NNsWfk8WrcFrQVImAtKlCev9QepqfBmDh+FgXTvfysoBCSxEBS626TvYms3OhI0LOO4A==",
+
      "version": "2.0.0-rc.10",
+
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.0.0-rc.10.tgz",
+
      "integrity": "sha512-RByz0zRbngps5QMQVsbgCD03TiCMxwAhaZhNtojXQ2AiJFkv1Mu68W/prbpWucw6Ep1nM3/yTIm0aL6ozdh/gw==",
      "cpu": [
        "arm"
      ],
@@ -972,9 +950,9 @@
      }
    },
    "node_modules/@tauri-apps/cli-linux-arm64-gnu": {
-
      "version": "2.0.0-rc.1",
-
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.0.0-rc.1.tgz",
-
      "integrity": "sha512-TGmadGW8BjTq864AYrv/u+vTnAodnOuzv1VCUOV23O8st35GZG6V47jwNsSjQjhrcO1XzmJiRAtrcVKuTZ/xUA==",
+
      "version": "2.0.0-rc.10",
+
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.0.0-rc.10.tgz",
+
      "integrity": "sha512-ZqpbDIMp5b0jz1ddutJH6S5geLaBEmsMG6eZix+MgcZZRyEfahTMGCq3xkvv+tnrNNq7drvwBISCVSSS0zu3wQ==",
      "cpu": [
        "arm64"
      ],
@@ -988,9 +966,9 @@
      }
    },
    "node_modules/@tauri-apps/cli-linux-arm64-musl": {
-
      "version": "2.0.0-rc.1",
-
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.0-rc.1.tgz",
-
      "integrity": "sha512-+SJsRTW0PvvD7awEn+tIPJ3s12s6RpKXdOib2mztoKocasrGRfz+EFZuXc42Iwk3xROsrQkiw2UAmcNLkW5NwA==",
+
      "version": "2.0.0-rc.10",
+
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.0-rc.10.tgz",
+
      "integrity": "sha512-EVh1xPqs5bi0aBYbv6Iy1ooFClyK6/wIsNw9DyxWwhPz9I7UNpDAgHm6lOhkMH26Cp/eQPiEA8OdfOLTfCY81A==",
      "cpu": [
        "arm64"
      ],
@@ -1004,9 +982,9 @@
      }
    },
    "node_modules/@tauri-apps/cli-linux-x64-gnu": {
-
      "version": "2.0.0-rc.1",
-
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.0.0-rc.1.tgz",
-
      "integrity": "sha512-vjPrj2btS97IOp6cU42IrkI49SQZDSg8TPqwOwFqyQeAotCT1i0F38pLZWe1gLyPUowO8XdaaBdwYg6IRDKcZg==",
+
      "version": "2.0.0-rc.10",
+
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.0.0-rc.10.tgz",
+
      "integrity": "sha512-ZLcXJbRRMfgSkZdxBegP/4PlXkoVR1zpx2pE7mKkRgyvwJCx+A2f0+IZM+VVu/WRECxAdzVCbgxztTAOoLkdrg==",
      "cpu": [
        "x64"
      ],
@@ -1020,9 +998,9 @@
      }
    },
    "node_modules/@tauri-apps/cli-linux-x64-musl": {
-
      "version": "2.0.0-rc.1",
-
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.0-rc.1.tgz",
-
      "integrity": "sha512-MrE68/u6rMrkM1uM/DR1MNnMXiYebhSPGqqxshJ12SmFdk3yQ/Z73Wzvk8xv78eOExh4lTtEXI22YwaBCf9AEg==",
+
      "version": "2.0.0-rc.10",
+
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.0-rc.10.tgz",
+
      "integrity": "sha512-IgzRemlQT+SHfb2x8kq32xKGnR3r7S69Ogv5pBKIDX1/G2qQofM6wfy0OHnAyS4Bj0y2lZPjiYQBwmLIkK/BNw==",
      "cpu": [
        "x64"
      ],
@@ -1036,9 +1014,9 @@
      }
    },
    "node_modules/@tauri-apps/cli-win32-arm64-msvc": {
-
      "version": "2.0.0-rc.1",
-
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.0.0-rc.1.tgz",
-
      "integrity": "sha512-SLulbiUjg8BGf/zX+U1PGjB+JpsN2nLRGuW07BYwSDW3s3mp2aagLuOwaTaOPBrDzfIMRYq8KT54A4jfRjEZlg==",
+
      "version": "2.0.0-rc.10",
+
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.0.0-rc.10.tgz",
+
      "integrity": "sha512-RmSh5omDiCEZgw1fOhdEFi6MzAQ1rQBmvTM13K2p8XUxxaYb/MHYYZbNEMqxqWvsg4fidZ8hNSqRkB7YCCWWgg==",
      "cpu": [
        "arm64"
      ],
@@ -1052,9 +1030,9 @@
      }
    },
    "node_modules/@tauri-apps/cli-win32-ia32-msvc": {
-
      "version": "2.0.0-rc.1",
-
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.0.0-rc.1.tgz",
-
      "integrity": "sha512-rz85riTjcWdZlgTku6HcBx625Otdc0/NwDjRXgdXakL1ybw7E+G5YlLZNcQX25u17RKUAWX/2/VZ1pSz945Ovw==",
+
      "version": "2.0.0-rc.10",
+
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.0.0-rc.10.tgz",
+
      "integrity": "sha512-6zxZ1KnKqflC5YpJmXZyNNVaRXMdOiRijimua8zLzfoAo+adb6gd8V4o03rZF3BPHtmd35rPkZHlgMlg/th2Bw==",
      "cpu": [
        "ia32"
      ],
@@ -1068,9 +1046,9 @@
      }
    },
    "node_modules/@tauri-apps/cli-win32-x64-msvc": {
-
      "version": "2.0.0-rc.1",
-
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.0.0-rc.1.tgz",
-
      "integrity": "sha512-aRO70dDbn4w3CbALMG+b7g460xmbSTuUiGmRh0r/MNVeoZk/YbqluBUyhXdWGxJb8OVubw/4RlczKYcPmJceMw==",
+
      "version": "2.0.0-rc.10",
+
      "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.0.0-rc.10.tgz",
+
      "integrity": "sha512-D7L9QnxUJcSykQ9S8AQ0CEdxaw3IMoyAwv2LR7x+w/j7Jg3UsEgnsX5ePkShBiqSmu/UXfSuQeGvAoA8kSLiUw==",
      "cpu": [
        "x64"
      ],
@@ -1084,28 +1062,19 @@
      }
    },
    "node_modules/@tauri-apps/plugin-shell": {
-
      "version": "2.0.0-beta.8",
-
      "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0-beta.8.tgz",
-
      "integrity": "sha512-rFXI6MvsCdSGbuKbDu/NaOePREb9YTVTdeugHdvvljnKWW3dvmThBb2h/8Hxj+Z7Cd8MUoRxPeJWUZbPbJ2Imw==",
+
      "version": "2.0.0-rc.1",
+
      "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0-rc.1.tgz",
+
      "integrity": "sha512-JtNROc0rqEwN/g93ig5pK4cl1vUo2yn+osCpY9de64cy/d9hRzof7AuYOgvt/Xcd5VPQmlgo2AGvUh5sQRSR1A==",
      "dependencies": {
-
        "@tauri-apps/api": "2.0.0-beta.15"
+
        "@tauri-apps/api": "^2.0.0-rc.4"
      }
    },
    "node_modules/@tauri-apps/plugin-window-state": {
-
      "version": "2.0.0-rc.0",
-
      "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-window-state/-/plugin-window-state-2.0.0-rc.0.tgz",
-
      "integrity": "sha512-lR8reD+D1yIHT+53v56WltLS0+Y2zIkKqTuwrvz1yNbY5Hk4Z6foFV2Byo4kJAAvi5vbeGtvxYAjSiczZK5euw==",
+
      "version": "2.0.0-rc.1",
+
      "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-window-state/-/plugin-window-state-2.0.0-rc.1.tgz",
+
      "integrity": "sha512-fQG6G6G+b3mx2QE6dAFxl3iyKvz35DpGggIczKn+qRc4Mdjsb9y42iJMUpMpZAC2q9j8h3LfknguCacifVP5lA==",
      "dependencies": {
-
        "@tauri-apps/api": "^2.0.0-rc.0"
-
      }
-
    },
-
    "node_modules/@tauri-apps/plugin-window-state/node_modules/@tauri-apps/api": {
-
      "version": "2.0.0-rc.2",
-
      "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.0-rc.2.tgz",
-
      "integrity": "sha512-qIJJ1gKkzpPmEmIGTpja6XhuCD8A+vEh/wKaO7TzbxKiws5w3E+Kg4sBtwrH85hjMyqT0SgAUVaUk9XS7FBg3g==",
-
      "funding": {
-
        "type": "opencollective",
-
        "url": "https://opencollective.com/tauri"
+
        "@tauri-apps/api": "^2.0.0-rc.4"
      }
    },
    "node_modules/@tsconfig/svelte": {
@@ -1120,17 +1089,6 @@
      "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
      "dev": true
    },
-
    "node_modules/@types/node": {
-
      "version": "22.5.0",
-
      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.0.tgz",
-
      "integrity": "sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==",
-
      "dev": true,
-
      "optional": true,
-
      "peer": true,
-
      "dependencies": {
-
        "undici-types": "~6.19.2"
-
      }
-
    },
    "node_modules/@typescript-eslint/eslint-plugin": {
      "version": "8.4.0",
      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.4.0.tgz",
@@ -1337,6 +1295,18 @@
        "url": "https://opencollective.com/typescript-eslint"
      }
    },
+
    "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+
      "version": "3.4.3",
+
      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+
      "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+
      "dev": true,
+
      "engines": {
+
        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+
      },
+
      "funding": {
+
        "url": "https://opencollective.com/eslint"
+
      }
+
    },
    "node_modules/acorn": {
      "version": "8.12.1",
      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
@@ -1411,6 +1381,18 @@
        "node": ">= 8"
      }
    },
+
    "node_modules/anymatch/node_modules/picomatch": {
+
      "version": "2.3.1",
+
      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+
      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+
      "dev": true,
+
      "engines": {
+
        "node": ">=8.6"
+
      },
+
      "funding": {
+
        "url": "https://github.com/sponsors/jonschlinkert"
+
      }
+
    },
    "node_modules/argparse": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -1435,6 +1417,12 @@
        "node": ">= 0.4"
      }
    },
+
    "node_modules/baconjs": {
+
      "version": "3.0.19",
+
      "resolved": "https://registry.npmjs.org/baconjs/-/baconjs-3.0.19.tgz",
+
      "integrity": "sha512-/h7R6hTql8yk1FxYk/bTALea7fGcSJrUoLHFhX1WEkfI4C2mbR4sPbaNd0EhUIDJi3QwTBWEFHh7xEAaz3A3/A==",
+
      "dev": true
+
    },
    "node_modules/balanced-match": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1524,6 +1512,18 @@
        "fsevents": "~2.3.2"
      }
    },
+
    "node_modules/chokidar/node_modules/glob-parent": {
+
      "version": "5.1.2",
+
      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+
      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+
      "dev": true,
+
      "dependencies": {
+
        "is-glob": "^4.0.1"
+
      },
+
      "engines": {
+
        "node": ">= 6"
+
      }
+
    },
    "node_modules/code-red": {
      "version": "1.0.4",
      "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz",
@@ -1601,9 +1601,9 @@
      }
    },
    "node_modules/debug": {
-
      "version": "4.3.5",
-
      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
-
      "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
+
      "version": "4.3.6",
+
      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
+
      "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
      "dev": true,
      "dependencies": {
        "ms": "2.1.2"
@@ -1812,34 +1812,6 @@
      }
    },
    "node_modules/eslint-scope": {
-
      "version": "7.2.2",
-
      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
-
      "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
-
      "dev": true,
-
      "dependencies": {
-
        "esrecurse": "^4.3.0",
-
        "estraverse": "^5.2.0"
-
      },
-
      "engines": {
-
        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-
      },
-
      "funding": {
-
        "url": "https://opencollective.com/eslint"
-
      }
-
    },
-
    "node_modules/eslint-visitor-keys": {
-
      "version": "3.4.3",
-
      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
-
      "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
-
      "dev": true,
-
      "engines": {
-
        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-
      },
-
      "funding": {
-
        "url": "https://opencollective.com/eslint"
-
      }
-
    },
-
    "node_modules/eslint/node_modules/eslint-scope": {
      "version": "8.0.2",
      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz",
      "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==",
@@ -1855,7 +1827,7 @@
        "url": "https://opencollective.com/eslint"
      }
    },
-
    "node_modules/eslint/node_modules/eslint-visitor-keys": {
+
    "node_modules/eslint-visitor-keys": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
      "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
@@ -1867,7 +1839,7 @@
        "url": "https://opencollective.com/eslint"
      }
    },
-
    "node_modules/eslint/node_modules/espree": {
+
    "node_modules/espree": {
      "version": "10.1.0",
      "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz",
      "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==",
@@ -1884,35 +1856,6 @@
        "url": "https://opencollective.com/eslint"
      }
    },
-
    "node_modules/eslint/node_modules/glob-parent": {
-
      "version": "6.0.2",
-
      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
-
      "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
-
      "dev": true,
-
      "dependencies": {
-
        "is-glob": "^4.0.3"
-
      },
-
      "engines": {
-
        "node": ">=10.13.0"
-
      }
-
    },
-
    "node_modules/espree": {
-
      "version": "9.6.1",
-
      "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
-
      "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
-
      "dev": true,
-
      "dependencies": {
-
        "acorn": "^8.9.0",
-
        "acorn-jsx": "^5.3.2",
-
        "eslint-visitor-keys": "^3.4.1"
-
      },
-
      "engines": {
-
        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
-
      },
-
      "funding": {
-
        "url": "https://opencollective.com/eslint"
-
      }
-
    },
    "node_modules/esquery": {
      "version": "1.6.0",
      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
@@ -1986,6 +1929,18 @@
        "node": ">=8.6.0"
      }
    },
+
    "node_modules/fast-glob/node_modules/glob-parent": {
+
      "version": "5.1.2",
+
      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+
      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+
      "dev": true,
+
      "dependencies": {
+
        "is-glob": "^4.0.1"
+
      },
+
      "engines": {
+
        "node": ">= 6"
+
      }
+
    },
    "node_modules/fast-json-stable-stringify": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -2007,6 +1962,20 @@
        "reusify": "^1.0.4"
      }
    },
+
    "node_modules/fdir": {
+
      "version": "6.3.0",
+
      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.3.0.tgz",
+
      "integrity": "sha512-QOnuT+BOtivR77wYvCWHfGt9s4Pz1VIMbD463vegT5MLqNXy8rYFT/lPVEqf/bhYeT6qmqrNHhsX+rWwe3rOCQ==",
+
      "dev": true,
+
      "peerDependencies": {
+
        "picomatch": "^3 || ^4"
+
      },
+
      "peerDependenciesMeta": {
+
        "picomatch": {
+
          "optional": true
+
        }
+
      }
+
    },
    "node_modules/file-entry-cache": {
      "version": "8.0.0",
      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -2081,15 +2050,15 @@
      }
    },
    "node_modules/glob-parent": {
-
      "version": "5.1.2",
-
      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
-
      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+
      "version": "6.0.2",
+
      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+
      "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
      "dev": true,
      "dependencies": {
-
        "is-glob": "^4.0.1"
+
        "is-glob": "^4.0.3"
      },
      "engines": {
-
        "node": ">= 6"
+
        "node": ">=10.13.0"
      }
    },
    "node_modules/globals": {
@@ -2120,9 +2089,9 @@
      }
    },
    "node_modules/ignore": {
-
      "version": "5.3.1",
-
      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
-
      "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
+
      "version": "5.3.2",
+
      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+
      "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
      "dev": true,
      "engines": {
        "node": ">= 4"
@@ -2323,12 +2292,12 @@
      "dev": true
    },
    "node_modules/magic-string": {
-
      "version": "0.30.10",
-
      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz",
-
      "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==",
+
      "version": "0.30.11",
+
      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
+
      "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
      "dev": true,
      "dependencies": {
-
        "@jridgewell/sourcemap-codec": "^1.4.15"
+
        "@jridgewell/sourcemap-codec": "^1.5.0"
      }
    },
    "node_modules/mdn-data": {
@@ -2359,6 +2328,18 @@
        "node": ">=8.6"
      }
    },
+
    "node_modules/micromatch/node_modules/picomatch": {
+
      "version": "2.3.1",
+
      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+
      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+
      "dev": true,
+
      "engines": {
+
        "node": ">=8.6"
+
      },
+
      "funding": {
+
        "url": "https://github.com/sponsors/jonschlinkert"
+
      }
+
    },
    "node_modules/minimatch": {
      "version": "3.1.2",
      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -2508,18 +2489,20 @@
      }
    },
    "node_modules/picocolors": {
-
      "version": "1.0.1",
-
      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
-
      "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
+
      "version": "1.1.0",
+
      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
+
      "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
      "dev": true
    },
    "node_modules/picomatch": {
-
      "version": "2.3.1",
-
      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
-
      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+
      "version": "4.0.2",
+
      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
+
      "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
      "dev": true,
+
      "optional": true,
+
      "peer": true,
      "engines": {
-
        "node": ">=8.6"
+
        "node": ">=12"
      },
      "funding": {
        "url": "https://github.com/sponsors/jonschlinkert"
@@ -2625,9 +2608,9 @@
      }
    },
    "node_modules/postcss-selector-parser": {
-
      "version": "6.1.1",
-
      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz",
-
      "integrity": "sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==",
+
      "version": "6.1.2",
+
      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+
      "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
      "dev": true,
      "dependencies": {
        "cssesc": "^3.0.0",
@@ -2712,6 +2695,18 @@
        "node": ">=8.10.0"
      }
    },
+
    "node_modules/readdirp/node_modules/picomatch": {
+
      "version": "2.3.1",
+
      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+
      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+
      "dev": true,
+
      "engines": {
+
        "node": ">=8.6"
+
      },
+
      "funding": {
+
        "url": "https://github.com/sponsors/jonschlinkert"
+
      }
+
    },
    "node_modules/resolve-from": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -2927,34 +2922,6 @@
        "typescript": ">=5.0.0"
      }
    },
-
    "node_modules/svelte-check/node_modules/fdir": {
-
      "version": "6.3.0",
-
      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.3.0.tgz",
-
      "integrity": "sha512-QOnuT+BOtivR77wYvCWHfGt9s4Pz1VIMbD463vegT5MLqNXy8rYFT/lPVEqf/bhYeT6qmqrNHhsX+rWwe3rOCQ==",
-
      "dev": true,
-
      "peerDependencies": {
-
        "picomatch": "^3 || ^4"
-
      },
-
      "peerDependenciesMeta": {
-
        "picomatch": {
-
          "optional": true
-
        }
-
      }
-
    },
-
    "node_modules/svelte-check/node_modules/picomatch": {
-
      "version": "4.0.2",
-
      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
-
      "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
-
      "dev": true,
-
      "optional": true,
-
      "peer": true,
-
      "engines": {
-
        "node": ">=12"
-
      },
-
      "funding": {
-
        "url": "https://github.com/sponsors/jonschlinkert"
-
      }
-
    },
    "node_modules/svelte-eslint-parser": {
      "version": "0.41.0",
      "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.41.0.tgz",
@@ -2982,6 +2949,51 @@
        }
      }
    },
+
    "node_modules/svelte-eslint-parser/node_modules/eslint-scope": {
+
      "version": "7.2.2",
+
      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+
      "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+
      "dev": true,
+
      "dependencies": {
+
        "esrecurse": "^4.3.0",
+
        "estraverse": "^5.2.0"
+
      },
+
      "engines": {
+
        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+
      },
+
      "funding": {
+
        "url": "https://opencollective.com/eslint"
+
      }
+
    },
+
    "node_modules/svelte-eslint-parser/node_modules/eslint-visitor-keys": {
+
      "version": "3.4.3",
+
      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+
      "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+
      "dev": true,
+
      "engines": {
+
        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+
      },
+
      "funding": {
+
        "url": "https://opencollective.com/eslint"
+
      }
+
    },
+
    "node_modules/svelte-eslint-parser/node_modules/espree": {
+
      "version": "9.6.1",
+
      "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+
      "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+
      "dev": true,
+
      "dependencies": {
+
        "acorn": "^8.9.0",
+
        "acorn-jsx": "^5.3.2",
+
        "eslint-visitor-keys": "^3.4.1"
+
      },
+
      "engines": {
+
        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+
      },
+
      "funding": {
+
        "url": "https://opencollective.com/eslint"
+
      }
+
    },
    "node_modules/svelte-hmr": {
      "version": "0.16.0",
      "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.16.0.tgz",
@@ -2994,12 +3006,6 @@
        "svelte": "^3.19.0 || ^4.0.0"
      }
    },
-
    "node_modules/svelte-routing": {
-
      "version": "2.13.0",
-
      "resolved": "https://registry.npmjs.org/svelte-routing/-/svelte-routing-2.13.0.tgz",
-
      "integrity": "sha512-/NTxqTwLc7Dq306hARJrH2HLXOBtKd7hu8nxgoFDlK0AC4SOKnzisiX/9m8Uksei1QAWtlAEdF91YphNM8iDMg==",
-
      "dev": true
-
    },
    "node_modules/text-table": {
      "version": "0.2.0",
      "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -3084,14 +3090,6 @@
        }
      }
    },
-
    "node_modules/undici-types": {
-
      "version": "6.19.8",
-
      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
-
      "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
-
      "dev": true,
-
      "optional": true,
-
      "peer": true
-
    },
    "node_modules/uri-js": {
      "version": "4.4.1",
      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
modified package.json
@@ -27,6 +27,7 @@
    "@tauri-apps/cli": "^2.0.0-rc.1",
    "@tsconfig/svelte": "^5.0.4",
    "eslint": "^9.9.1",
+
    "baconjs": "^3.0.19",
    "eslint-config-prettier": "^9.1.0",
    "eslint-plugin-svelte": "^2.43.0",
    "prettier": "^3.3.3",
@@ -34,7 +35,6 @@
    "svelte": "^4.2.19",
    "svelte-check": "^4.0.0",
    "svelte-eslint-parser": "^0.41.0",
-
    "svelte-routing": "^2.13.0",
    "tslib": "^2.7.0",
    "typescript": "^5.2.2",
    "typescript-eslint": "^8.4.0",
modified public/index.css
@@ -15,10 +15,3 @@ html {
  height: 100%;
  width: 100%;
}
-

-
code {
-
  font-family: var(--font-family-monospace);
-
  font-size: var(--font-size-small);
-
  background-color: var(--color-fill-ghost);
-
  padding: 0.125rem 0.25rem;
-
}
modified src/App.svelte
@@ -1,16 +1,46 @@
<script lang="ts">
-
  import { Router, Route } from "svelte-routing";
+
  import { onMount } from "svelte";
+

+
  import { invoke } from "@tauri-apps/api/core";
+

+
  import * as router from "@app/lib/router";
  import { theme } from "@app/components/ThemeSwitch.svelte";
+
  import { unreachable } from "@app/lib/utils";

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

+
  const activeRouteStore = router.activeRouteStore;
+
  void router.loadFromLocation();
+

+
  onMount(async () => {
+
    try {
+
      await invoke("authenticate");
+
      void router.push({ resource: "home" });
+
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+
    } catch (e: any) {
+
      void router.push({
+
        resource: "authenticationError",
+
        params: {
+
          error: e.err,
+
          hint: e.hint,
+
        },
+
      });
+
    }
+
  });

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

-
<Router>
-
  <Route path="/" component={Startup} />
-
  <Route path="/repos" component={Repos} />
-
  <Route path="/design-system" component={DesignSystem} />
-
</Router>
+
{#if $activeRouteStore.resource === "booting"}
+
  <!-- Don't show anything -->
+
{:else if $activeRouteStore.resource === "home"}
+
  <Home />
+
{:else if $activeRouteStore.resource === "authenticationError"}
+
  <AuthenticationError {...$activeRouteStore.params} />
+
{:else if $activeRouteStore.resource === "designSystem"}
+
  <DesignSystem />
+
{:else}
+
  {unreachable($activeRouteStore)}
+
{/if}
modified src/components/Header.svelte
@@ -5,6 +5,8 @@
  import Icon from "./Icon.svelte";
  import Popover from "./Popover.svelte";
  import ThemeSwitch from "./ThemeSwitch.svelte";
+

+
  export let currentPage: string;
</script>

<style>
@@ -26,17 +28,30 @@
    gap: 0.5rem;
    padding: 0 0.5rem;
  }
+

+
  .navigation :global(svg:hover) {
+
    display: flex;
+
    color: var(--color-fill-secondary);
+
  }
</style>

<header class="flex-item">
  <div class="wrapper flex-item">
    <div class="wrapper-left flex-item">
-
      <div class="flex-item" style:gap="0.5rem">
-
        <Icon name="arrow-left" />
-
        <Icon name="arrow-right" />
+
      <div class="flex-item navigation" style:gap="0.5rem">
+
        <Icon
+
          name="arrow-left"
+
          on:click={() => {
+
            window.history.back();
+
          }} />
+
        <Icon
+
          name="arrow-right"
+
          on:click={() => {
+
            window.history.forward();
+
          }} />
      </div>
      <Fill variant="ghost" stylePadding="0 0.5rem" styleHeight="32px">
-
        Repositories
+
        {currentPage}
      </Fill>
      <Border variant="ghost" stylePadding="0 0.25rem" styleHeight="32px">
        <Icon name="plus" />
modified src/components/Icon.svelte
@@ -26,6 +26,9 @@
  svg {
    display: flex;
    flex-shrink: 0;
+
    -webkit-touch-callout: none;
+
    -webkit-user-select: none;
+
    user-select: none;
  }
</style>

added src/components/Link.svelte
@@ -0,0 +1,39 @@
+
<script lang="ts" strictEvents>
+
  import type { Route } from "@app/lib/router/definitions";
+

+
  import { createEventDispatcher } from "svelte";
+
  import { push, routeToPath } from "@app/lib/router";
+

+
  export let route: Route;
+
  export let disabled: boolean = false;
+

+
  const dispatch = createEventDispatcher<{
+
    afterNavigate: null;
+
  }>();
+

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

+
    void push(route);
+
    dispatch("afterNavigate");
+
  }
+
</script>
+

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

+
<a on:click={navigateToRoute} href={routeToPath(route)}>
+
  <slot />
+
</a>
added src/lib/mutexExecutor.ts
@@ -0,0 +1,113 @@
+
// Copyright ยฉ 2021 The Radicle Upstream Contributors
+
//
+
// This file is part of radicle-upstream, distributed under the GPLv3
+
// with Radicle Linking Exception. For full terms see the included
+
// LICENSE file.
+

+
//@ts-expect-error the typescript bindings are out of date.
+
import * as Bacon from "baconjs";
+

+
// A task executor that runs only one task concurrently. If a new task
+
// is run, any previously running task is aborted and the promise
+
// returned from `run()` will return undefined.
+
//
+
//     import * as mutexExecutor from "ui/src/mutexExecutor"
+
//     const executor = mutexExecutor.create()
+
//     const first = await executor.run(async () => {
+
//       await sleep(1000)
+
//       return "first"
+
//     })
+
//     const second = await executor.run(async () => "second")
+
//
+
// In the example above the promise `first` will resolve to `undefined`
+
// while the promise `second` will resolve to "second".
+
//
+
// If the first tasks throws after the second task has run the
+
// behavior is the same.
+
//
+
//     const first = await executor.run(async () => {
+
//       await sleep(1000)
+
//       throw new Error()
+
//     })
+
//
+
// The task call back receives an AbortSignal as a parameter. The abort
+
// event is emitted when another task is run.
+
export function create(): MutexExecutor {
+
  return new MutexExecutor();
+
}
+

+
// A worker that asynchronously process one item at a time and provides
+
// the result as an event stream.
+
//
+
//     import * as mutexExecutor from "ui/src/mutexExecutor"
+
//     const worker = mutexExecutor.createWorker(async (value) => {
+
//       await sleep(1000)
+
//       return value
+
//     })
+
//
+
//     const firstPromise = worker.output.firstToPromise()
+
//     worker.push("first)
+
//     assert.equal(await firstPromise, "first")
+
//
+
// When an item is submitted to the worker while the previous items is
+
// still being processed, the result of the first item will not be
+
// emitted to `worker.output`. Instead, only the last item will be
+
// emitted.
+
export function createWorker<In, Out>(
+
  fn: (x: In, abortSignal: AbortSignal) => Promise<Out>,
+
): MutexWorker<In, Out> {
+
  return new MutexWorker(fn);
+
}
+

+
class MutexExecutor {
+
  private runningTaskId = 0;
+
  private abortController: AbortController | null = null;
+

+
  public async run<T>(
+
    f: (abortSignal: AbortSignal) => Promise<T>,
+
  ): Promise<T | undefined> {
+
    this.runningTaskId += 1;
+
    const taskId = this.runningTaskId;
+

+
    if (this.abortController) {
+
      this.abortController.abort();
+
    }
+
    this.abortController = new AbortController();
+
    return f(this.abortController.signal).then(
+
      data => {
+
        if (this.runningTaskId === taskId) {
+
          return data;
+
        } else {
+
          return undefined;
+
        }
+
      },
+
      err => {
+
        if (this.runningTaskId === taskId) {
+
          throw err;
+
        } else {
+
          return undefined;
+
        }
+
      },
+
    );
+
  }
+
}
+

+
class MutexWorker<In, Out> {
+
  private outputBus = new Bacon.Bus<Out>();
+
  private executor = new MutexExecutor();
+

+
  public output: Bacon.EventStream<Out>;
+

+
  public constructor(
+
    private fn: (x: In, abortSignal: AbortSignal) => Promise<Out>,
+
  ) {
+
    this.output = this.outputBus.toEventStream();
+
  }
+

+
  public async submit(x: In): Promise<void> {
+
    const output = await this.executor.run(abort => this.fn(x, abort));
+
    if (output !== undefined) {
+
      this.outputBus.push(output);
+
    }
+
  }
+
}
added src/lib/router.ts
@@ -0,0 +1,150 @@
+
import type { LoadedRoute, Route } from "@app/lib/router/definitions";
+

+
import { get, writable } from "svelte/store";
+

+
import * as mutexExecutor from "@app/lib/mutexExecutor";
+
import * as utils from "@app/lib/utils";
+
import { loadRoute } from "@app/lib/router/definitions";
+

+
export { type Route };
+

+
const InitialStore = { resource: "booting" as const };
+

+
export const isLoading = writable<boolean>(true);
+
export const activeRouteStore = writable<LoadedRoute>(InitialStore);
+
export const activeUnloadedRouteStore = writable<Route>(InitialStore);
+

+
let currentUrl: URL | undefined;
+

+
export async function loadFromLocation(): Promise<void> {
+
  await navigateToUrl("replace", new URL(window.location.href));
+
}
+

+
async function navigateToUrl(
+
  action: "push" | "replace",
+
  url: URL,
+
): Promise<void> {
+
  const { pathname, hash } = url;
+

+
  if (url.origin !== window.origin) {
+
    throw new Error("Cannot navigate to other origin");
+
  }
+

+
  if (
+
    currentUrl &&
+
    currentUrl.pathname === pathname &&
+
    currentUrl.search === url.search
+
  ) {
+
    return;
+
  }
+

+
  const relativeUrl = pathname + url.search + (hash || "");
+
  url = new URL(relativeUrl, window.origin);
+
  const route = urlToRoute(url);
+

+
  if (route) {
+
    await navigate(action, route);
+
  } else {
+
    await navigate(action, {
+
      // On 404 we redirect to the Home page, shouldn't happen.
+
      resource: "home",
+
    });
+
  }
+
}
+

+
window.addEventListener("popstate", () => loadFromLocation());
+

+
const loadExecutor = mutexExecutor.create();
+

+
async function navigate(
+
  action: "push" | "replace",
+
  newRoute: Route,
+
): Promise<void> {
+
  isLoading.set(true);
+
  const path = routeToPath(newRoute);
+

+
  if (action === "push") {
+
    window.history.pushState(newRoute, "", path);
+
  } else if (action === "replace") {
+
    window.history.replaceState(newRoute, "");
+
  }
+
  currentUrl = new URL(window.location.href);
+
  const currentLoadedRoute = get(activeRouteStore);
+

+
  const loadedRoute = await loadExecutor.run(async () => {
+
    return loadRoute(newRoute, currentLoadedRoute);
+
  });
+

+
  // Only let the last request through.
+
  if (loadedRoute === undefined) {
+
    return;
+
  }
+

+
  setTitle(loadedRoute);
+
  activeRouteStore.set(loadedRoute);
+
  activeUnloadedRouteStore.set(newRoute);
+
  isLoading.set(false);
+
}
+

+
function setTitle(loadedRoute: LoadedRoute) {
+
  const title: string[] = [];
+

+
  if (loadedRoute.resource === "booting") {
+
    title.push("Radicle");
+
  } else if (loadedRoute.resource === "home") {
+
    title.push("Home");
+
    title.push("Radicle");
+
  } else if (loadedRoute.resource === "authenticationError") {
+
    title.push("Authentication Error");
+
    title.push("Radicle");
+
  } else if (loadedRoute.resource === "designSystem") {
+
    title.push("Design System");
+
    title.push("Radicle");
+
  } else {
+
    utils.unreachable(loadedRoute);
+
  }
+

+
  document.title = title.join(" ยท ");
+
}
+

+
export async function push(newRoute: Route): Promise<void> {
+
  await navigate("push", newRoute);
+
}
+

+
export async function replace(newRoute: Route): Promise<void> {
+
  await navigate("replace", newRoute);
+
}
+

+
function urlToRoute(url: URL): Route | null {
+
  const segments = url.pathname.substring(1).split("/");
+

+
  const resource = segments.shift();
+
  switch (resource) {
+
    case "": {
+
      return { resource: "home" };
+
    }
+
    case "authenticationError": {
+
      return { resource: "authenticationError", params: { error: "" } };
+
    }
+
    case "designSystem": {
+
      return { resource: "designSystem" };
+
    }
+
    default: {
+
      return null;
+
    }
+
  }
+
}
+

+
export function routeToPath(route: Route): string {
+
  if (route.resource === "home") {
+
    return "/";
+
  } else if (route.resource === "authenticationError") {
+
    return "/authenticationError";
+
  } else if (route.resource === "designSystem") {
+
    return "/designSystem";
+
  } else if (route.resource === "booting") {
+
    return "";
+
  } else {
+
    return utils.unreachable(route);
+
  }
+
}
added src/lib/router/definitions.ts
@@ -0,0 +1,38 @@
+
interface BootingRoute {
+
  resource: "booting";
+
}
+

+
interface AuthenticationErrorRoute {
+
  resource: "authenticationError";
+
  params: {
+
    error: string;
+
    hint?: string;
+
  };
+
}
+

+
interface HomeRoute {
+
  resource: "home";
+
}
+

+
interface DesignSystemRoute {
+
  resource: "designSystem";
+
}
+

+
export type Route =
+
  | BootingRoute
+
  | DesignSystemRoute
+
  | HomeRoute
+
  | AuthenticationErrorRoute;
+

+
export type LoadedRoute =
+
  | BootingRoute
+
  | DesignSystemRoute
+
  | HomeRoute
+
  | AuthenticationErrorRoute;
+

+
export async function loadRoute(
+
  route: Route,
+
  _previousLoaded: LoadedRoute,
+
): Promise<LoadedRoute> {
+
  return route;
+
}
added src/views/AuthenticationError.svelte
@@ -0,0 +1,36 @@
+
<script lang="ts">
+
  import Icon from "@app/components/Icon.svelte";
+

+
  export let error: string;
+
  export let hint: string | undefined = undefined;
+
</script>
+

+
<style>
+
  main {
+
    display: flex;
+
    flex-direction: column;
+
    justify-content: center;
+
    align-items: center;
+
    height: 100%;
+
    width: 100%;
+
    row-gap: 0.5rem;
+
  }
+

+
  /* This tag comes from the backend. */
+
  :global(code) {
+
    font-family: var(--font-family-monospace);
+
    font-size: var(--font-size-small);
+
    background-color: var(--color-fill-ghost);
+
    padding: 0.125rem 0.25rem;
+
  }
+
</style>
+

+
<main>
+
  <Icon name="warning" size="32" />
+
  <div class="txt-medium txt-semibold">
+
    {error}
+
  </div>
+
  {#if hint}
+
    <div class="txt-small">{@html hint}</div>
+
  {/if}
+
</main>
modified src/views/DesignSystem.svelte
@@ -1,42 +1,18 @@
<script lang="ts">
-
  import Border from "@app/components/Border.svelte";
  import Button from "@app/components/Button.svelte";
-
  import Fill from "@app/components/Fill.svelte";
+
  import Header from "@app/components/Header.svelte";
  import Icon from "@app/components/Icon.svelte";
-

-
  let theme = "dark";
-

-
  $: document.documentElement.setAttribute("data-theme", theme);
+
  import Link from "@app/components/Link.svelte";
</script>

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

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

+
<div style="display: flex; gap: 1rem;">
  <div style="display: flex; gap: 1rem;">
    <Button variant="primary"><Icon name="seedling" />Press me</Button>
    <Button variant="secondary"><Icon name="seedling" /> Press me</Button>
    <Button variant="ghost"><Icon name="seedling" /> Press me</Button>
  </div>
</div>
+

+
๐Ÿ‘‰ <Link route={{ resource: "home" }}>Home</Link>
added src/views/Home.svelte
@@ -0,0 +1,8 @@
+
<script lang="ts">
+
  import Header from "@app/components/Header.svelte";
+
  import Link from "@app/components/Link.svelte";
+
</script>
+

+
<Header currentPage="Repositories" />
+

+
๐Ÿ‘‰ <Link route={{ resource: "designSystem" }}>Design System</Link>
deleted src/views/Startup.svelte
@@ -1,47 +0,0 @@
-
<script lang="ts">
-
  import { invoke } from "@tauri-apps/api/core";
-
  import { onMount } from "svelte";
-

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

-
  let loading = true;
-
  let error: { err: string; hint?: string } | undefined = undefined;
-

-
  onMount(async () => {
-
    try {
-
      await invoke("authenticate");
-
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
-
    } catch (e: any) {
-
      error = e;
-
    } finally {
-
      loading = false;
-
    }
-
  });
-
</script>
-

-
<style>
-
  main {
-
    display: flex;
-
    flex-direction: column;
-
    justify-content: center;
-
    align-items: center;
-
    height: 100%;
-
    width: 100%;
-
    row-gap: 0.5rem;
-
  }
-
</style>
-

-
{#if error}
-
  <main>
-
    <Icon name="warning" size="32" />
-
    <div class="txt-medium txt-semibold">
-
      {error.err}
-
    </div>
-
    {#if error.hint}
-
      <div class="txt-small">{@html error.hint}</div>
-
    {/if}
-
  </main>
-
{:else if !loading}
-
  <Repos />
-
{/if}