diff --git a/packages/ui-default/components/omnisearch/index.page.tsx b/packages/ui-default/components/omnisearch/index.page.tsx
new file mode 100644
index 000000000..64c10b982
--- /dev/null
+++ b/packages/ui-default/components/omnisearch/index.page.tsx
@@ -0,0 +1,125 @@
+import { debounce } from 'lodash';
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import { STATUS_CODES } from 'vj/constant/record';
+import { AutoloadPage } from 'vj/misc/Page';
+import {
+ api, gql, i18n, request,
+} from 'vj/utils';
+
+export default new AutoloadPage('omnibar', () => {
+ const $search = $(`
+
+ `);
+ const $entry = $(`
+
+ `);
+ $(document.body).append($search);
+ $(document.body).append($entry);
+ const $input = $('.omnibar input') as JQuery;
+
+ const prefix = window.location.href.startsWith('/d/') ? `/d/${UiContext.domainId}` : '';
+ let setSearching;
+ let pdocs = [];
+ let psdict = {};
+ let udocs = [];
+ function SearchResult() {
+ const [searching, set] = React.useState(false);
+ setSearching = set;
+ return (
+ <>
+ {searching && Searching...
}
+ {pdocs.length > 0 && {i18n('Problems')}
}
+ {pdocs.map(({
+ domainId, docId, pid, title, nSubmit, nAccept,
+ }) => (
+
+
+
+ {domainId !== UiContext.domainId ? `${domainId}#` : ''}{pid || docId}
+ {nAccept}/{nSubmit}
+
+
+ ))}
+ {udocs.length > 0 && {i18n('Users')}
}
+ {udocs.map(({
+ _id, uname, avatarUrl,
+ }) => (
+
+
+

+
+
+
+ ))}
+ >
+ );
+ }
+ ReactDOM.createRoot(document.getElementById('omnibar-content')!).render();
+
+ async function search(query: string) {
+ if (!query?.trim()) {
+ pdocs = [];
+ setSearching?.(false);
+ return;
+ }
+ setSearching?.(true);
+ [{ pdocs, psdict }, udocs] = await Promise.all([
+ request.get(`/d/${UiContext.domainId}/p?q=${encodeURIComponent(query)}&limit=10`),
+ api(gql`
+ users(search: ${query}) {
+ _id
+ uname
+ displayName
+ avatarUrl
+ }
+ `, ['data', 'users']),
+ ]);
+ setSearching?.(false);
+ }
+ $input.on('input', debounce((ev) => search(ev.target.value), 1000));
+
+ let open = false;
+ const show = () => {
+ $search.show();
+ setTimeout(() => {
+ $search.css('opacity', '1');
+ $input.focus();
+ }, 20);
+ open = true;
+ };
+ const hide = () => {
+ $search.css('opacity', '0');
+ setTimeout(() => $search.hide(), 200);
+ open = false;
+ };
+ $entry.on('click', () => (open ? hide() : show()));
+ $input.on('click', (ev) => ev.stopPropagation());
+ $('.omnibar-main').on('click', '.omnibar-content-section', (ev) => ev.stopPropagation());
+ $search.on('click', hide);
+});
diff --git a/packages/ui-default/components/omnisearch/omnibar.page.styl b/packages/ui-default/components/omnisearch/omnibar.page.styl
new file mode 100644
index 000000000..89af0e366
--- /dev/null
+++ b/packages/ui-default/components/omnisearch/omnibar.page.styl
@@ -0,0 +1,133 @@
+.omnibar {
+ position: fixed;
+ top: 0px;
+ left: 0px;
+ height: 100%;
+ width: 100%;
+ background-color: rgba(0, 0, 0, 0.1);
+ backdrop-filter: blur(3px);
+ z-index: 999;
+ transition: 0.2s;
+}
+.omnibar-main {
+ height: min(600px, 100% - 20px);
+ width: min(750px, 60%);
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ display: flex;
+ flex-direction: column;
+ >div:first-child {
+ transition: 0.2s;
+ margin-bottom: 10px;
+ input {
+ background-color: #fff;
+ border-radius: 5px;
+ border: 2px solid #aaa;
+ padding-left: 42px;
+ height: 48px;
+ width: 100%;
+ font-size: 18px;
+ outline: 0;
+ box-sizing: border-box;
+ }
+ span {
+ height: 48px;
+ width: 48px;
+ position: absolute;
+ display: grid;
+ scale: 1.5;
+ place-items: center;
+ top: 0px;
+ left: 0px;
+ }
+ }
+ >div:last-child {
+ flex: 1;
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+ overflow-x: hidden;
+ overflow-y: auto;
+ &::-webkit-scrollbar { width: 0 !important; }
+ }
+}
+.omnibar-toggle {
+ z-index: 99;
+ background: white;
+ display: grid;
+ place-items: center;
+ width: 40px;
+ height: 40px;
+ position: fixed;
+ right: 20px;
+ bottom: 30px;
+ transition: 0.2s;
+ border-radius: 5px;
+ &:hover {
+ background: #ccc;
+ }
+ span {
+ scale: 1.5;
+ }
+}
+#omnibar-content {
+ overscroll-behavior: none;
+}
+.omnibar-content-title {
+ text-align: left;
+ margin-bottom: 10px;
+ width: 100%;
+ font-size: 18px;
+ font-weight: bold;
+}
+.omnibar-content-section {
+ background: white;
+ border-radius: 10px;
+ display: flex;
+ flex-direction: column;
+ cursor: pointer;
+ padding: 10px;
+ line-height: 1;
+ margin-bottom: 10px;
+ border: 2px solid #888;
+ box-sizing: border-box;
+ width: 100%;
+ text-decoration: none !important;
+ &:hover {
+ background: #eee;
+ }
+ >div {
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ }
+}
+
+.omnibar-user img {
+ height: 36px;
+ width: 36px;
+ border-radius: 50%;
+ margin-right: 10px;
+}
+.omnibar-user-info {
+ >span {
+ font-size: 14px;
+ margin-bottom: 3px;
+ display: inline-block;
+ }
+ >div {
+ display: flex;
+ flex-direction: row;
+ }
+}
+
+.omnibar-problem a {
+ margin: 2px 0px 3px 0px;
+}
+.omnibar-problem .icon {
+ margin-right: 3px;
+ &:not(:first-child) {
+ margin-left: 10px;
+ }
+}
diff --git a/packages/ui-default/misc/icons/pie-chart.svg b/packages/ui-default/misc/icons/pie-chart.svg
new file mode 100644
index 000000000..dbd648b81
--- /dev/null
+++ b/packages/ui-default/misc/icons/pie-chart.svg
@@ -0,0 +1 @@
+
\ No newline at end of file