1.7.1原版
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Search Widget Preview</title>
|
||||
<script type="module" src="/src/index.ts"></script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0 auto;
|
||||
max-width: 1024px;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.dark search-modal {
|
||||
color-scheme: dark;
|
||||
--halo-search-widget-muted-color: #cbd5e1;
|
||||
--halo-search-widget-content-color: #f1f5f9;
|
||||
--halo-search-widget-hit-bg-color: #090a11;
|
||||
--halo-search-widget-modal-bg-color: #15172a;
|
||||
--halo-search-widget-modal-layer-color: #000000cc;
|
||||
--halo-search-widget-base-bg-color: #090a11;
|
||||
--halo-search-widget-divider-color: #1e293b;
|
||||
--halo-search-widget-kbd-border-color: #334155;
|
||||
--halo-search-widget-kbd-shadow: 0px 2px 0px 0px #ffffff1a;
|
||||
|
||||
--halo-search-widget-color-modal-layer: rgba(0, 0, 0, 0.8);
|
||||
--halo-search-widget-color-modal-content-bg: rgb(15 23 42);
|
||||
--halo-search-widget-color-form-input: rgb(255, 255, 255);
|
||||
--halo-search-widget-color-form-input-placeholder: rgb(148 163 184);
|
||||
--halo-search-widget-color-form-input-bg: rgb(15 23 42);
|
||||
--halo-search-widget-color-form-divider: rgb(30 41 59);
|
||||
--halo-search-widget-color-result-item-bg: rgb(30 41 59);
|
||||
--halo-search-widget-color-result-item-hover-bg: rgb(51 65 85);
|
||||
--halo-search-widget-color-result-item-title: rgb(255 255 255);
|
||||
--halo-search-widget-color-result-item-content: rgb(148 163 184);
|
||||
--halo-search-widget-color-command-kbd-item: rgb(148 163 184);
|
||||
--halo-search-widget-color-command-kbd-border: rgb(30 41 59);
|
||||
--halo-search-widget-color-result-empty: rgb(148 163 184);
|
||||
}
|
||||
|
||||
body > * {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<button>启动</button>
|
||||
<button id="color">暗色/亮色</button>
|
||||
<search-modal></search-modal>
|
||||
</body>
|
||||
<script>
|
||||
const button = document.querySelector('button');
|
||||
const searchModal = document.querySelector('search-modal');
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
searchModal.open = !searchModal.open;
|
||||
});
|
||||
|
||||
const color = document.getElementById('color');
|
||||
color.addEventListener('click', () => {
|
||||
document.body.classList.toggle('dark');
|
||||
});
|
||||
</script>
|
||||
</html>
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/lit/lit/main/packages/localize-tools/config.schema.json",
|
||||
"sourceLocale": "en",
|
||||
"targetLocales": ["es", "zh-CN", "zh-TW"],
|
||||
"tsConfig": "./tsconfig.json",
|
||||
"output": {
|
||||
"mode": "runtime",
|
||||
"outputDir": "./src/generated/locales",
|
||||
"localeCodesModule": "./src/generated/locale-codes.ts"
|
||||
},
|
||||
"interchange": {
|
||||
"format": "xliff",
|
||||
"xliffDir": "./xliff/"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "@halo-dev/search-widget",
|
||||
"version": "1.7.1",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./var.css": "./var.css"
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"var.css"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "vite --config vite.config.dev.ts",
|
||||
"build": "vite build --config vite.config.lib.ts",
|
||||
"lint": "lit-analyzer && eslint 'src/**/*.ts' --fix",
|
||||
"prettier": "prettier \"**/*.{cjs,html,js,json,md,ts}\" --ignore-path ./.gitignore --write",
|
||||
"prepublishOnly": "pnpm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lit/localize": "^0.12.2",
|
||||
"es-toolkit": "^1.39.7",
|
||||
"lit": "^3.2.0",
|
||||
"overlayscrollbars": "^2.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/json": "^2.2.234",
|
||||
"@julr/unocss-preset-forms": "^2.0.0",
|
||||
"@lit/localize-tools": "^0.8.0",
|
||||
"@unocss/reset": "^66.3.3",
|
||||
"lit-analyzer": "^1.2.1",
|
||||
"unocss": "^66.3.3",
|
||||
"vite-plugin-dts": "4.0.0-beta.2",
|
||||
"vite": "npm:rolldown-vite@7.0.8"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export const HISTORY_KEY = 'halo:search-widgets:history:hits';
|
||||
export const MAX_HISTORY_ITEMS = 50;
|
||||
@@ -0,0 +1,27 @@
|
||||
// Do not modify this file by hand!
|
||||
// Re-generate this file by running lit-localize.
|
||||
|
||||
/**
|
||||
* The locale code that templates in this source code are written in.
|
||||
*/
|
||||
export const sourceLocale = `en`;
|
||||
|
||||
/**
|
||||
* The other locale codes that this application is localized into. Sorted
|
||||
* lexicographically.
|
||||
*/
|
||||
export const targetLocales = [
|
||||
`es`,
|
||||
`zh-CN`,
|
||||
`zh-TW`,
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* All valid project locale codes. Sorted lexicographically.
|
||||
*/
|
||||
export const allLocales = [
|
||||
`en`,
|
||||
`es`,
|
||||
`zh-CN`,
|
||||
`zh-TW`,
|
||||
] as const;
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
// Do not modify this file by hand!
|
||||
// Re-generate this file by running lit-localize
|
||||
|
||||
|
||||
|
||||
|
||||
/* eslint-disable no-irregular-whitespace */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export const templates = {
|
||||
's5e8250fb85d64c23': `Cerrar`,
|
||||
'sa84142fc34654130': `Sin resultados de búsqueda`,
|
||||
'sad5f82f8364e5a1c': `Introducir palabras clave para buscar`,
|
||||
'sb4f1dffbb6be6302': `Borrar`,
|
||||
'sb85774dc5d18ff0f': `Confirmar`,
|
||||
'sbce70cbdb856635e': `Recientes`,
|
||||
'sfc63e31ac7c956ed': `Seleccionar`,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
// Do not modify this file by hand!
|
||||
// Re-generate this file by running lit-localize
|
||||
|
||||
|
||||
|
||||
|
||||
/* eslint-disable no-irregular-whitespace */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export const templates = {
|
||||
's5e8250fb85d64c23': `关闭`,
|
||||
'sa84142fc34654130': `没有搜索结果`,
|
||||
'sad5f82f8364e5a1c': `输入关键词以搜索`,
|
||||
'sb4f1dffbb6be6302': `清空`,
|
||||
'sb85774dc5d18ff0f': `确认`,
|
||||
'sbce70cbdb856635e': `最近搜索`,
|
||||
'sfc63e31ac7c956ed': `选择`,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
// Do not modify this file by hand!
|
||||
// Re-generate this file by running lit-localize
|
||||
|
||||
|
||||
|
||||
|
||||
/* eslint-disable no-irregular-whitespace */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export const templates = {
|
||||
's5e8250fb85d64c23': `關閉`,
|
||||
'sa84142fc34654130': `沒有搜尋結果`,
|
||||
'sad5f82f8364e5a1c': `輸入關鍵字以搜尋`,
|
||||
'sb4f1dffbb6be6302': `清空`,
|
||||
'sb85774dc5d18ff0f': `確認`,
|
||||
'sbce70cbdb856635e': `最近搜尋`,
|
||||
'sfc63e31ac7c956ed': `選擇`,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { SearchForm } from './search-form';
|
||||
import { SearchModal } from './search-modal';
|
||||
import './locale';
|
||||
|
||||
export { SearchForm, SearchModal };
|
||||
@@ -0,0 +1,38 @@
|
||||
import { configureLocalization, type LocaleModule } from '@lit/localize';
|
||||
import {
|
||||
allLocales,
|
||||
sourceLocale,
|
||||
targetLocales,
|
||||
} from '../generated/locale-codes';
|
||||
import * as templates_es from '../generated/locales/es';
|
||||
import * as templates_zh_CN from '../generated/locales/zh-CN';
|
||||
import * as templates_zh_TW from '../generated/locales/zh-TW';
|
||||
|
||||
const localizedTemplates = new Map([
|
||||
['es', templates_es],
|
||||
['zh-CN', templates_zh_CN],
|
||||
['zh-TW', templates_zh_TW],
|
||||
]);
|
||||
|
||||
const { setLocale } = configureLocalization({
|
||||
sourceLocale,
|
||||
targetLocales,
|
||||
loadLocale: async (locale) => localizedTemplates.get(locale) as LocaleModule,
|
||||
});
|
||||
|
||||
setLocale(getLocale());
|
||||
|
||||
function getLanguageFromCookie() {
|
||||
const match = document.cookie.match(/(^| )language=([^;]+)/);
|
||||
const matchedLanguage = allLocales.find((locale) => locale === match?.[2]);
|
||||
return matchedLanguage;
|
||||
}
|
||||
|
||||
function getLanguageFromBrowser() {
|
||||
const language = allLocales.find((locale) => locale === navigator.language);
|
||||
return language;
|
||||
}
|
||||
|
||||
export function getLocale() {
|
||||
return getLanguageFromCookie() || getLanguageFromBrowser() || sourceLocale;
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
import type {
|
||||
HaloDocument,
|
||||
SearchOption,
|
||||
SearchResult,
|
||||
} from '@halo-dev/api-client';
|
||||
import { msg } from '@lit/localize';
|
||||
import resetStyles from '@unocss/reset/tailwind.css?inline';
|
||||
import { type DebouncedFunction, debounce } from 'es-toolkit';
|
||||
import { uniqBy } from 'es-toolkit/compat';
|
||||
import { css, html, LitElement, unsafeCSS } from 'lit';
|
||||
import { property, state } from 'lit/decorators.js';
|
||||
import { createRef, type Ref, ref } from 'lit/directives/ref.js';
|
||||
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
||||
import { HISTORY_KEY, MAX_HISTORY_ITEMS } from './constants';
|
||||
import baseStyles from './styles/base';
|
||||
|
||||
export class SearchForm extends LitElement {
|
||||
@property({ type: String })
|
||||
baseUrl = '';
|
||||
|
||||
@property({ type: Object })
|
||||
options = {};
|
||||
|
||||
@state()
|
||||
private keyword = '';
|
||||
|
||||
@state()
|
||||
private searchResult?: SearchResult;
|
||||
|
||||
@state()
|
||||
private loading: boolean = false;
|
||||
|
||||
@state()
|
||||
private selectedIndex = 0;
|
||||
|
||||
@state()
|
||||
private historyHits: HaloDocument[] = [];
|
||||
|
||||
inputRef: Ref<HTMLInputElement> = createRef<HTMLInputElement>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.addEventListener('keydown', this.handleKeydown);
|
||||
|
||||
this.historyHits = JSON.parse(
|
||||
localStorage.getItem(HISTORY_KEY) || '[]'
|
||||
) as HaloDocument[];
|
||||
|
||||
setTimeout(() => {
|
||||
this.inputRef.value?.focus();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<div class="p-3 z-1 bg-base sticky top-0 border-b border-divider">
|
||||
<form
|
||||
class="flex items-center ring-2 h-12 rounded-base px-2.5 ring-primary bg-base"
|
||||
@submit=${(e: Event) => e.preventDefault()}
|
||||
>
|
||||
<span
|
||||
class="shrink flex-none size-6 text-primary ${
|
||||
this.loading
|
||||
? 'i-lucide-loader-circle animate-spin'
|
||||
: 'i-lucide-search'
|
||||
}"
|
||||
></span>
|
||||
<input
|
||||
@input="${this.onInput}"
|
||||
placeholder=${msg('Enter keywords to search')}
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
${ref(this.inputRef)}
|
||||
class="flex-1 min-w-0 outline-none text-content h-full px-2.5 bg-transparent"
|
||||
/>
|
||||
${
|
||||
this.keyword
|
||||
? html`
|
||||
<span
|
||||
@click=${this.handleClearInput}
|
||||
class="flex-none cursor-pointer shrink i-lucide-x size-5 text-muted hover:text-content"
|
||||
></span>
|
||||
`
|
||||
: ''
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
${this.keyword ? this.renderItems() : this.renderHistoryItems()}
|
||||
|
||||
<div
|
||||
class="border-t border-divider p-3 bg-base sticky bottom-0 space-x-5 flex justify-end"
|
||||
>
|
||||
${[
|
||||
{
|
||||
text: html`${msg('Select')}`,
|
||||
kbdIcons: ['i-lucide-arrow-up', 'i-lucide-arrow-down'],
|
||||
},
|
||||
{
|
||||
text: html`${msg('Confirm')}`,
|
||||
kbdIcons: ['i-lucide-corner-down-left'],
|
||||
},
|
||||
{
|
||||
text: html`${msg('Close')}`,
|
||||
kbdIcons: ['i-mdi-keyboard-esc'],
|
||||
},
|
||||
].map(
|
||||
(item) => html`
|
||||
<div class="flex items-center space-x-1.5">
|
||||
${item.kbdIcons.map(
|
||||
(icon) => html`
|
||||
<kbd
|
||||
class="inline-flex justify-center items-center py-1 px-1.5 bg-base border border-kbd font-mono text-sm text-content rounded-base shadow-kbd"
|
||||
>
|
||||
<i class="${icon}"></i>
|
||||
</kbd>
|
||||
`
|
||||
)}
|
||||
<span class="text-xs text-muted">${item.text}</span>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
handleClearInput() {
|
||||
this.keyword = '';
|
||||
this.searchResult = undefined;
|
||||
if (this.inputRef.value) {
|
||||
this.inputRef.value.value = '';
|
||||
this.inputRef.value.focus();
|
||||
}
|
||||
}
|
||||
|
||||
renderItems() {
|
||||
return html`<div class="p-3">
|
||||
${!this.searchResult?.hits?.length ? this.renderEmpty() : ''}
|
||||
<ul class="space-y-1.5" role="list">
|
||||
${this.searchResult?.hits?.map((hit, index) =>
|
||||
this.renderListItem(hit, index, 'i-lucide-file-text')
|
||||
)}
|
||||
</ul>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
renderEmpty() {
|
||||
return html`<div class="flex py-4 justify-center text-sm text-muted">
|
||||
<span>${msg('No search results')}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
renderHistoryItems() {
|
||||
return html`
|
||||
<div class="p-3">
|
||||
${
|
||||
this.historyHits.length
|
||||
? html`<div class="flex justify-between items-center">
|
||||
<h3 class="text-sm font-medium text-primary">
|
||||
${msg('Recent')}
|
||||
</h3>
|
||||
<span
|
||||
class="text-xs cursor-pointer text-muted hover:text-content"
|
||||
@click=${this.handleClearHistory}
|
||||
>
|
||||
${msg('Clear')}
|
||||
</span>
|
||||
</div>
|
||||
<ul class="mt-3 space-y-1.5" role="list">
|
||||
${this.historyHits?.map((hit, index) =>
|
||||
this.renderListItem(hit, index, 'i-lucide-history')
|
||||
)}
|
||||
</ul>`
|
||||
: this.renderEmpty()
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
renderListItem(hit: HaloDocument, index: number, listIcon: string) {
|
||||
return html`
|
||||
<li
|
||||
@click="${() => this.handleOpenLink(hit)}"
|
||||
@mouseenter=${() => {
|
||||
this.selectedIndex = index;
|
||||
}}
|
||||
class="shadow-sm flex items-center space-x-3 rounded-base cursor-pointer p-3 bg-hit [&_mark]:text-primary [&_mark]:font-semibold [&_mark]:bg-transparent ${
|
||||
index === this.selectedIndex
|
||||
? '!bg-primary [&_mark]:!text-white [&_mark]:underline'
|
||||
: ''
|
||||
}"
|
||||
data-index=${index}
|
||||
>
|
||||
<span
|
||||
class="flex-none shrink ${listIcon} ${
|
||||
this.selectedIndex === index ? 'text-white' : 'text-muted'
|
||||
}"
|
||||
></span>
|
||||
<div class="flex-1 space-y-1.5 min-w-0">
|
||||
<h2
|
||||
class="text-sm font-medium ${
|
||||
this.selectedIndex === index ? 'text-white' : 'text-content'
|
||||
}"
|
||||
>
|
||||
${unsafeHTML(hit.title)}
|
||||
</h2>
|
||||
${
|
||||
hit.description
|
||||
? html`
|
||||
<p
|
||||
class="text-xs leading-6 ${
|
||||
this.selectedIndex === index
|
||||
? 'text-white/90'
|
||||
: 'text-muted'
|
||||
}"
|
||||
>
|
||||
${unsafeHTML(hit.description)}
|
||||
</p>
|
||||
`
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
<span
|
||||
class="i-lucide-corner-down-left flex-none shrink text-white invisible ${
|
||||
this.selectedIndex === index ? '!visible' : ''
|
||||
}"
|
||||
></span>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
|
||||
onInput(e: InputEvent) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
const value = input.value;
|
||||
|
||||
this.keyword = value || '';
|
||||
|
||||
this.selectedIndex = 0;
|
||||
|
||||
if (value === '') {
|
||||
this.searchResult = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.fetchHits(value);
|
||||
}
|
||||
|
||||
handleClearHistory() {
|
||||
localStorage.removeItem(HISTORY_KEY);
|
||||
this.historyHits = [];
|
||||
}
|
||||
|
||||
fetchHits: DebouncedFunction<(keyword: string) => Promise<void>> = debounce(
|
||||
async (keyword: string) => {
|
||||
const searchOptions: SearchOption = {
|
||||
...this.options,
|
||||
highlightPostTag: '</mark>',
|
||||
highlightPreTag: '<mark>',
|
||||
keyword,
|
||||
limit: 20,
|
||||
};
|
||||
|
||||
const response = await fetch(
|
||||
`/apis/api.halo.run/v1alpha1/indices/-/search`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(searchOptions),
|
||||
method: 'post',
|
||||
}
|
||||
);
|
||||
|
||||
const data = (await response.json()) as SearchResult;
|
||||
this.searchResult = data;
|
||||
this.loading = false;
|
||||
},
|
||||
300
|
||||
);
|
||||
|
||||
handleOpenLink(hit: HaloDocument) {
|
||||
const updatedHistory = uniqBy([hit, ...this.historyHits], 'id').slice(
|
||||
0,
|
||||
MAX_HISTORY_ITEMS
|
||||
);
|
||||
localStorage.setItem(HISTORY_KEY, JSON.stringify(updatedHistory));
|
||||
this.historyHits = updatedHistory;
|
||||
window.location.href = hit.permalink;
|
||||
}
|
||||
|
||||
handleKeydown = (e: KeyboardEvent) => {
|
||||
const { key, ctrlKey } = e;
|
||||
|
||||
const hits = this.keyword ? this.searchResult?.hits : this.historyHits;
|
||||
|
||||
if (!hits) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === 'ArrowUp' || (key === 'k' && ctrlKey)) {
|
||||
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
||||
this.handleScrollIntoSelected();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (key === 'ArrowDown' || (key === 'j' && ctrlKey)) {
|
||||
this.selectedIndex = Math.min(hits.length || 0, this.selectedIndex + 1);
|
||||
this.handleScrollIntoSelected();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (key === 'Enter') {
|
||||
const hit = hits[this.selectedIndex];
|
||||
if (hit) {
|
||||
this.handleOpenLink(hit);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleScrollIntoSelected() {
|
||||
const selectedElement = this.shadowRoot?.querySelector(
|
||||
`[data-index="${this.selectedIndex}"]`
|
||||
);
|
||||
if (selectedElement) {
|
||||
selectedElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
unsafeCSS(resetStyles),
|
||||
baseStyles,
|
||||
css`
|
||||
@unocss-placeholder;
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
customElements.get('search-form') ||
|
||||
customElements.define('search-form', SearchForm);
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'search-form': SearchForm;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import resetStyles from '@unocss/reset/tailwind.css?inline';
|
||||
import { css, html, LitElement, type PropertyValues, unsafeCSS } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { OverlayScrollbars } from 'overlayscrollbars';
|
||||
import overlayscrollbarsStyles from 'overlayscrollbars/styles/overlayscrollbars.css?inline';
|
||||
import './search-form';
|
||||
import baseStyles from './styles/base';
|
||||
|
||||
export class SearchModal extends LitElement {
|
||||
@property({
|
||||
type: Boolean,
|
||||
})
|
||||
open = false;
|
||||
|
||||
@property({ type: String })
|
||||
baseUrl = '';
|
||||
|
||||
@property({ type: Object })
|
||||
options = {};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
setTimeout(() => {
|
||||
const modalContent = this.shadowRoot?.querySelector(
|
||||
'.modal__content'
|
||||
) as HTMLElement;
|
||||
if (modalContent) {
|
||||
OverlayScrollbars(modalContent, {
|
||||
scrollbars: {
|
||||
autoHide: 'scroll',
|
||||
autoHideDelay: 600,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
override willUpdate(changedProperties: PropertyValues) {
|
||||
if (!changedProperties.has('open')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.open) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.removeProperty('overflow');
|
||||
}
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`<div
|
||||
class="modal__wrapper"
|
||||
style="${styleMap({ display: this.open ? 'flex' : 'none' })}"
|
||||
>
|
||||
<div class="modal__layer" @click="${this.close}"></div>
|
||||
<div
|
||||
data-overlayscrollbars-initialize
|
||||
class="modal__content shadow-xl bg-modal"
|
||||
>
|
||||
${
|
||||
this.open
|
||||
? html`<search-form
|
||||
.baseUrl=${this.baseUrl}
|
||||
.options=${this.options}
|
||||
></search-form>`
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
close() {
|
||||
this.open = false;
|
||||
}
|
||||
|
||||
handleKeydown = (e: KeyboardEvent) => {
|
||||
const { key } = e;
|
||||
|
||||
if (key === 'Escape') {
|
||||
this.close();
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener('keydown', this.handleKeydown);
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener('keydown', this.handleKeydown);
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
unsafeCSS(resetStyles),
|
||||
unsafeCSS(overlayscrollbarsStyles),
|
||||
baseStyles,
|
||||
css`
|
||||
:host {
|
||||
// @deprecated --halo-search-widget-color-modal-layer and
|
||||
// --halo-search-widget-base-border-radius will be removed in future
|
||||
--base-rounded: var(
|
||||
--halo-search-widget-base-rounded,
|
||||
var(--halo-search-widget-base-border-radius, 0.4em)
|
||||
);
|
||||
--modal-layer-color: var(
|
||||
--halo-search-widget-modal-layer-color,
|
||||
var(--halo-search-widget-color-modal-layer, rgb(107 114 128 / 0.75))
|
||||
);
|
||||
}
|
||||
|
||||
.modal__wrapper {
|
||||
position: fixed;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding-top: 3em;
|
||||
padding-bottom: 3em;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.modal__layer {
|
||||
background-color: var(--modal-layer-color, rgb(107 114 128 / 0.75));
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
flex: none;
|
||||
animation: fadeIn 0.15s both;
|
||||
}
|
||||
|
||||
.modal__content {
|
||||
border-radius: var(--base-rounded, 0.4em);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
width: calc(100vw - 20px);
|
||||
max-height: calc(100vh - 5em);
|
||||
max-width: 580px;
|
||||
overflow: auto;
|
||||
animation: fadeInUp 0.3s both;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 10%, 0);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@unocss-placeholder;
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
customElements.get('search-modal') ||
|
||||
customElements.define('search-modal', SearchModal);
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'search-modal': SearchModal;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { css } from 'lit';
|
||||
|
||||
const baseStyles = css`
|
||||
:host {
|
||||
font-size: var(--halo-search-widget-base-font-size, 1em);
|
||||
font-family: var(
|
||||
--halo-search-widget-base-font-family,
|
||||
ui-sans-serif,
|
||||
system-ui,
|
||||
sans-serif,
|
||||
'Apple Color Emoji',
|
||||
'Segoe UI Emoji',
|
||||
'Segoe UI Symbol',
|
||||
'Noto Color Emoji'
|
||||
);
|
||||
}
|
||||
`;
|
||||
|
||||
export default baseStyles;
|
||||
@@ -0,0 +1,46 @@
|
||||
import { css } from 'lit';
|
||||
|
||||
const varStyles = css`
|
||||
:host {
|
||||
--color-form-input-bg: var(--halo-search-widget-color-form-input-bg, #fff);
|
||||
--color-form-input: var(--halo-search-widget-color-form-input, #333);
|
||||
--color-form-input-placeholder: var(
|
||||
--halo-search-widget-color-form-input-placeholder,
|
||||
rgb(107 114 128)
|
||||
);
|
||||
--color-form-divider: var(
|
||||
--halo-search-widget-color-form-divider,
|
||||
rgb(243 244 246)
|
||||
);
|
||||
--color-result-empty: var(
|
||||
--halo-search-widget-color-result-empty,
|
||||
rgb(107 114 128)
|
||||
);
|
||||
--color-result-item-bg: var(
|
||||
--halo-search-widget-color-result-item-bg,
|
||||
rgb(249 250 251)
|
||||
);
|
||||
--color-result-item-hover-bg: var(
|
||||
--halo-search-widget-color-result-item-hover-bg,
|
||||
rgb(243 244 246)
|
||||
);
|
||||
--color-result-item-title: var(
|
||||
--halo-search-widget-color-result-item-title,
|
||||
#333
|
||||
);
|
||||
--color-result-item-content: var(
|
||||
--halo-search-widget-color-result-item-content,
|
||||
rgb(75, 85, 99)
|
||||
);
|
||||
--color-command-kbd-item: var(
|
||||
--halo-search-widget-color-command-kbd-item,
|
||||
#333
|
||||
);
|
||||
--color-command-kbd-border: var(
|
||||
--halo-search-widget-color-command-kbd-border,
|
||||
#e5e7eb
|
||||
);
|
||||
}
|
||||
`;
|
||||
|
||||
export default varStyles;
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
export interface Hit {
|
||||
name: string;
|
||||
title: string;
|
||||
content: string;
|
||||
publishTimestamp: string;
|
||||
permalink: string;
|
||||
}
|
||||
|
||||
export interface Result {
|
||||
hits: Hit[];
|
||||
keyword: string;
|
||||
total: number;
|
||||
limit: number;
|
||||
processingTimeMills: number;
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2021",
|
||||
"module": "es2020",
|
||||
"lib": ["es2021", "DOM", "DOM.Iterable"],
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"inlineSources": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitThis": true,
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"experimentalDecorators": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noImplicitOverride": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "ts-lit-plugin",
|
||||
"strict": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": []
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
defineConfig,
|
||||
definePreset,
|
||||
presetIcons,
|
||||
presetUno,
|
||||
presetWind3,
|
||||
transformerCompileClass,
|
||||
transformerDirectives,
|
||||
} from 'unocss';
|
||||
|
||||
const remRE = /(-?[.\d]+)rem/g;
|
||||
|
||||
const presetRemToEm = definePreset(() => {
|
||||
return {
|
||||
name: '@unocss/preset-rem-to-em',
|
||||
postprocess: (util) => {
|
||||
util.entries.forEach((i) => {
|
||||
const value = i[1];
|
||||
if (typeof value === 'string' && remRE.test(value))
|
||||
i[1] = value.replace(remRE, (_, p1) => `${p1}em`);
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default defineConfig({
|
||||
presets: [presetWind3(), presetIcons(), presetRemToEm()],
|
||||
transformers: [transformerDirectives(), transformerCompileClass()],
|
||||
theme: {
|
||||
colors: {
|
||||
primary: 'var(--halo-search-widget-primary-color,#4CCBA0)',
|
||||
muted: 'var(--halo-search-widget-muted-color,#475569)',
|
||||
content:
|
||||
'var(--halo-search-widget-content-color,var(--halo-search-widget-color-form-input,#0f172a))',
|
||||
},
|
||||
borderRadius: {
|
||||
base: 'var(--halo-search-widget-base-rounded, 0.4em)',
|
||||
},
|
||||
},
|
||||
shortcuts: {
|
||||
'bg-modal':
|
||||
'bg-[var(--halo-search-widget-modal-bg-color,var(--halo-search-widget-color-modal-content-bg,#f8fafc))]',
|
||||
'bg-hit':
|
||||
'bg-[var(--halo-search-widget-hit-bg-color,var(--halo-search-widget-color-result-item-bg,#fff))]',
|
||||
'bg-base':
|
||||
'bg-[var(--halo-search-widget-base-bg-color,var(--halo-search-widget-color-modal-content-bg,#fff))]',
|
||||
'border-divider':
|
||||
'border-[var(--halo-search-widget-divider-color,var(--halo-search-widget-color-form-divider,#f1f5f9))]',
|
||||
'border-kbd':
|
||||
'border-[var(--halo-search-widget-kbd-border-color,var(--halo-search-widget-color-command-kbd-border,#e2e8f0))]',
|
||||
'shadow-kbd':
|
||||
'shadow-[var(--halo-search-widget-kbd-shadow,0px_2px_0px_0px_rgba(0,0,0,0.08))]',
|
||||
},
|
||||
safelist: [
|
||||
'i-lucide-arrow-up',
|
||||
'i-lucide-arrow-down',
|
||||
'i-mdi-keyboard-esc',
|
||||
'i-lucide-corner-down-left',
|
||||
'i-lucide-file-text',
|
||||
'i-lucide-history',
|
||||
'i-lucide-loader-circle',
|
||||
'i-lucide-search',
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.color-scheme-auto,
|
||||
[data-color-scheme="auto"] {
|
||||
color-scheme: dark;
|
||||
--halo-search-widget-muted-color: #cbd5e1;
|
||||
--halo-search-widget-content-color: #f1f5f9;
|
||||
--halo-search-widget-hit-bg-color: #090a11;
|
||||
--halo-search-widget-modal-bg-color: #15172a;
|
||||
--halo-search-widget-modal-layer-color: #000000cc;
|
||||
--halo-search-widget-base-bg-color: #090a11;
|
||||
--halo-search-widget-divider-color: #1e293b;
|
||||
--halo-search-widget-kbd-border-color: #334155;
|
||||
--halo-search-widget-kbd-shadow: 0px 2px 0px 0px #ffffff1a;
|
||||
}
|
||||
}
|
||||
|
||||
.color-scheme-dark,
|
||||
.dark,
|
||||
[data-color-scheme="dark"] {
|
||||
color-scheme: dark;
|
||||
--halo-search-widget-muted-color: #cbd5e1;
|
||||
--halo-search-widget-content-color: #f1f5f9;
|
||||
--halo-search-widget-hit-bg-color: #090a11;
|
||||
--halo-search-widget-modal-bg-color: #15172a;
|
||||
--halo-search-widget-modal-layer-color: #000000cc;
|
||||
--halo-search-widget-base-bg-color: #090a11;
|
||||
--halo-search-widget-divider-color: #1e293b;
|
||||
--halo-search-widget-kbd-border-color: #334155;
|
||||
--halo-search-widget-kbd-shadow: 0px 2px 0px 0px #ffffff1a;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import UnoCSS from 'unocss/vite';
|
||||
import { defineConfig, type UserConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
UnoCSS({
|
||||
mode: 'shadow-dom',
|
||||
configFile: './uno.config.ts',
|
||||
}),
|
||||
],
|
||||
server: {
|
||||
proxy: {
|
||||
'/apis': {
|
||||
target: 'http://localhost:8090',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}) satisfies UserConfig;
|
||||
@@ -0,0 +1,27 @@
|
||||
import UnoCSS from 'unocss/vite';
|
||||
import { defineConfig, type Plugin } from 'vite';
|
||||
import dts from 'vite-plugin-dts';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
entry: 'src/index.ts',
|
||||
name: 'search-widget',
|
||||
fileName: 'index',
|
||||
formats: ['es'],
|
||||
},
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
extend: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
UnoCSS({
|
||||
mode: 'shadow-dom',
|
||||
configFile: './uno.config.ts',
|
||||
}),
|
||||
dts() as Plugin,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file target-language="es" source-language="en" original="lit-localize-inputs" datatype="plaintext">
|
||||
<body>
|
||||
<trans-unit id="sfc63e31ac7c956ed">
|
||||
<source>Select</source>
|
||||
<target>Seleccionar</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb85774dc5d18ff0f">
|
||||
<source>Confirm</source>
|
||||
<target>Confirmar</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5e8250fb85d64c23">
|
||||
<source>Close</source>
|
||||
<target>Cerrar</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sad5f82f8364e5a1c">
|
||||
<source>Enter keywords to search</source>
|
||||
<target>Introducir palabras clave para buscar</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa84142fc34654130">
|
||||
<source>No search results</source>
|
||||
<target>Sin resultados de búsqueda</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbce70cbdb856635e">
|
||||
<source>Recent</source>
|
||||
<target>Recientes</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb4f1dffbb6be6302">
|
||||
<source>Clear</source>
|
||||
<target>Borrar</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file target-language="zh-CN" source-language="en" original="lit-localize-inputs" datatype="plaintext">
|
||||
<body>
|
||||
<trans-unit id="sfc63e31ac7c956ed">
|
||||
<source>Select</source>
|
||||
<target>选择</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb85774dc5d18ff0f">
|
||||
<source>Confirm</source>
|
||||
<target>确认</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5e8250fb85d64c23">
|
||||
<source>Close</source>
|
||||
<target>关闭</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sad5f82f8364e5a1c">
|
||||
<source>Enter keywords to search</source>
|
||||
<target>输入关键词以搜索</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa84142fc34654130">
|
||||
<source>No search results</source>
|
||||
<target>没有搜索结果</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbce70cbdb856635e">
|
||||
<source>Recent</source>
|
||||
<target>最近搜索</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb4f1dffbb6be6302">
|
||||
<source>Clear</source>
|
||||
<target>清空</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<file target-language="zh-TW" source-language="en" original="lit-localize-inputs" datatype="plaintext">
|
||||
<body>
|
||||
<trans-unit id="sfc63e31ac7c956ed">
|
||||
<source>Select</source>
|
||||
<target>選擇</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb85774dc5d18ff0f">
|
||||
<source>Confirm</source>
|
||||
<target>確認</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s5e8250fb85d64c23">
|
||||
<source>Close</source>
|
||||
<target>關閉</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sad5f82f8364e5a1c">
|
||||
<source>Enter keywords to search</source>
|
||||
<target>輸入關鍵字以搜尋</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa84142fc34654130">
|
||||
<source>No search results</source>
|
||||
<target>沒有搜尋結果</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbce70cbdb856635e">
|
||||
<source>Recent</source>
|
||||
<target>最近搜尋</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb4f1dffbb6be6302">
|
||||
<source>Clear</source>
|
||||
<target>清空</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "tsc && vite build",
|
||||
"dev": "vite",
|
||||
"lint": "eslint 'src/**/*.ts'",
|
||||
"prettier": "prettier \"**/*.{cjs,html,js,json,md,ts}\" --ignore-path ./.gitignore --write"
|
||||
},
|
||||
"dependencies": {
|
||||
"@halo-dev/search-widget": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { SearchModal } from '@halo-dev/search-widget';
|
||||
import '@halo-dev/search-widget/var.css';
|
||||
import type { SearchOption } from '@halo-dev/api-client';
|
||||
|
||||
export { SearchModal };
|
||||
|
||||
const searchModalElement = document.createElement(
|
||||
'search-modal'
|
||||
) as SearchModal;
|
||||
|
||||
document.body.append(searchModalElement);
|
||||
|
||||
export function open(options: SearchOption) {
|
||||
searchModalElement.options = options;
|
||||
searchModalElement.open = true;
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath, URL } from 'node:url';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
experimental: {
|
||||
enableNativePlugin: true,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
define: { 'process.env.NODE_ENV': "'production'" },
|
||||
build: {
|
||||
outDir: fileURLToPath(
|
||||
new URL('../../src/main/resources/static', import.meta.url)
|
||||
),
|
||||
emptyOutDir: true,
|
||||
cssCodeSplit: false,
|
||||
lib: {
|
||||
entry: path.resolve(__dirname, 'src/index.ts'),
|
||||
formats: ['iife'],
|
||||
name: 'SearchWidget',
|
||||
fileName: (format) => `search-widget.${format}.js`,
|
||||
cssFileName: 'style',
|
||||
},
|
||||
sourcemap: false,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user