refactor(search): extract search into workspace package

This commit is contained in:
Maofeng
2026-09-20 15:52:09 +08:00
parent e02c3dceb5
commit 1fc56b6982
34 changed files with 262 additions and 360 deletions
+1
View File
@@ -1,6 +1,7 @@
@import "@workspace/ui/globals.css"; @import "@workspace/ui/globals.css";
@import "@workspace/blocks/globals.css"; @import "@workspace/blocks/globals.css";
@import "@workspace/lexical/globals.css"; @import "@workspace/lexical/globals.css";
@import "@workspace/search/globals.css";
:root { :root {
--ui-content-top-gap: --spacing(2); --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 lhistorique",
"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 lespace 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 lespace 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>
+32
View File
@@ -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 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({ export function SearchProvider({
adapter, adapter,
children, children,
@@ -48,7 +48,9 @@ export function SearchProvider({
export function useSearchContext(): SearchContextValue { export function useSearchContext(): SearchContextValue {
const value = React.useContext(SearchContext) const value = React.useContext(SearchContext)
if (!value) { 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 return value
} }
@@ -11,7 +11,11 @@ import {
DialogTitle, DialogTitle,
} from "@workspace/ui/components/dialog" } from "@workspace/ui/components/dialog"
import { cn } from "@workspace/ui/lib/utils" 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 { useSearchContext } from "./context"
import { searchDialogHandle } from "./handle" import { searchDialogHandle } from "./handle"
+4
View File
@@ -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 { Button } from "@workspace/ui/components/button"
import { ScrollArea } from "@workspace/ui/components/scroll-area" import { ScrollArea } from "@workspace/ui/components/scroll-area"
import { cn } from "@workspace/ui/lib/utils" 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" 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)
}
}
+16
View File
@@ -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>
>
+27
View File
@@ -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
+27
View File
@@ -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
+64
View File
@@ -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, CornerDownLeftIcon,
FileTextIcon, FileTextIcon,
LoaderCircleIcon, LoaderCircleIcon,
} from "lucide-react" } from "@workspace/ui/components/icon"
import { splitHighlightSegments } from "./highlight" import { splitHighlightSegments } from "./highlight"
import { searchMessages } from "./messages" import { searchMessages } from "./messages"
@@ -124,9 +124,9 @@ function SearchResultRow({
<HighlightedText text={item.description} query={query} /> <HighlightedText text={item.description} query={query} />
</span> </span>
)} )}
{item.meta?.map((line, index) => ( {item.meta?.map((line) => (
<span <span
key={`${line}:${index}`} key={line}
className="block truncate text-xs text-muted-foreground" className="block truncate text-xs text-muted-foreground"
> >
<HighlightedText text={line} query={query} /> <HighlightedText text={line} query={query} />
@@ -158,16 +158,18 @@ function ResultMedia({ item }: { item: SearchResultItem }) {
} }
function HighlightedText({ text, query }: { text: string; query: string }) { function HighlightedText({ text, query }: { text: string; query: string }) {
return splitHighlightSegments(text, query).map((segment, index) => let offset = 0
segment.highlighted ? (
<mark return splitHighlightSegments(text, query).map((segment) => {
key={index} const key = `${offset}:${segment.highlighted ? "highlight" : "text"}`
className="rounded-sm bg-primary/15 px-0.5 text-primary" offset += segment.text.length
>
return segment.highlighted ? (
<mark key={key} className="rounded-sm bg-primary/15 px-0.5 text-primary">
{segment.text} {segment.text}
</mark> </mark>
) : ( ) : (
<React.Fragment key={index}>{segment.text}</React.Fragment> <React.Fragment key={key}>{segment.text}</React.Fragment>
) )
) })
} }
@@ -14,7 +14,7 @@ import { SearchProvider } from "./context"
import { SearchDialog } from "./dialog" import { SearchDialog } from "./dialog"
import { searchDialogHandle } from "./handle" import { searchDialogHandle } from "./handle"
import { splitHighlightSegments } from "./highlight" import { splitHighlightSegments } from "./highlight"
import { messages as englishMessages } from "./locales/en" import { messages as englishMessages } from "./locales/en-US"
import type { SearchAdapter } from "./types" import type { SearchAdapter } from "./types"
class ResizeObserverStub { class ResizeObserverStub {
@@ -37,7 +37,7 @@ function renderSearch({
onSelect?: (event: { item: { id: string }; query: string }) => void onSelect?: (event: { item: { id: string }; query: string }) => void
}) { }) {
return render( return render(
<I18nProvider locale="en" catalogs={{ en: englishMessages }}> <I18nProvider locale="en-US" catalogs={{ "en-US": englishMessages }}>
<SearchProvider <SearchProvider
adapter={adapter} adapter={adapter}
historyStorageKey={false} historyStorageKey={false}
@@ -49,7 +49,7 @@ function renderSearch({
) )
} }
describe("search blocks", () => { describe("search package", () => {
afterEach(() => { afterEach(() => {
cleanup() cleanup()
window.localStorage.clear() window.localStorage.clear()
@@ -7,7 +7,11 @@ import {
EmptyMedia, EmptyMedia,
EmptyTitle, EmptyTitle,
} from "@workspace/ui/components/empty" } 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" import { searchMessages } from "./messages"
+1
View File
@@ -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. * application with its own database, HTTP client, command registry, or index.
*/ */
export interface SearchAdapter { export interface SearchAdapter {
@@ -1,6 +1,6 @@
import * as React from "react" import * as React from "react"
import type { SearchCursor, SearchPage, SearchResultGroup } from "./types" import type { SearchPage, SearchResultGroup } from "./types"
import { useSearchContext } from "./context" import { useSearchContext } from "./context"
interface UseSearchOptions { interface UseSearchOptions {
+19
View File
@@ -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"]
}