init cmd+k open search (#399)

* #398 init cmd+k open search

* #398 ctrl+shift+del clear search

---------

Co-authored-by: neil molina <neil@neils-MacBook-Pro.local>
This commit is contained in:
Neil 2023-04-06 10:30:49 +08:00 committed by GitHub
parent 01f2118093
commit d2cf2cb4a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 202 additions and 119 deletions

View file

@ -78,6 +78,7 @@
"@sentry/electron": "^4.3.0",
"@sentry/svelte": "^7.38.0",
"@types/electron": "^1.6.10",
"@types/mousetrap": "^1.6.11",
"@vitest/coverage-c8": "^0.27.1",
"axios": "^1.3.2",
"bcryptjs": "^2.4.3",
@ -97,6 +98,7 @@
"mixpanel-browser": "^2.45.0",
"mkdirp": "^2.1.3",
"moment": "^2.29.4",
"mousetrap": "^1.6.5",
"pushy-electron": "^1.0.11",
"renderer": "link:@types/electron/renderer",
"semver": "^7.3.8",

View file

@ -1,27 +1,28 @@
<script lang="ts">
import { searchStore } from '$libs/stores';
import type { GUIPackage } from '$libs/types';
import SearchInput from '@tea/ui/search-input/search-input.svelte';
import { t } from '$libs/translations';
import Preloader from '@tea/ui/Preloader/Preloader.svelte';
import Package from "$components/packages/package.svelte";
import { PackageStates } from '$libs/types';
import PackageResult from "./package-search-result.svelte";
import Mousetrap from 'mousetrap';
// import Posts from '@tea/ui/posts/posts.svelte';
import { installPackage } from '@native';
import { onMount } from 'svelte';
const { searching, packagesSearch } = searchStore;
// import type { AirtablePost } from '@tea/ui/types';
let term: string;
let packages: GUIPackage[] = [];
// let articles: AirtablePost[] = []; // news, blogs, etc
// let workshops: AirtablePost[] = []; // workshops, course
let loading = true;
searchStore.subscribe((v) => {
term = v;
});
searchStore.packagesSearch.subscribe((pkgs) => {
packages = pkgs;
});
// searchStore.packagesSearch.subscribe((pkgs) => {
// packages = pkgs;
// });
// searchStore.postsSearch.subscribe((posts) => {
// let partialArticles: AirtablePost[] = [];
// let partialWorkshops: AirtablePost[] = [];
@ -42,110 +43,141 @@
const onClose = () => {
term = '';
searchStore.searching.set(false);
};
</script>
<section class={term ? 'show' : ''}>
{#if $searching}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<figure on:click={onClose} />
<div class="border-gray z-20 border bg-black">
<header class="flex justify-between p-4">
<div class="text-primary text-2xl">Showing search results for `{term}`</div>
<button on:click={onClose}>&#x2715</button>
<div id="bg-close" class="z-40" on:click={onClose}></div>
<section class="z-50">
<header class="flex border border-gray border-t-0 border-x-0 bg-black">
<div class="relative w-full">
<SearchInput
class="w-full rounded-sm h-9"
size="small"
placeholder={`${$t("store-search-placeholder")}`}
onSearch={(search) => {
term = search;
searchStore.search(search);
}}
/>
<div class="absolute top-2 right-3 opacity-50 flex items-center gap-1">
<button class="text-xs mt-1">clear</button>
<kbd class=" bg-gray text-white px-2 mt-1 rounded-sm flex items-center">
<span class="text-xs">ctrl + shift + del</span>
</kbd>
</div>
</div>
<button class="mr-2" on:click={onClose}>&#x2715</button>
</header>
<header class="text-gray p-4 text-lg">
Packages ({packages.length})
</header>
<ul class="flex flex-col gap-2 p-2">
{#if packages.length > 0}
{#each packages as pkg}
<div class={pkg.state === PackageStates.INSTALLING ? 'animate-pulse' : ''}>
<PackageResult
{pkg}
{onClose}
ctaLabel={$t(`package.cta-${pkg.state}`)}
ctaColor={pkg.state === PackageStates.INSTALLED ? "green": "secondary"}
onClick={async () => {
try {
pkg.state = PackageStates.INSTALLING;
await installPackage(pkg, pkg.version);
pkg.state = PackageStates.INSTALLED;
} catch (error) {
console.error(error);
}
}}
/>
</div>
{/each}
{:else if loading}
{#each Array(12) as _}
<section class="h-50 border-gray border p-4">
{#if term}
<div class="z-20 bg-black">
<header class="text-gray p-4 text-lg">
Packages ({$packagesSearch.length})
</header>
<ul class="flex flex-col gap-2 p-2">
{#if $packagesSearch.length > 0}
{#each $packagesSearch as pkg}
<div class={pkg.state === PackageStates.INSTALLING ? 'animate-pulse' : ''}>
<PackageResult
{pkg}
{onClose}
onClick={async () => {
try {
pkg.state = PackageStates.INSTALLING;
await installPackage(pkg, pkg.version);
pkg.state = PackageStates.INSTALLED;
} catch (error) {
console.error(error);
}
}}
/>
</div>
{/each}
{/if}
</ul>
<!-- <header class="text-primary p-4 text-lg">
Top Article Results ({articles.length})
</header>
{#if articles.length}
<Posts posts={articles} linkTarget="_blank" />
{:else if loading}
<section class="border-gray h-64 border bg-black p-4">
<Preloader />
</section>
{/each}
{/if}
</ul>
<!-- <header class="text-primary p-4 text-lg">
Top Article Results ({articles.length})
</header>
{#if articles.length}
<Posts posts={articles} linkTarget="_blank" />
{:else if loading}
<section class="border-gray h-64 border bg-black p-4">
<Preloader />
</section>
{/if}
<header class="text-primary p-4 text-lg">
Top Workshop Results ({workshops.length})
</header>
{#if workshops.length}
<Posts posts={workshops} linkTarget="_blank" />
{:else if loading}
<section class="border-gray h-64 border bg-black p-4">
<Preloader />
</section>
{/if} -->
</div>
{:else}
<div class="w-full h-full flex flex-col justify-center bg-black">
<p class="text-gray text-center">start typing to search</p>
</div>
{/if}
<header class="text-primary p-4 text-lg">
Top Workshop Results ({workshops.length})
</header>
{#if workshops.length}
<Posts posts={workshops} linkTarget="_blank" />
{:else if loading}
<section class="border-gray h-64 border bg-black p-4">
<Preloader />
</section>
{/if} -->
</div>
</section>
</section>
{/if}
<style>
#bg-close {
position: fixed;
width: calc(100vw - 2px);
height: calc(100vh - 2px);
top: 1px;
left: 1px;
background: rgba(0, 0, 0, 0.7);
border-radius: 12px;
}
section {
position: fixed;
top: 48px;
left: 0px;
right: 0;
top: 50px;
left: 50px;
right: 50px;
bottom: 50px;
background: rgba(0, 0, 0, 0.7);
transition: opacity 0.3s ease-in-out;
opacity: 0%;
opacity: 1;
overflow: hidden;
height: 0px;
}
section.show {
padding: 36px;
z-index: 10;
}
section > figure {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
section.show {
opacity: 100%;
height: 100%;
height: auto;
border: gray 1px solid;
border-radius: 12px;
}
section > div {
position: relative;
height: 0%;
margin-top: 2px;
height: calc(100% - 40px);
width: 100%;
transition: height 0.6s ease-in-out;
overflow-y: scroll;
}
section.show > div {
height: 90%;
/* width */
::-webkit-scrollbar {
width: 6px;
}
/* Track */
::-webkit-scrollbar-track {
background: #272626;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: #949494;
border-radius: 4px;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: white;
}
</style>

View file

@ -10,10 +10,6 @@
let { nextPath, prevPath } = navStore;
const onSearch = (term: string) => {
searchStore.search(term);
};
let currentPath: string;
beforeUpdate(async () => {
currentPath = $page.url.pathname;
@ -28,13 +24,19 @@
<button on:click={navStore.back} class:active={$prevPath} class="opacity-50 pt-1 text-xs"><i class="icon-arrow-left"/></button>
<button on:click={navStore.next} class:active={$nextPath} class="opacity-50 pt-1 text-xs"><i class="icon-arrow-right"/></button>
</ul>
<div class="px-2 w-1/3">
<div class="px-2 w-1/3 relative">
<SearchInput
class="w-full border border-gray rounded-sm h-9"
size="small"
placeholder={`${$t("store-search-placeholder")}`}
{onSearch}
onFocus={() => {
searchStore.searching.set(true);
}}
/>
<kbd class="absolute top-0 right-3 opacity-50 bg-gray text-white px-2 mt-1 rounded-sm flex items-center">
<i class="text-lg"></i>
<span class="text-xs">K</span>
</kbd>
</div>
<ProfileNavButton />
</header>

View file

@ -95,44 +95,35 @@ export const postsStore = initPosts();
function initSearchStore() {
const searching = writable<boolean>(false);
const { subscribe, set } = writable<string>("");
const packagesSearch = writable<GUIPackage[]>([]);
const postsSearch = writable<AirtablePost[]>([]);
// TODO:
// should use algolia if user is somehow online
const packagesFound: GUIPackage[] = [];
let term = "";
subscribe((v) => (term = v));
packagesSearch.subscribe((v) => packagesFound.push(...v));
return {
term,
searching,
packagesSearch,
postsSearch,
packagesFound,
subscribe,
search: async (term: string) => {
searching.set(true);
try {
if (term) {
const [resultPkgs, resultPosts] = await Promise.all([
packagesStore.search(term, 5),
postsStore.search(term, 10)
const [
resultPkgs
// resultPosts
] = await Promise.all([
packagesStore.search(term, 5)
// postsStore.search(term, 10)
]);
packagesSearch.set(resultPkgs);
postsSearch.set(resultPosts);
// postsSearch.set(resultPosts);
} else {
packagesSearch.set([]);
postsSearch.set([]);
// postsSearch.set([]);
}
set(term);
} finally {
searching.set(false);
} catch (error) {
console.error(error);
}
}
};

View file

@ -6,8 +6,9 @@
import TopBar from '$components/top-bar/top-bar.svelte';
import PopoutMenu from '$components/popout-menu/popout-menu.svelte';
import Footer from '$components/footer/footer.svelte';
import { navStore, packagesStore } from '$libs/stores';
import { navStore, packagesStore, searchStore } from '$libs/stores';
import { listenToChannel } from "@native";
import Mousetrap from 'mousetrap';
import SearchPopupResults from '$components/search-popup-results/search-popup-results.svelte';
import { getProtocolPath } from '@native';
@ -17,8 +18,10 @@
let view: HTMLElement;
const { sideNavOpen, setNewPath } = navStore;
const { searching } = searchStore;
$: if ($navigating) view.scrollTop = 0;
afterNavigate(({ from, to }) => {
if (to && to?.route.id && from && from?.url) {
@ -38,6 +41,12 @@
// used by the tea:// protocol to suggest a path to open
syncPath();
listenToChannel("sync-path", syncPath);
Mousetrap.bind(['command+k', 'ctrl+k'], function() {
searchStore.searching.set(!$searching);
// return false to prevent default browser behavior
// and stop event from bubbling
return false;
});
packagesStore.init();
});
</script>

View file

@ -1,27 +1,55 @@
<script lang="ts">
import { onMount } from "svelte";
import "../app.css";
import Mousetrap from "mousetrap";
let clazz = "";
export { clazz as class };
export let size: "small" | "medium" | "large" = "small";
export let onSearch: (text: string) => void;
export let onSearch = (text: string) => {
console.log(text);
};
export let onFocus = () => {
console.log("focus");
};
export let placeholder = "search_";
let searchInput: HTMLInputElement;
let timer: NodeJS.Timeout;
const onChange = (e: KeyboardEvent) => {
const t = e.target as HTMLInputElement;
clearTimeout(timer);
timer = setTimeout(() => {
onSearch && onSearch(t.value);
onSearch(t.value);
}, 300);
};
onMount(() => {
searchInput.focus();
Mousetrap.bind(["ctrl+shift+del"], function () {
searchInput.value = "";
return false;
});
Mousetrap.prototype.stopCallback = () => {
return false;
};
});
</script>
<section class={`flex items-center pb-1 ${size} ${clazz}`}>
<div class="icon pl-4">
<i class="icon-search-icon" />
</div>
<input type="search" class="flex-grow pb-2 text-sm" {placeholder} on:keyup={onChange} />
<input
bind:this={searchInput}
type="search"
class="flex-grow pb-2 text-sm"
{placeholder}
on:keyup={onChange}
on:focus={onFocus}
/>
</section>
<!-- <input type="search" class="w-full bg-black h-12 p-4 border border-x-0 border-gray"/> -->
@ -72,4 +100,11 @@
color: #949494;
opacity: 1; /* Firefox */
}
input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-results-button,
input[type="search"]::-webkit-search-results-decoration {
display: none;
}
</style>

View file

@ -32,6 +32,7 @@ importers:
'@types/electron': ^1.6.10
'@types/js-yaml': ^4.0.5
'@types/mixpanel-browser': ^2.38.1
'@types/mousetrap': ^1.6.11
'@types/testing-library__jest-dom': ^5.14.5
'@typescript-eslint/eslint-plugin': ^5.27.0
'@typescript-eslint/parser': ^5.27.0
@ -64,6 +65,7 @@ importers:
mixpanel-browser: ^2.45.0
mkdirp: ^2.1.3
moment: ^2.29.4
mousetrap: ^1.6.5
postcss: ^8.4.19
prettier: ^2.7.1
prettier-plugin-svelte: ^2.7.0
@ -93,6 +95,7 @@ importers:
'@sentry/electron': 4.3.0
'@sentry/svelte': 7.38.0_svelte@3.55.1
'@types/electron': 1.6.10
'@types/mousetrap': 1.6.11
'@vitest/coverage-c8': 0.27.1_jsdom@21.0.0
axios: 1.3.2
bcryptjs: 2.4.3
@ -112,6 +115,7 @@ importers:
mixpanel-browser: 2.45.0
mkdirp: 2.1.3
moment: 2.29.4
mousetrap: 1.6.5
pushy-electron: 1.0.11
renderer: link:@types/electron/renderer
semver: 7.3.8
@ -4424,6 +4428,10 @@ packages:
resolution: {integrity: sha512-XzQbwgiOPsFXUQnjz3vSwcwrvJDbQ35bCiwa/1VXGrHvU1ti9+eqO1GY91DShzkEzKkkEEkxfNshS5dbBZqd7w==}
dev: true
/@types/mousetrap/1.6.11:
resolution: {integrity: sha512-F0oAily9Q9QQpv9JKxKn0zMKfOo36KHCW7myYsmUyf2t0g+sBTbG3UleTPoguHdE1z3GLFr3p7/wiOio52QFjQ==}
dev: false
/@types/ms/0.7.31:
resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==}
dev: true
@ -10039,6 +10047,10 @@ packages:
resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==}
dev: false
/mousetrap/1.6.5:
resolution: {integrity: sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==}
dev: false
/mqtt-packet/6.10.0:
resolution: {integrity: sha512-ja8+mFKIHdB1Tpl6vac+sktqy3gA8t9Mduom1BA75cI+R9AHnZOiaBQwpGiWnaVJLDGRdNhQmFaAqd7tkKSMGA==}
dependencies: