diff --git a/src/app.jsx b/src/app.jsx
index 1f9ddc01..ed9bd15e 100644
--- a/src/app.jsx
+++ b/src/app.jsx
@@ -26,6 +26,7 @@ import Loader from './components/loader';
import MediaModal from './components/media-modal';
import Modal from './components/modal';
import NotificationService from './components/notification-service';
+import SearchCommand from './components/search-command';
import Shortcuts from './components/shortcuts';
import ShortcutsSettings from './components/shortcuts-settings';
import NotFound from './pages/404';
@@ -449,6 +450,7 @@ function App() {
)}
+
>
);
}
diff --git a/src/components/search-command.css b/src/components/search-command.css
new file mode 100644
index 00000000..3700758b
--- /dev/null
+++ b/src/components/search-command.css
@@ -0,0 +1,54 @@
+#search-command-container {
+ position: fixed;
+ inset: 0;
+ z-index: 1002;
+ background-color: var(--backdrop-darker-color);
+ background-image: radial-gradient(
+ farthest-corner at top,
+ var(--backdrop-color),
+ transparent
+ );
+ display: flex;
+ justify-content: center;
+ align-items: flex-start;
+ padding: 16px;
+ transition: opacity 0.1s ease-in-out;
+}
+#search-command-container[hidden] {
+ opacity: 0;
+ pointer-events: none;
+}
+
+#search-command-container form {
+ width: calc(40em - 32px);
+ max-width: 100%;
+ transition: transform 0.1s ease-in-out;
+}
+#search-command-container[hidden] form {
+ transform: translateY(-64px) scale(0.9);
+}
+#search-command-container input {
+ width: 100%;
+ padding: 16px;
+ border-radius: 999px;
+ background-color: var(--bg-faded-color);
+ border: 2px solid var(--outline-color);
+ box-shadow: 0 2px 16px var(--drop-shadow-color),
+ 0 32px 64px var(--drop-shadow-color);
+}
+#search-command-container input:focus {
+ outline: 0;
+ background-color: var(--bg-color);
+ border-color: var(--link-color);
+}
+
+@media (min-width: 40em) {
+ #search-command-container {
+ align-items: center;
+ background-image: radial-gradient(
+ closest-side,
+ var(--backdrop-color),
+ transparent
+ );
+ }
+}
diff --git a/src/components/search-command.jsx b/src/components/search-command.jsx
new file mode 100644
index 00000000..591ab270
--- /dev/null
+++ b/src/components/search-command.jsx
@@ -0,0 +1,67 @@
+import './search-command.css';
+
+import { useRef, useState } from 'preact/hooks';
+import { useHotkeys } from 'react-hotkeys-hook';
+
+import SearchForm from './search-form';
+
+export default function SearchCommand({ onClose = () => {} }) {
+ const [showSearch, setShowSearch] = useState(false);
+ const searchFormRef = useRef(null);
+
+ useHotkeys(
+ '/',
+ (e) => {
+ setShowSearch(true);
+ setTimeout(() => {
+ searchFormRef.current?.focus?.();
+ }, 0);
+ },
+ {
+ preventDefault: true,
+ ignoreEventWhen: (e) => {
+ const isSearchPage = /\/search/.test(location.hash);
+ const hasModal = !!document.querySelector('#modal-container > *');
+ return isSearchPage || hasModal;
+ },
+ },
+ );
+
+ const closeSearch = () => {
+ setShowSearch(false);
+ onClose();
+ };
+
+ useHotkeys(
+ 'esc',
+ (e) => {
+ searchFormRef.current?.blur?.();
+ closeSearch();
+ },
+ {
+ enabled: showSearch,
+ enableOnFormTags: true,
+ preventDefault: true,
+ },
+ );
+
+ return (
+
{
+ console.log(e);
+ if (e.target === e.currentTarget) {
+ closeSearch();
+ }
+ }}
+ >
+ {
+ closeSearch();
+ }}
+ />
+
+ );
+}
diff --git a/src/components/search-form.jsx b/src/components/search-form.jsx
new file mode 100644
index 00000000..9b7e1051
--- /dev/null
+++ b/src/components/search-form.jsx
@@ -0,0 +1,237 @@
+import { forwardRef } from 'preact/compat';
+import { useImperativeHandle, useRef, useState } from 'preact/hooks';
+import { useSearchParams } from 'react-router-dom';
+
+import { api } from '../utils/api';
+
+import Icon from './icon';
+import Link from './link';
+
+const SearchForm = forwardRef((props, ref) => {
+ const { instance } = api();
+ const [searchParams, setSearchParams] = useSearchParams();
+ const [searchMenuOpen, setSearchMenuOpen] = useState(false);
+ const [query, setQuery] = useState(searchParams.get('q') || '');
+ const type = searchParams.get('type');
+ const formRef = useRef(null);
+
+ const searchFieldRef = useRef(null);
+ useImperativeHandle(ref, () => ({
+ setValue: (value) => {
+ setQuery(value);
+ },
+ focus: () => {
+ searchFieldRef.current.focus();
+ },
+ blur: () => {
+ searchFieldRef.current.blur();
+ },
+ }));
+
+ return (
+
+ );
+});
+
+export default SearchForm;
diff --git a/src/index.css b/src/index.css
index 2b2eaa16..751ec5f2 100644
--- a/src/index.css
+++ b/src/index.css
@@ -52,6 +52,7 @@
--outline-hover-color: rgba(128, 128, 128, 0.7);
--divider-color: rgba(0, 0, 0, 0.1);
--backdrop-color: rgba(0, 0, 0, 0.05);
+ --backdrop-darker-color: rgba(0, 0, 0, 0.25);
--backdrop-solid-color: #ccc;
--img-bg-color: rgba(128, 128, 128, 0.2);
--loader-color: #1c1e2199;
diff --git a/src/pages/search.jsx b/src/pages/search.jsx
index 7775308d..8037724e 100644
--- a/src/pages/search.jsx
+++ b/src/pages/search.jsx
@@ -1,13 +1,7 @@
import './search.css';
-import { forwardRef } from 'preact/compat';
-import {
- useEffect,
- useImperativeHandle,
- useLayoutEffect,
- useRef,
- useState,
-} from 'preact/hooks';
+import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
+import { useHotkeys } from 'react-hotkeys-hook';
import { InView } from 'react-intersection-observer';
import { useParams, useSearchParams } from 'react-router-dom';
@@ -16,6 +10,7 @@ import Icon from '../components/icon';
import Link from '../components/link';
import Loader from '../components/loader';
import NavMenu from '../components/nav-menu';
+import SearchForm from '../components/search-form';
import Status from '../components/status';
import { api } from '../utils/api';
import useTitle from '../utils/useTitle';
@@ -128,10 +123,21 @@ function Search(props) {
if (q) {
searchFormRef.current?.setValue?.(q);
loadResults(true);
+ } else {
+ searchFormRef.current?.focus?.();
}
- searchFormRef.current?.focus?.();
}, [q, type, instance]);
+ useHotkeys(
+ '/',
+ (e) => {
+ searchFormRef.current?.focus?.();
+ },
+ {
+ preventDefault: true,
+ },
+ );
+
return (
@@ -356,213 +362,3 @@ function Search(props) {
}
export default Search;
-
-const SearchForm = forwardRef((props, ref) => {
- const { instance } = api();
- const [searchParams, setSearchParams] = useSearchParams();
- const [searchMenuOpen, setSearchMenuOpen] = useState(false);
- const [query, setQuery] = useState(searchParams.get('q') || '');
- const type = searchParams.get('type');
- const formRef = useRef(null);
-
- const searchFieldRef = useRef(null);
- useImperativeHandle(ref, () => ({
- setValue: (value) => {
- setQuery(value);
- },
- focus: () => {
- searchFieldRef.current.focus();
- },
- }));
-
- return (
-
- );
-});