Add repo fuzzy finder to home screen

This commit is contained in:
Rūdolfs Ošiņš
2025-03-28 09:03:17 +01:00
parent 5c5685d560
commit 8a3e480b3c
6 changed files with 104 additions and 13 deletions
+25
View File
@@ -38,9 +38,11 @@
"eslint-config-prettier": "^10.1.1", "eslint-config-prettier": "^10.1.1",
"eslint-plugin-svelte": "^3.3.2", "eslint-plugin-svelte": "^3.3.2",
"execa": "^9.5.2", "execa": "^9.5.2",
"fuzzysort": "^3.1.0",
"get-port": "^7.1.0", "get-port": "^7.1.0",
"happy-dom": "^17.4.4", "happy-dom": "^17.4.4",
"hast-util-to-dom": "^4.0.1", "hast-util-to-dom": "^4.0.1",
"keyux": "^0.11.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"marked": "^15.0.7", "marked": "^15.0.7",
"marked-emoji": "^2.0.0", "marked-emoji": "^2.0.0",
@@ -2830,6 +2832,13 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/get-intrinsic": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -3260,6 +3269,22 @@
"katex": "cli.js" "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": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+2
View File
@@ -53,9 +53,11 @@
"eslint-config-prettier": "^10.1.1", "eslint-config-prettier": "^10.1.1",
"eslint-plugin-svelte": "^3.3.2", "eslint-plugin-svelte": "^3.3.2",
"execa": "^9.5.2", "execa": "^9.5.2",
"fuzzysort": "^3.1.0",
"get-port": "^7.1.0", "get-port": "^7.1.0",
"happy-dom": "^17.4.4", "happy-dom": "^17.4.4",
"hast-util-to-dom": "^4.0.1", "hast-util-to-dom": "^4.0.1",
"keyux": "^0.11.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"marked": "^15.0.7", "marked": "^15.0.7",
"marked-emoji": "^2.0.0", "marked-emoji": "^2.0.0",
+3 -2
View File
@@ -12,9 +12,10 @@
repo: RepoInfo; repo: RepoInfo;
selfDid: string; selfDid: string;
onclick?: () => void; 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"]!); const project = $derived(repo.payloads["xyz.radicle.project"]!);
</script> </script>
@@ -34,7 +35,7 @@
</style> </style>
<Border <Border
variant="ghost" variant={focussed ? "secondary" : "ghost"}
styleCursor="pointer" styleCursor="pointer"
styleWidth="100%" styleWidth="100%"
stylePadding="8px 12px" stylePadding="8px 12px"
+3
View File
@@ -16,6 +16,7 @@
onDismiss?: () => void; onDismiss?: () => void;
valid?: boolean; valid?: boolean;
oninput?: FormEventHandler<HTMLInputElement>; oninput?: FormEventHandler<HTMLInputElement>;
keyShortcuts?: string;
} }
/* eslint-disable prefer-const */ /* eslint-disable prefer-const */
@@ -31,6 +32,7 @@
onDismiss, onDismiss,
valid = true, valid = true,
oninput, oninput,
keyShortcuts,
}: Props = $props(); }: Props = $props();
/* eslint-enable prefer-const */ /* eslint-enable prefer-const */
@@ -93,6 +95,7 @@
variant={valid ? (focussed ? "secondary" : "ghost") : "danger"} variant={valid ? (focussed ? "secondary" : "ghost") : "danger"}
styleWidth="100%"> styleWidth="100%">
<input <input
aria-keyshortcuts={keyShortcuts}
onfocus={() => { onfocus={() => {
focussed = true; focussed = true;
}} }}
+4
View File
@@ -1,6 +1,10 @@
import { mount } from "svelte"; import { mount } from "svelte";
import { hotkeyKeyUX, hotkeyMacCompat, startKeyUX } from "keyux";
import App from "./App.svelte"; import App from "./App.svelte";
const app = mount(App, { target: document.body }); const app = mount(App, { target: document.body });
const mac = hotkeyMacCompat();
startKeyUX(window, [hotkeyKeyUX([mac])]);
export default app; export default app;
+67 -11
View File
@@ -6,17 +6,22 @@
import type { RepoCount } from "@bindings/repo/RepoCount"; import type { RepoCount } from "@bindings/repo/RepoCount";
import type { RepoInfo } from "@bindings/repo/RepoInfo"; import type { RepoInfo } from "@bindings/repo/RepoInfo";
import fuzzysort from "fuzzysort";
import * as router from "@app/lib/router"; 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 { dynamicInterval } from "@app/lib/interval";
import { invoke } from "@app/lib/invoke"; import { invoke } from "@app/lib/invoke";
import { onMount } from "svelte"; import { onMount } from "svelte";
import Border from "@app/components/Border.svelte";
import CopyableId from "@app/components/CopyableId.svelte"; import CopyableId from "@app/components/CopyableId.svelte";
import HomeSidebar from "@app/components/HomeSidebar.svelte"; import HomeSidebar from "@app/components/HomeSidebar.svelte";
import Icon from "@app/components/Icon.svelte";
import Layout from "@app/views/repo/Layout.svelte"; import Layout from "@app/views/repo/Layout.svelte";
import Onboarding from "@app/views/home/Onboarding.svelte"; import Onboarding from "@app/views/home/Onboarding.svelte";
import RepoCard from "@app/components/RepoCard.svelte"; import RepoCard from "@app/components/RepoCard.svelte";
import TextInput from "@app/components/TextInput.svelte";
interface Props { interface Props {
activeTab?: HomeReposTab; activeTab?: HomeReposTab;
@@ -63,6 +68,22 @@
invoke<Config>("config"), invoke<Config>("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<typeof item> => item !== undefined),
);
const searchResults = $derived(
fuzzysort.go(searchInput, searchableRepos, { key: "name", all: true }),
);
</script> </script>
<style> <style>
@@ -82,7 +103,6 @@
padding-right: 1.5rem; padding-right: 1.5rem;
align-items: center; align-items: center;
min-height: 40px; min-height: 40px;
margin-bottom: 0.5rem;
} }
</style> </style>
@@ -100,24 +120,60 @@
{notificationCount} /> {notificationCount} />
{/snippet} {/snippet}
<div class="container"> <div class="container">
<div class="header">Repositories</div> <div class="global-flex" style:margin-bottom="0.5rem">
<div class="header">Repositories</div>
<div style:margin-left="auto">
<TextInput
onSubmit={async () => {
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} />
</div>
</div>
{#if repos.length > 0} {#if repos.length > 0}
<div class="repo-grid"> {#if searchResults.length > 0}
{#each repos as repo} <div class="repo-grid">
{#if repo.payloads["xyz.radicle.project"]} {#each searchResults as result}
<RepoCard <RepoCard
{repo} focussed={searchResults.length === 1}
repo={result.obj.repo}
selfDid={didFromPublicKey(config.publicKey)} selfDid={didFromPublicKey(config.publicKey)}
onclick={() => { onclick={() => {
void router.push({ void router.push({
resource: "repo.issues", resource: "repo.issues",
rid: repo.rid, rid: result.obj.repo.rid,
status: "open", status: "open",
}); });
}} /> }} />
{/if} {/each}
{/each} </div>
</div> {:else}
<Border
variant="ghost"
styleAlignItems="center"
styleJustifyContent="center">
<div
class="global-flex"
style:height="74px"
style:justify-content="center">
<div class="txt-missing txt-small global-flex" style:gap="0.25rem">
<Icon name="none" />
No matching repositories.
</div>
</div>
</Border>
{/if}
{:else} {:else}
<Onboarding {reload} /> <Onboarding {reload} />
{/if} {/if}