diff --git a/package-lock.json b/package-lock.json index 2900a8906..80e441efb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,9 +38,11 @@ "eslint-config-prettier": "^10.1.1", "eslint-plugin-svelte": "^3.3.2", "execa": "^9.5.2", + "fuzzysort": "^3.1.0", "get-port": "^7.1.0", "happy-dom": "^17.4.4", "hast-util-to-dom": "^4.0.1", + "keyux": "^0.11.1", "lodash": "^4.17.21", "marked": "^15.0.7", "marked-emoji": "^2.0.0", @@ -2830,6 +2832,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fuzzysort": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-3.1.0.tgz", + "integrity": "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==", + "dev": true, + "license": "MIT" + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -3260,6 +3269,22 @@ "katex": "cli.js" } }, + "node_modules/keyux": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/keyux/-/keyux-0.11.1.tgz", + "integrity": "sha512-FIPQNnbtYuGdVU/vL0X9fcJwtkuzOwhzceUezMZMgTwGDNA2tmO4A/uA4tt1hdb0Cu3S3o+dNm5ifA+/reNX2A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", diff --git a/package.json b/package.json index e0e1ceffc..bff7d28a6 100644 --- a/package.json +++ b/package.json @@ -53,9 +53,11 @@ "eslint-config-prettier": "^10.1.1", "eslint-plugin-svelte": "^3.3.2", "execa": "^9.5.2", + "fuzzysort": "^3.1.0", "get-port": "^7.1.0", "happy-dom": "^17.4.4", "hast-util-to-dom": "^4.0.1", + "keyux": "^0.11.1", "lodash": "^4.17.21", "marked": "^15.0.7", "marked-emoji": "^2.0.0", diff --git a/src/components/RepoCard.svelte b/src/components/RepoCard.svelte index 751beb180..ff563dbb4 100644 --- a/src/components/RepoCard.svelte +++ b/src/components/RepoCard.svelte @@ -12,9 +12,10 @@ repo: RepoInfo; selfDid: string; onclick?: () => void; + focussed?: boolean; } - const { repo, selfDid, onclick }: Props = $props(); + const { repo, selfDid, onclick, focussed }: Props = $props(); const project = $derived(repo.payloads["xyz.radicle.project"]!); @@ -34,7 +35,7 @@ void; valid?: boolean; oninput?: FormEventHandler; + keyShortcuts?: string; } /* eslint-disable prefer-const */ @@ -31,6 +32,7 @@ onDismiss, valid = true, oninput, + keyShortcuts, }: Props = $props(); /* eslint-enable prefer-const */ @@ -93,6 +95,7 @@ variant={valid ? (focussed ? "secondary" : "ghost") : "danger"} styleWidth="100%"> { focussed = true; }} diff --git a/src/main.ts b/src/main.ts index b15a904a7..a05a7540f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,10 @@ import { mount } from "svelte"; +import { hotkeyKeyUX, hotkeyMacCompat, startKeyUX } from "keyux"; import App from "./App.svelte"; const app = mount(App, { target: document.body }); +const mac = hotkeyMacCompat(); +startKeyUX(window, [hotkeyKeyUX([mac])]); + export default app; diff --git a/src/views/home/Repos.svelte b/src/views/home/Repos.svelte index 747a637f5..bad21f64d 100644 --- a/src/views/home/Repos.svelte +++ b/src/views/home/Repos.svelte @@ -6,17 +6,22 @@ import type { RepoCount } from "@bindings/repo/RepoCount"; import type { RepoInfo } from "@bindings/repo/RepoInfo"; + import fuzzysort from "fuzzysort"; + import * as router from "@app/lib/router"; - import { didFromPublicKey } from "@app/lib/utils"; + import { didFromPublicKey, modifierKey } from "@app/lib/utils"; import { dynamicInterval } from "@app/lib/interval"; import { invoke } from "@app/lib/invoke"; import { onMount } from "svelte"; + import Border from "@app/components/Border.svelte"; import CopyableId from "@app/components/CopyableId.svelte"; import HomeSidebar from "@app/components/HomeSidebar.svelte"; + import Icon from "@app/components/Icon.svelte"; import Layout from "@app/views/repo/Layout.svelte"; import Onboarding from "@app/views/home/Onboarding.svelte"; import RepoCard from "@app/components/RepoCard.svelte"; + import TextInput from "@app/components/TextInput.svelte"; interface Props { activeTab?: HomeReposTab; @@ -63,6 +68,22 @@ invoke("config"), ]); } + + let searchInput = $state(""); + + const searchableRepos = $derived( + repos + .flatMap(r => { + if (r.payloads["xyz.radicle.project"]) { + return { repo: r, name: r.payloads["xyz.radicle.project"].data.name }; + } + }) + .filter((item): item is NonNullable => item !== undefined), + ); + + const searchResults = $derived( + fuzzysort.go(searchInput, searchableRepos, { key: "name", all: true }), + ); @@ -100,24 +120,60 @@ {notificationCount} /> {/snippet}
-
Repositories
+
+
Repositories
+
+ { + if (searchResults.length === 1) { + await router.push({ + resource: "repo.issues", + rid: searchResults[0].obj.repo.rid, + status: "open", + }); + } + }} + onDismiss={() => { + searchInput = ""; + }} + placeholder={`Filter repositories ${modifierKey()} + f`} + keyShortcuts="ctrl+f" + bind:value={searchInput} /> +
+
{#if repos.length > 0} -
- {#each repos as repo} - {#if repo.payloads["xyz.radicle.project"]} + {#if searchResults.length > 0} +
+ {#each searchResults as result} { void router.push({ resource: "repo.issues", - rid: repo.rid, + rid: result.obj.repo.rid, status: "open", }); }} /> - {/if} - {/each} -
+ {/each} +
+ {:else} + +
+
+ + No matching repositories. +
+
+
+ {/if} {:else} {/if}