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-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",
+2
View File
@@ -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",
+3 -2
View File
@@ -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"]!);
</script>
@@ -34,7 +35,7 @@
</style>
<Border
variant="ghost"
variant={focussed ? "secondary" : "ghost"}
styleCursor="pointer"
styleWidth="100%"
stylePadding="8px 12px"
+3
View File
@@ -16,6 +16,7 @@
onDismiss?: () => void;
valid?: boolean;
oninput?: FormEventHandler<HTMLInputElement>;
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%">
<input
aria-keyshortcuts={keyShortcuts}
onfocus={() => {
focussed = true;
}}
+4
View File
@@ -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;
+67 -11
View File
@@ -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>("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>
<style>
@@ -82,7 +103,6 @@
padding-right: 1.5rem;
align-items: center;
min-height: 40px;
margin-bottom: 0.5rem;
}
</style>
@@ -100,24 +120,60 @@
{notificationCount} />
{/snippet}
<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}
<div class="repo-grid">
{#each repos as repo}
{#if repo.payloads["xyz.radicle.project"]}
{#if searchResults.length > 0}
<div class="repo-grid">
{#each searchResults as result}
<RepoCard
{repo}
focussed={searchResults.length === 1}
repo={result.obj.repo}
selfDid={didFromPublicKey(config.publicKey)}
onclick={() => {
void router.push({
resource: "repo.issues",
rid: repo.rid,
rid: result.obj.repo.rid,
status: "open",
});
}} />
{/if}
{/each}
</div>
{/each}
</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}
<Onboarding {reload} />
{/if}