refactor(search): extract search into workspace package
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
@import "@workspace/ui/globals.css";
|
||||
@import "@workspace/blocks/globals.css";
|
||||
@import "@workspace/lexical/globals.css";
|
||||
@import "@workspace/search/globals.css";
|
||||
|
||||
:root {
|
||||
--ui-content-top-gap: --spacing(2);
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import { Dialog } from "@base-ui/react/dialog"
|
||||
|
||||
/** A shared handle for the optional global-search trigger. */
|
||||
export const searchDialogHandle = Dialog.createHandle<void>()
|
||||
@@ -1,34 +0,0 @@
|
||||
import { describe, it } from "vitest"
|
||||
|
||||
import { expectCompleteCatalogs } from "../../../i18n/test-catalogs"
|
||||
import { searchMessages } from "../messages"
|
||||
import { searchCatalogLocales } from "./catalogs"
|
||||
import { messages as de } from "./de"
|
||||
import { messages as en } from "./en"
|
||||
import { messages as es } from "./es"
|
||||
import { messages as fr } from "./fr"
|
||||
import { messages as ja } from "./ja"
|
||||
import { messages as ko } from "./ko"
|
||||
import { messages as zhHans } from "./zh-Hans"
|
||||
import { messages as zhHant } from "./zh-Hant"
|
||||
|
||||
describe("search locale catalogs", () => {
|
||||
it("ship every search message in every built-in locale", () => {
|
||||
expectCompleteCatalogs({
|
||||
catalogs: {
|
||||
de,
|
||||
en,
|
||||
es,
|
||||
fr,
|
||||
ja,
|
||||
ko,
|
||||
"zh-Hans": zhHans,
|
||||
"zh-Hant": zhHant,
|
||||
},
|
||||
locales: searchCatalogLocales,
|
||||
messageIds: Object.values(searchMessages).map(
|
||||
(descriptor) => descriptor.id
|
||||
),
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,10 +0,0 @@
|
||||
import type { searchMessages } from "../messages"
|
||||
import {
|
||||
blockCatalogLocales,
|
||||
type BlockCatalogLocale,
|
||||
type BlockMessageCatalog,
|
||||
} from "../../../i18n/catalogs"
|
||||
|
||||
export { blockCatalogLocales as searchCatalogLocales }
|
||||
export type SearchCatalogLocale = BlockCatalogLocale
|
||||
export type SearchMessageCatalog = BlockMessageCatalog<typeof searchMessages>
|
||||
@@ -1,29 +0,0 @@
|
||||
import type { SearchMessageCatalog } from "./catalogs"
|
||||
|
||||
export const locale = "de"
|
||||
export const languageTag = "de-DE"
|
||||
export const messages = {
|
||||
"blocks.search.actions.clear": "Suche löschen",
|
||||
"blocks.search.actions.clearHistory": "Verlauf löschen",
|
||||
"blocks.search.actions.close": "Suche schließen",
|
||||
"blocks.search.actions.loadMore": "Mehr laden",
|
||||
"blocks.search.actions.loadingMore": "Wird geladen…",
|
||||
"blocks.search.actions.retry": "Erneut versuchen",
|
||||
"blocks.search.actions.selectResult": "{title} öffnen",
|
||||
"blocks.search.command.description": "Im gesamten Arbeitsbereich suchen",
|
||||
"blocks.search.description": "Inhalte im gesamten Arbeitsbereich finden",
|
||||
"blocks.search.empty.description": "Keine Ergebnisse für „{query}“",
|
||||
"blocks.search.empty.title": "Keine Ergebnisse gefunden",
|
||||
"blocks.search.error.description":
|
||||
"Prüfe deine Verbindung und versuche es erneut.",
|
||||
"blocks.search.error.title": "Suche nach „{query}“ fehlgeschlagen",
|
||||
"blocks.search.history.remove": "„{query}“ aus den letzten Suchen entfernen",
|
||||
"blocks.search.history.title": "Letzte Suchen",
|
||||
"blocks.search.initial.description":
|
||||
"Gib ein Stichwort ein, um den Arbeitsbereich zu durchsuchen.",
|
||||
"blocks.search.initial.title": "Suche starten",
|
||||
"blocks.search.loading": "Suche läuft…",
|
||||
"blocks.search.placeholder": "Arbeitsbereich durchsuchen…",
|
||||
"blocks.search.results.count": "{count} Ergebnisse",
|
||||
"blocks.search.title": "Suche",
|
||||
} as const satisfies SearchMessageCatalog
|
||||
@@ -1,28 +0,0 @@
|
||||
import type { SearchMessageCatalog } from "./catalogs"
|
||||
|
||||
export const locale = "en"
|
||||
export const languageTag = "en-US"
|
||||
export const messages = {
|
||||
"blocks.search.actions.clear": "Clear query",
|
||||
"blocks.search.actions.clearHistory": "Clear history",
|
||||
"blocks.search.actions.close": "Close search",
|
||||
"blocks.search.actions.loadMore": "Load more",
|
||||
"blocks.search.actions.loadingMore": "Loading…",
|
||||
"blocks.search.actions.retry": "Try again",
|
||||
"blocks.search.actions.selectResult": "Open {title}",
|
||||
"blocks.search.command.description": "Search across your workspace",
|
||||
"blocks.search.description": "Find content across your workspace",
|
||||
"blocks.search.empty.description": "No results for “{query}”",
|
||||
"blocks.search.empty.title": "No results found",
|
||||
"blocks.search.error.description": "Check your connection and try again.",
|
||||
"blocks.search.error.title": "Could not search for “{query}”",
|
||||
"blocks.search.history.remove": "Remove “{query}” from recent searches",
|
||||
"blocks.search.history.title": "Recent searches",
|
||||
"blocks.search.initial.description":
|
||||
"Enter a keyword to search your workspace.",
|
||||
"blocks.search.initial.title": "Start searching",
|
||||
"blocks.search.loading": "Searching…",
|
||||
"blocks.search.placeholder": "Search your workspace…",
|
||||
"blocks.search.results.count": "{count} results",
|
||||
"blocks.search.title": "Search",
|
||||
} as const satisfies SearchMessageCatalog
|
||||
@@ -1,30 +0,0 @@
|
||||
import type { SearchMessageCatalog } from "./catalogs"
|
||||
|
||||
export const locale = "es"
|
||||
export const languageTag = "es-ES"
|
||||
export const messages = {
|
||||
"blocks.search.actions.clear": "Borrar búsqueda",
|
||||
"blocks.search.actions.clearHistory": "Borrar historial",
|
||||
"blocks.search.actions.close": "Cerrar búsqueda",
|
||||
"blocks.search.actions.loadMore": "Cargar más",
|
||||
"blocks.search.actions.loadingMore": "Cargando…",
|
||||
"blocks.search.actions.retry": "Intentar de nuevo",
|
||||
"blocks.search.actions.selectResult": "Abrir {title}",
|
||||
"blocks.search.command.description": "Buscar en todo tu espacio de trabajo",
|
||||
"blocks.search.description":
|
||||
"Encuentra contenido en todo tu espacio de trabajo",
|
||||
"blocks.search.empty.description": "No hay resultados para «{query}»",
|
||||
"blocks.search.empty.title": "No se encontraron resultados",
|
||||
"blocks.search.error.description":
|
||||
"Comprueba tu conexión e inténtalo de nuevo.",
|
||||
"blocks.search.error.title": "No se pudo buscar «{query}»",
|
||||
"blocks.search.history.remove": "Quitar «{query}» de las búsquedas recientes",
|
||||
"blocks.search.history.title": "Búsquedas recientes",
|
||||
"blocks.search.initial.description":
|
||||
"Introduce una palabra clave para buscar en tu espacio de trabajo.",
|
||||
"blocks.search.initial.title": "Empezar a buscar",
|
||||
"blocks.search.loading": "Buscando…",
|
||||
"blocks.search.placeholder": "Buscar en tu espacio de trabajo…",
|
||||
"blocks.search.results.count": "{count} resultados",
|
||||
"blocks.search.title": "Buscar",
|
||||
} as const satisfies SearchMessageCatalog
|
||||
@@ -1,30 +0,0 @@
|
||||
import type { SearchMessageCatalog } from "./catalogs"
|
||||
|
||||
export const locale = "fr"
|
||||
export const languageTag = "fr-FR"
|
||||
export const messages = {
|
||||
"blocks.search.actions.clear": "Effacer la recherche",
|
||||
"blocks.search.actions.clearHistory": "Effacer l’historique",
|
||||
"blocks.search.actions.close": "Fermer la recherche",
|
||||
"blocks.search.actions.loadMore": "Charger plus",
|
||||
"blocks.search.actions.loadingMore": "Chargement…",
|
||||
"blocks.search.actions.retry": "Réessayer",
|
||||
"blocks.search.actions.selectResult": "Ouvrir {title}",
|
||||
"blocks.search.command.description":
|
||||
"Rechercher dans tout l’espace de travail",
|
||||
"blocks.search.description":
|
||||
"Trouver du contenu dans votre espace de travail",
|
||||
"blocks.search.empty.description": "Aucun résultat pour « {query} »",
|
||||
"blocks.search.empty.title": "Aucun résultat trouvé",
|
||||
"blocks.search.error.description": "Vérifiez votre connexion et réessayez.",
|
||||
"blocks.search.error.title": "Impossible de rechercher « {query} »",
|
||||
"blocks.search.history.remove": "Retirer « {query} » des recherches récentes",
|
||||
"blocks.search.history.title": "Recherches récentes",
|
||||
"blocks.search.initial.description":
|
||||
"Saisissez un mot-clé pour rechercher dans votre espace de travail.",
|
||||
"blocks.search.initial.title": "Commencer la recherche",
|
||||
"blocks.search.loading": "Recherche…",
|
||||
"blocks.search.placeholder": "Rechercher dans l’espace de travail…",
|
||||
"blocks.search.results.count": "{count} résultats",
|
||||
"blocks.search.title": "Rechercher",
|
||||
} as const satisfies SearchMessageCatalog
|
||||
@@ -1,28 +0,0 @@
|
||||
import type { SearchMessageCatalog } from "./catalogs"
|
||||
|
||||
export const locale = "ja"
|
||||
export const languageTag = "ja-JP"
|
||||
export const messages = {
|
||||
"blocks.search.actions.clear": "検索をクリア",
|
||||
"blocks.search.actions.clearHistory": "履歴を削除",
|
||||
"blocks.search.actions.close": "検索を閉じる",
|
||||
"blocks.search.actions.loadMore": "さらに読み込む",
|
||||
"blocks.search.actions.loadingMore": "読み込み中…",
|
||||
"blocks.search.actions.retry": "再試行",
|
||||
"blocks.search.actions.selectResult": "{title} を開く",
|
||||
"blocks.search.command.description": "ワークスペース全体を検索",
|
||||
"blocks.search.description": "ワークスペース全体からコンテンツを探す",
|
||||
"blocks.search.empty.description": "「{query}」の結果はありません",
|
||||
"blocks.search.empty.title": "結果が見つかりません",
|
||||
"blocks.search.error.description": "接続を確認して、もう一度お試しください。",
|
||||
"blocks.search.error.title": "「{query}」を検索できませんでした",
|
||||
"blocks.search.history.remove": "最近の検索から「{query}」を削除",
|
||||
"blocks.search.history.title": "最近の検索",
|
||||
"blocks.search.initial.description":
|
||||
"キーワードを入力してワークスペースを検索します。",
|
||||
"blocks.search.initial.title": "検索を始める",
|
||||
"blocks.search.loading": "検索中…",
|
||||
"blocks.search.placeholder": "ワークスペースを検索…",
|
||||
"blocks.search.results.count": "{count} 件の結果",
|
||||
"blocks.search.title": "検索",
|
||||
} as const satisfies SearchMessageCatalog
|
||||
@@ -1,28 +0,0 @@
|
||||
import type { SearchMessageCatalog } from "./catalogs"
|
||||
|
||||
export const locale = "ko"
|
||||
export const languageTag = "ko-KR"
|
||||
export const messages = {
|
||||
"blocks.search.actions.clear": "검색어 지우기",
|
||||
"blocks.search.actions.clearHistory": "기록 지우기",
|
||||
"blocks.search.actions.close": "검색 닫기",
|
||||
"blocks.search.actions.loadMore": "더 불러오기",
|
||||
"blocks.search.actions.loadingMore": "불러오는 중…",
|
||||
"blocks.search.actions.retry": "다시 시도",
|
||||
"blocks.search.actions.selectResult": "{title} 열기",
|
||||
"blocks.search.command.description": "작업 공간 전체 검색",
|
||||
"blocks.search.description": "작업 공간 전체에서 콘텐츠 찾기",
|
||||
"blocks.search.empty.description": "“{query}”에 대한 결과가 없습니다",
|
||||
"blocks.search.empty.title": "결과를 찾을 수 없습니다",
|
||||
"blocks.search.error.description": "연결을 확인한 후 다시 시도하세요.",
|
||||
"blocks.search.error.title": "“{query}”을(를) 검색할 수 없습니다",
|
||||
"blocks.search.history.remove": "최근 검색에서 “{query}” 제거",
|
||||
"blocks.search.history.title": "최근 검색",
|
||||
"blocks.search.initial.description":
|
||||
"키워드를 입력하여 작업 공간을 검색하세요.",
|
||||
"blocks.search.initial.title": "검색 시작",
|
||||
"blocks.search.loading": "검색 중…",
|
||||
"blocks.search.placeholder": "작업 공간 검색…",
|
||||
"blocks.search.results.count": "결과 {count}개",
|
||||
"blocks.search.title": "검색",
|
||||
} as const satisfies SearchMessageCatalog
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { SearchMessageCatalog } from "./catalogs"
|
||||
|
||||
export const locale = "zh-Hans"
|
||||
export const languageTag = "zh-CN"
|
||||
export const messages = {
|
||||
"blocks.search.actions.clear": "清除查询",
|
||||
"blocks.search.actions.clearHistory": "清空历史",
|
||||
"blocks.search.actions.close": "关闭搜索",
|
||||
"blocks.search.actions.loadMore": "加载更多",
|
||||
"blocks.search.actions.loadingMore": "正在加载…",
|
||||
"blocks.search.actions.retry": "重试",
|
||||
"blocks.search.actions.selectResult": "打开 {title}",
|
||||
"blocks.search.command.description": "搜索整个工作区",
|
||||
"blocks.search.description": "在工作区中查找内容",
|
||||
"blocks.search.empty.description": "没有找到与“{query}”相关的内容",
|
||||
"blocks.search.empty.title": "未找到结果",
|
||||
"blocks.search.error.description": "请检查网络连接后重试。",
|
||||
"blocks.search.error.title": "无法搜索“{query}”",
|
||||
"blocks.search.history.remove": "从搜索历史中删除“{query}”",
|
||||
"blocks.search.history.title": "搜索历史",
|
||||
"blocks.search.initial.description": "输入关键词以搜索工作区。",
|
||||
"blocks.search.initial.title": "开始搜索",
|
||||
"blocks.search.loading": "正在搜索…",
|
||||
"blocks.search.placeholder": "搜索工作区…",
|
||||
"blocks.search.results.count": "{count} 条结果",
|
||||
"blocks.search.title": "搜索",
|
||||
} as const satisfies SearchMessageCatalog
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { SearchMessageCatalog } from "./catalogs"
|
||||
|
||||
export const locale = "zh-Hant"
|
||||
export const languageTag = "zh-TW"
|
||||
export const messages = {
|
||||
"blocks.search.actions.clear": "清除搜尋",
|
||||
"blocks.search.actions.clearHistory": "清除記錄",
|
||||
"blocks.search.actions.close": "關閉搜尋",
|
||||
"blocks.search.actions.loadMore": "載入更多",
|
||||
"blocks.search.actions.loadingMore": "載入中…",
|
||||
"blocks.search.actions.retry": "再試一次",
|
||||
"blocks.search.actions.selectResult": "開啟 {title}",
|
||||
"blocks.search.command.description": "搜尋整個工作區",
|
||||
"blocks.search.description": "在工作區中尋找內容",
|
||||
"blocks.search.empty.description": "沒有「{query}」的結果",
|
||||
"blocks.search.empty.title": "找不到結果",
|
||||
"blocks.search.error.description": "請檢查網絡連線後再試一次。",
|
||||
"blocks.search.error.title": "無法搜尋「{query}」",
|
||||
"blocks.search.history.remove": "從最近搜尋中移除「{query}」",
|
||||
"blocks.search.history.title": "最近搜尋",
|
||||
"blocks.search.initial.description": "輸入關鍵字以搜尋工作區。",
|
||||
"blocks.search.initial.title": "開始搜尋",
|
||||
"blocks.search.loading": "搜尋中…",
|
||||
"blocks.search.placeholder": "搜尋工作區…",
|
||||
"blocks.search.results.count": "{count} 項結果",
|
||||
"blocks.search.title": "搜尋",
|
||||
} as const satisfies SearchMessageCatalog
|
||||
@@ -1,64 +0,0 @@
|
||||
import type { MessageDescriptor } from "@workspace/i18n"
|
||||
|
||||
export const searchMessages = {
|
||||
clear: { id: "blocks.search.actions.clear", message: "Clear query" },
|
||||
clearHistory: {
|
||||
id: "blocks.search.actions.clearHistory",
|
||||
message: "Clear history",
|
||||
},
|
||||
close: { id: "blocks.search.actions.close", message: "Close search" },
|
||||
loadMore: { id: "blocks.search.actions.loadMore", message: "Load more" },
|
||||
loadingMore: {
|
||||
id: "blocks.search.actions.loadingMore",
|
||||
message: "Loading…",
|
||||
},
|
||||
retry: { id: "blocks.search.actions.retry", message: "Try again" },
|
||||
selectResult: {
|
||||
id: "blocks.search.actions.selectResult",
|
||||
message: "Open {title}",
|
||||
},
|
||||
commandDescription: {
|
||||
id: "blocks.search.command.description",
|
||||
message: "Search across your workspace",
|
||||
},
|
||||
description: {
|
||||
id: "blocks.search.description",
|
||||
message: "Find content across your workspace",
|
||||
},
|
||||
emptyDescription: {
|
||||
id: "blocks.search.empty.description",
|
||||
message: "No results for “{query}”",
|
||||
},
|
||||
emptyTitle: { id: "blocks.search.empty.title", message: "No results found" },
|
||||
errorDescription: {
|
||||
id: "blocks.search.error.description",
|
||||
message: "Check your connection and try again.",
|
||||
},
|
||||
errorTitle: {
|
||||
id: "blocks.search.error.title",
|
||||
message: "Could not search for “{query}”",
|
||||
},
|
||||
history: { id: "blocks.search.history.title", message: "Recent searches" },
|
||||
initialDescription: {
|
||||
id: "blocks.search.initial.description",
|
||||
message: "Enter a keyword to search your workspace.",
|
||||
},
|
||||
initialTitle: {
|
||||
id: "blocks.search.initial.title",
|
||||
message: "Start searching",
|
||||
},
|
||||
loading: { id: "blocks.search.loading", message: "Searching…" },
|
||||
placeholder: {
|
||||
id: "blocks.search.placeholder",
|
||||
message: "Search your workspace…",
|
||||
},
|
||||
removeHistory: {
|
||||
id: "blocks.search.history.remove",
|
||||
message: "Remove “{query}” from recent searches",
|
||||
},
|
||||
resultCount: {
|
||||
id: "blocks.search.results.count",
|
||||
message: "{count} results",
|
||||
},
|
||||
title: { id: "blocks.search.title", message: "Search" },
|
||||
} as const satisfies Record<string, MessageDescriptor>
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@workspace/search",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-hotkeys": "^0.10.0",
|
||||
"@workspace/i18n": "workspace:*",
|
||||
"@workspace/ui": "workspace:*",
|
||||
"cmdk": "^1.1.1",
|
||||
"react": "^19.2.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"jsdom": "^30.0.1",
|
||||
"react-dom": "^19.2.6",
|
||||
"typescript": "~6",
|
||||
"vitest": "^4.1.10"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./globals.css": "./src/styles/globals.css",
|
||||
"./locales": "./src/locales/catalogs.ts",
|
||||
"./locales/*": "./src/locales/*.ts"
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export interface SearchProviderProps {
|
||||
onSelect?: SearchSelectionHandler
|
||||
}
|
||||
|
||||
/** Supplies the host application's search implementation to the search blocks. */
|
||||
/** Supplies the host application's search implementation to the search package. */
|
||||
export function SearchProvider({
|
||||
adapter,
|
||||
children,
|
||||
@@ -48,7 +48,9 @@ export function SearchProvider({
|
||||
export function useSearchContext(): SearchContextValue {
|
||||
const value = React.useContext(SearchContext)
|
||||
if (!value) {
|
||||
throw new Error("Search blocks must be rendered inside a SearchProvider.")
|
||||
throw new Error(
|
||||
"Search components must be rendered inside a SearchProvider."
|
||||
)
|
||||
}
|
||||
return value
|
||||
}
|
||||
@@ -11,7 +11,11 @@ import {
|
||||
DialogTitle,
|
||||
} from "@workspace/ui/components/dialog"
|
||||
import { cn } from "@workspace/ui/lib/utils"
|
||||
import { LoaderCircleIcon, SearchIcon, XIcon } from "lucide-react"
|
||||
import {
|
||||
LoaderCircleIcon,
|
||||
SearchIcon,
|
||||
XIcon,
|
||||
} from "@workspace/ui/components/icon"
|
||||
|
||||
import { useSearchContext } from "./context"
|
||||
import { searchDialogHandle } from "./handle"
|
||||
@@ -0,0 +1,4 @@
|
||||
import { createDialogHandle } from "@workspace/ui/components/dialog"
|
||||
|
||||
/** A shared handle for the optional global-search trigger. */
|
||||
export const searchDialogHandle = createDialogHandle<void>()
|
||||
@@ -3,7 +3,7 @@ import { useTranslate } from "@workspace/i18n"
|
||||
import { Button } from "@workspace/ui/components/button"
|
||||
import { ScrollArea } from "@workspace/ui/components/scroll-area"
|
||||
import { cn } from "@workspace/ui/lib/utils"
|
||||
import { HistoryIcon, Trash2Icon, XIcon } from "lucide-react"
|
||||
import { HistoryIcon, Trash2Icon, XIcon } from "@workspace/ui/components/icon"
|
||||
|
||||
import { searchMessages } from "./messages"
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import { searchMessages } from "../messages"
|
||||
import { searchCatalogLocales } from "./catalogs"
|
||||
import { messages as enUS } from "./en-US"
|
||||
import { messages as zhHans } from "./zh-Hans"
|
||||
|
||||
describe("search locale catalogs", () => {
|
||||
it("ship every search message in every built-in locale", () => {
|
||||
expectCompleteCatalogs({
|
||||
catalogs: {
|
||||
"en-US": enUS,
|
||||
"zh-Hans": zhHans,
|
||||
},
|
||||
locales: searchCatalogLocales,
|
||||
messageIds: Object.values(searchMessages).map(
|
||||
(descriptor) => descriptor.id
|
||||
),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function expectCompleteCatalogs({
|
||||
catalogs,
|
||||
locales,
|
||||
messageIds,
|
||||
}: {
|
||||
catalogs: Readonly<Record<string, Readonly<Record<string, string>>>>
|
||||
locales: readonly string[]
|
||||
messageIds: readonly string[]
|
||||
}) {
|
||||
expect(Object.keys(catalogs)).toEqual([...locales])
|
||||
|
||||
const expectedIds = new Set(messageIds)
|
||||
for (const messages of Object.values(catalogs)) {
|
||||
expect(new Set(Object.keys(messages))).toEqual(expectedIds)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { searchMessages } from "../messages"
|
||||
|
||||
export const searchCatalogLocales = ["en-US", "zh-Hans"] as const
|
||||
|
||||
interface MessageDescriptorMap {
|
||||
readonly [key: string]: {
|
||||
readonly id: string
|
||||
}
|
||||
}
|
||||
|
||||
type MessageId<T extends MessageDescriptorMap> = T[keyof T]["id"]
|
||||
|
||||
export type SearchCatalogLocale = (typeof searchCatalogLocales)[number]
|
||||
export type SearchMessageCatalog = Readonly<
|
||||
Record<MessageId<typeof searchMessages>, string>
|
||||
>
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { SearchMessageCatalog } from "./catalogs"
|
||||
|
||||
export const locale = "en-US"
|
||||
export const languageTag = "en-US"
|
||||
export const messages = {
|
||||
"search.actions.clear": "Clear query",
|
||||
"search.actions.clearHistory": "Clear history",
|
||||
"search.actions.close": "Close search",
|
||||
"search.actions.loadMore": "Load more",
|
||||
"search.actions.loadingMore": "Loading…",
|
||||
"search.actions.retry": "Try again",
|
||||
"search.actions.selectResult": "Open {title}",
|
||||
"search.command.description": "Search across your workspace",
|
||||
"search.description": "Find content across your workspace",
|
||||
"search.empty.description": "No results for “{query}”",
|
||||
"search.empty.title": "No results found",
|
||||
"search.error.description": "Check your connection and try again.",
|
||||
"search.error.title": "Could not search for “{query}”",
|
||||
"search.history.remove": "Remove “{query}” from recent searches",
|
||||
"search.history.title": "Recent searches",
|
||||
"search.initial.description": "Enter a keyword to search your workspace.",
|
||||
"search.initial.title": "Start searching",
|
||||
"search.loading": "Searching…",
|
||||
"search.placeholder": "Search your workspace…",
|
||||
"search.results.count": "{count} results",
|
||||
"search.title": "Search",
|
||||
} as const satisfies SearchMessageCatalog
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { SearchMessageCatalog } from "./catalogs"
|
||||
|
||||
export const locale = "zh-Hans"
|
||||
export const languageTag = "zh-CN"
|
||||
export const messages = {
|
||||
"search.actions.clear": "清除查询",
|
||||
"search.actions.clearHistory": "清空历史",
|
||||
"search.actions.close": "关闭搜索",
|
||||
"search.actions.loadMore": "加载更多",
|
||||
"search.actions.loadingMore": "正在加载…",
|
||||
"search.actions.retry": "重试",
|
||||
"search.actions.selectResult": "打开 {title}",
|
||||
"search.command.description": "搜索整个工作区",
|
||||
"search.description": "在工作区中查找内容",
|
||||
"search.empty.description": "没有找到与“{query}”相关的内容",
|
||||
"search.empty.title": "未找到结果",
|
||||
"search.error.description": "请检查网络连接后重试。",
|
||||
"search.error.title": "无法搜索“{query}”",
|
||||
"search.history.remove": "从搜索历史中删除“{query}”",
|
||||
"search.history.title": "搜索历史",
|
||||
"search.initial.description": "输入关键词以搜索工作区。",
|
||||
"search.initial.title": "开始搜索",
|
||||
"search.loading": "正在搜索…",
|
||||
"search.placeholder": "搜索工作区…",
|
||||
"search.results.count": "{count} 条结果",
|
||||
"search.title": "搜索",
|
||||
} as const satisfies SearchMessageCatalog
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { MessageDescriptor } from "@workspace/i18n"
|
||||
|
||||
export const searchMessages = {
|
||||
clear: { id: "search.actions.clear", message: "Clear query" },
|
||||
clearHistory: {
|
||||
id: "search.actions.clearHistory",
|
||||
message: "Clear history",
|
||||
},
|
||||
close: { id: "search.actions.close", message: "Close search" },
|
||||
loadMore: { id: "search.actions.loadMore", message: "Load more" },
|
||||
loadingMore: {
|
||||
id: "search.actions.loadingMore",
|
||||
message: "Loading…",
|
||||
},
|
||||
retry: { id: "search.actions.retry", message: "Try again" },
|
||||
selectResult: {
|
||||
id: "search.actions.selectResult",
|
||||
message: "Open {title}",
|
||||
},
|
||||
commandDescription: {
|
||||
id: "search.command.description",
|
||||
message: "Search across your workspace",
|
||||
},
|
||||
description: {
|
||||
id: "search.description",
|
||||
message: "Find content across your workspace",
|
||||
},
|
||||
emptyDescription: {
|
||||
id: "search.empty.description",
|
||||
message: "No results for “{query}”",
|
||||
},
|
||||
emptyTitle: { id: "search.empty.title", message: "No results found" },
|
||||
errorDescription: {
|
||||
id: "search.error.description",
|
||||
message: "Check your connection and try again.",
|
||||
},
|
||||
errorTitle: {
|
||||
id: "search.error.title",
|
||||
message: "Could not search for “{query}”",
|
||||
},
|
||||
history: { id: "search.history.title", message: "Recent searches" },
|
||||
initialDescription: {
|
||||
id: "search.initial.description",
|
||||
message: "Enter a keyword to search your workspace.",
|
||||
},
|
||||
initialTitle: {
|
||||
id: "search.initial.title",
|
||||
message: "Start searching",
|
||||
},
|
||||
loading: { id: "search.loading", message: "Searching…" },
|
||||
placeholder: {
|
||||
id: "search.placeholder",
|
||||
message: "Search your workspace…",
|
||||
},
|
||||
removeHistory: {
|
||||
id: "search.history.remove",
|
||||
message: "Remove “{query}” from recent searches",
|
||||
},
|
||||
resultCount: {
|
||||
id: "search.results.count",
|
||||
message: "{count} results",
|
||||
},
|
||||
title: { id: "search.title", message: "Search" },
|
||||
} as const satisfies Record<string, MessageDescriptor>
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
CornerDownLeftIcon,
|
||||
FileTextIcon,
|
||||
LoaderCircleIcon,
|
||||
} from "lucide-react"
|
||||
} from "@workspace/ui/components/icon"
|
||||
|
||||
import { splitHighlightSegments } from "./highlight"
|
||||
import { searchMessages } from "./messages"
|
||||
@@ -124,9 +124,9 @@ function SearchResultRow({
|
||||
<HighlightedText text={item.description} query={query} />
|
||||
</span>
|
||||
)}
|
||||
{item.meta?.map((line, index) => (
|
||||
{item.meta?.map((line) => (
|
||||
<span
|
||||
key={`${line}:${index}`}
|
||||
key={line}
|
||||
className="block truncate text-xs text-muted-foreground"
|
||||
>
|
||||
<HighlightedText text={line} query={query} />
|
||||
@@ -158,16 +158,18 @@ function ResultMedia({ item }: { item: SearchResultItem }) {
|
||||
}
|
||||
|
||||
function HighlightedText({ text, query }: { text: string; query: string }) {
|
||||
return splitHighlightSegments(text, query).map((segment, index) =>
|
||||
segment.highlighted ? (
|
||||
<mark
|
||||
key={index}
|
||||
className="rounded-sm bg-primary/15 px-0.5 text-primary"
|
||||
>
|
||||
let offset = 0
|
||||
|
||||
return splitHighlightSegments(text, query).map((segment) => {
|
||||
const key = `${offset}:${segment.highlighted ? "highlight" : "text"}`
|
||||
offset += segment.text.length
|
||||
|
||||
return segment.highlighted ? (
|
||||
<mark key={key} className="rounded-sm bg-primary/15 px-0.5 text-primary">
|
||||
{segment.text}
|
||||
</mark>
|
||||
) : (
|
||||
<React.Fragment key={index}>{segment.text}</React.Fragment>
|
||||
)
|
||||
<React.Fragment key={key}>{segment.text}</React.Fragment>
|
||||
)
|
||||
})
|
||||
}
|
||||
+3
-3
@@ -14,7 +14,7 @@ import { SearchProvider } from "./context"
|
||||
import { SearchDialog } from "./dialog"
|
||||
import { searchDialogHandle } from "./handle"
|
||||
import { splitHighlightSegments } from "./highlight"
|
||||
import { messages as englishMessages } from "./locales/en"
|
||||
import { messages as englishMessages } from "./locales/en-US"
|
||||
import type { SearchAdapter } from "./types"
|
||||
|
||||
class ResizeObserverStub {
|
||||
@@ -37,7 +37,7 @@ function renderSearch({
|
||||
onSelect?: (event: { item: { id: string }; query: string }) => void
|
||||
}) {
|
||||
return render(
|
||||
<I18nProvider locale="en" catalogs={{ en: englishMessages }}>
|
||||
<I18nProvider locale="en-US" catalogs={{ "en-US": englishMessages }}>
|
||||
<SearchProvider
|
||||
adapter={adapter}
|
||||
historyStorageKey={false}
|
||||
@@ -49,7 +49,7 @@ function renderSearch({
|
||||
)
|
||||
}
|
||||
|
||||
describe("search blocks", () => {
|
||||
describe("search package", () => {
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
window.localStorage.clear()
|
||||
@@ -7,7 +7,11 @@ import {
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@workspace/ui/components/empty"
|
||||
import { CircleAlertIcon, LoaderCircleIcon, SearchIcon } from "lucide-react"
|
||||
import {
|
||||
CircleAlertIcon,
|
||||
LoaderCircleIcon,
|
||||
SearchIcon,
|
||||
} from "@workspace/ui/components/icon"
|
||||
|
||||
import { searchMessages } from "./messages"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
@source "../";
|
||||
@@ -46,7 +46,7 @@ export interface SearchRequest {
|
||||
}
|
||||
|
||||
/**
|
||||
* The only data dependency of the search blocks. Implement this in the host
|
||||
* The only data dependency of the search package. Implement this in the host
|
||||
* application with its own database, HTTP client, command registry, or index.
|
||||
*/
|
||||
export interface SearchAdapter {
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react"
|
||||
|
||||
import type { SearchCursor, SearchPage, SearchResultGroup } from "./types"
|
||||
import type { SearchPage, SearchResultGroup } from "./types"
|
||||
import { useSearchContext } from "./context"
|
||||
|
||||
interface UseSearchOptions {
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"jsx": "react-jsx",
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"paths": {
|
||||
"@workspace/i18n": ["../i18n/src/index.ts"],
|
||||
"@workspace/ui/*": ["../ui/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user