@echothink-ui/search 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +5 -0
  2. package/dist/components/AgentSearchSuggestion.d.ts +2 -0
  3. package/dist/components/AppCommandSearch.d.ts +2 -0
  4. package/dist/components/AppDomainSearch.d.ts +2 -0
  5. package/dist/components/EntitySearchInput.d.ts +4 -0
  6. package/dist/components/GlobalCommandPalette.d.ts +4 -0
  7. package/dist/components/ProjectCommandPalette.d.ts +2 -0
  8. package/dist/components/RecentItems.d.ts +2 -0
  9. package/dist/components/ResourceSearch.d.ts +2 -0
  10. package/dist/components/SavedSearches.d.ts +2 -0
  11. package/dist/components/SearchFacetPanel.d.ts +2 -0
  12. package/dist/components/SearchResultsPanel.d.ts +2 -0
  13. package/dist/components/SemanticSearchResult.d.ts +2 -0
  14. package/dist/components/searchUtils.d.ts +3 -0
  15. package/dist/components/types.d.ts +175 -0
  16. package/dist/index.cjs +1224 -0
  17. package/dist/index.cjs.map +1 -0
  18. package/dist/index.css +1460 -0
  19. package/dist/index.css.map +1 -0
  20. package/dist/index.d.ts +16 -0
  21. package/dist/index.js +1185 -0
  22. package/dist/index.js.map +1 -0
  23. package/package.json +43 -0
  24. package/src/components/AgentSearchSuggestion.tsx +44 -0
  25. package/src/components/AppCommandSearch.tsx +163 -0
  26. package/src/components/AppDomainSearch.tsx +90 -0
  27. package/src/components/EntitySearchInput.tsx +165 -0
  28. package/src/components/GlobalCommandPalette.tsx +182 -0
  29. package/src/components/ProjectCommandPalette.tsx +24 -0
  30. package/src/components/RecentItems.tsx +136 -0
  31. package/src/components/ResourceSearch.tsx +27 -0
  32. package/src/components/SavedSearches.tsx +27 -0
  33. package/src/components/SearchFacetPanel.tsx +100 -0
  34. package/src/components/SearchResultsPanel.tsx +327 -0
  35. package/src/components/SemanticSearchResult.tsx +52 -0
  36. package/src/components/searchUtils.ts +20 -0
  37. package/src/components/types.ts +208 -0
  38. package/src/index.test.tsx +254 -0
  39. package/src/index.tsx +55 -0
  40. package/src/styles.css +1716 -0
@@ -0,0 +1,254 @@
1
+ import { fireEvent, render, screen } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import {
4
+ AppCommandSearch,
5
+ ProjectCommandPalette,
6
+ RecentItems,
7
+ ResourceSearch,
8
+ SearchFacetPanel,
9
+ SearchResultsPanel,
10
+ SemanticSearchResult
11
+ } from "./index";
12
+
13
+ const commands = [
14
+ {
15
+ id: "open-approvals",
16
+ label: "Open approval queue",
17
+ description: "Review resource actions waiting on legal.",
18
+ domain: "Workflow",
19
+ kind: "app-domain" as const,
20
+ shortcut: "G A"
21
+ },
22
+ {
23
+ id: "draft-follow-up",
24
+ label: "Draft follow-up",
25
+ description: "Create a message from the selected resource.",
26
+ domain: "Inbox",
27
+ kind: "action" as const
28
+ }
29
+ ];
30
+
31
+ const paletteCommands = [
32
+ {
33
+ id: "open-approvals",
34
+ label: "Open approval queue",
35
+ group: "Navigation",
36
+ hint: "Review resource actions waiting on legal.",
37
+ onSelect: () => undefined
38
+ }
39
+ ];
40
+
41
+ describe("@echothink-ui/search", () => {
42
+ it("renders command search affordances and scoped command results", () => {
43
+ render(
44
+ <AppCommandSearch value="approval" scopeLabel="Workspace domains" commands={commands} />
45
+ );
46
+
47
+ expect(screen.getByDisplayValue("approval")).toBeTruthy();
48
+ expect(screen.getByText("⌘K")).toBeTruthy();
49
+ expect(screen.getByText("Workspace domains")).toBeTruthy();
50
+ expect(screen.getByText("Open approval queue")).toBeTruthy();
51
+ expect(screen.getByText("Workflow")).toBeTruthy();
52
+ expect(screen.getByRole("button", { name: "Run" })).toBeTruthy();
53
+ });
54
+
55
+ it("selects the active command on submit", () => {
56
+ const onSelect = vi.fn();
57
+ const { container } = render(
58
+ <AppCommandSearch value="approval" commands={commands} onSelect={onSelect} />
59
+ );
60
+
61
+ fireEvent.submit(container.querySelector("form")!);
62
+
63
+ expect(onSelect).toHaveBeenCalledWith("open-approvals");
64
+ });
65
+
66
+ it("labels the project scoped command palette with its project context", () => {
67
+ render(
68
+ <ProjectCommandPalette
69
+ open
70
+ onClose={vi.fn()}
71
+ projectRef="marketing"
72
+ commands={paletteCommands}
73
+ />
74
+ );
75
+
76
+ expect(screen.getByRole("dialog", { name: "Project command palette" })).toBeTruthy();
77
+ expect(screen.getByText("Project marketing")).toBeTruthy();
78
+ expect(screen.getByText("Open approval queue")).toBeTruthy();
79
+ });
80
+
81
+ it("renders grouped search results with metadata and row selection", () => {
82
+ const onSelect = vi.fn();
83
+ const { rerender } = render(
84
+ <SearchResultsPanel
85
+ query="brief"
86
+ groups={[
87
+ {
88
+ id: "tasks",
89
+ label: "Tasks",
90
+ results: [
91
+ {
92
+ id: "t1",
93
+ label: "Approve Q3 brief",
94
+ description: "Campaign task waiting on final signoff",
95
+ owner: "Jane",
96
+ status: "Due today"
97
+ },
98
+ {
99
+ id: "t2",
100
+ label: "Draft pricing copy",
101
+ description: "Matched by launch brief"
102
+ }
103
+ ]
104
+ },
105
+ {
106
+ id: "docs",
107
+ label: "Documents",
108
+ results: [
109
+ {
110
+ id: "d1",
111
+ label: "Brief.md",
112
+ version: 12,
113
+ updatedAt: "2h ago"
114
+ }
115
+ ]
116
+ }
117
+ ]}
118
+ onSelect={onSelect}
119
+ />
120
+ );
121
+
122
+ expect(screen.getByText("3 results")).toBeTruthy();
123
+ expect(screen.getByText("\"brief\"")).toBeTruthy();
124
+ expect(screen.getByText("Tasks")).toBeTruthy();
125
+ expect(screen.getByText("Campaign task waiting on final signoff")).toBeTruthy();
126
+ expect(screen.getByText("Owner: Jane")).toBeTruthy();
127
+ expect(screen.getByText("Due today")).toBeTruthy();
128
+ expect(screen.getByText("v12")).toBeTruthy();
129
+
130
+ fireEvent.click(screen.getByRole("button", { name: /approve q3 brief/i }));
131
+
132
+ expect(onSelect).toHaveBeenCalledWith("t1");
133
+
134
+ rerender(
135
+ <SearchResultsPanel query="missing" groups={[]} emptyLabel="Nothing found" />
136
+ );
137
+
138
+ expect(screen.getByText("Nothing found")).toBeTruthy();
139
+ expect(screen.getByText("Try another keyword or remove a filter.")).toBeTruthy();
140
+ });
141
+
142
+ it("renders recent items with row-level actions and an empty state", () => {
143
+ const onSelect = vi.fn();
144
+ const { rerender } = render(
145
+ <RecentItems
146
+ items={[
147
+ {
148
+ id: "brief",
149
+ label: "Brief.md",
150
+ kind: "document",
151
+ description: "Updated project kickoff notes",
152
+ visitedAt: "10:42"
153
+ }
154
+ ]}
155
+ onSelect={onSelect}
156
+ />
157
+ );
158
+
159
+ fireEvent.click(screen.getByRole("button", { name: /open brief\.md/i }));
160
+
161
+ expect(onSelect).toHaveBeenCalledWith("brief");
162
+ expect(screen.getByText("Document")).toBeTruthy();
163
+ expect(screen.getByText("Updated project kickoff notes")).toBeTruthy();
164
+ expect(screen.getByText("10:42")).toBeTruthy();
165
+
166
+ rerender(<RecentItems items={[]} />);
167
+
168
+ expect(screen.getByText("No recent items")).toBeTruthy();
169
+ });
170
+
171
+ it("renders resource search with resource-specific suggestion semantics", () => {
172
+ const { container } = render(
173
+ <ResourceSearch
174
+ open
175
+ resourceScope="project:marketing"
176
+ suggestions={[
177
+ {
178
+ id: "brief",
179
+ label: "Launch brief",
180
+ kind: "Document",
181
+ meta: "Synced 12 min ago"
182
+ }
183
+ ]}
184
+ />
185
+ );
186
+
187
+ expect(container.firstElementChild?.getAttribute("data-eth-component")).toBe(
188
+ "ResourceSearch"
189
+ );
190
+ expect(container.firstElementChild?.getAttribute("data-resource-scope")).toBe(
191
+ "project:marketing"
192
+ );
193
+ expect(screen.getByRole("listbox", { name: "Resource suggestions" })).toBeTruthy();
194
+ expect(screen.getByText("Resource matches")).toBeTruthy();
195
+ expect(screen.getByText("Launch brief")).toBeTruthy();
196
+ });
197
+
198
+ it("renders selected search facets with a clear action", () => {
199
+ const onChange = vi.fn();
200
+
201
+ render(
202
+ <SearchFacetPanel
203
+ facets={[
204
+ {
205
+ id: "kind",
206
+ label: "Kind",
207
+ options: [
208
+ { value: "task", label: "Task", count: 12 },
209
+ { value: "doc", label: "Document", count: 4 }
210
+ ]
211
+ },
212
+ {
213
+ id: "status",
214
+ label: "Status",
215
+ options: [{ value: "open", label: "Open", count: 10 }]
216
+ }
217
+ ]}
218
+ selected={{ kind: ["task"], status: ["open"] }}
219
+ onChange={onChange}
220
+ />
221
+ );
222
+
223
+ expect(screen.getByRole("complementary", { name: "Search filters" })).toBeTruthy();
224
+ expect(screen.getByText("2 selected")).toBeTruthy();
225
+ expect(screen.getByText("Task")).toBeTruthy();
226
+ expect(screen.getByText("12")).toBeTruthy();
227
+
228
+ fireEvent.click(screen.getByRole("button", { name: "Clear" }));
229
+
230
+ expect(onChange).toHaveBeenCalledWith({ kind: [], status: [] });
231
+ });
232
+
233
+ it("renders semantic search snippets with highlighted evidence", () => {
234
+ const { container } = render(
235
+ <SemanticSearchResult
236
+ result={{
237
+ id: "policy",
238
+ snippet: "Approval policy requires legal review for external sends.",
239
+ highlights: [[0, 17]],
240
+ evidence: [
241
+ { id: "policy-v3", label: "policy-v3.md", href: "#" },
242
+ { id: "incident-review", label: "Q2 incident review" }
243
+ ]
244
+ }}
245
+ />
246
+ );
247
+
248
+ expect(container.querySelector("mark")?.textContent).toBe("Approval policy");
249
+ expect(screen.getByText("Approval policy")).toBeTruthy();
250
+ expect(screen.getByText("Evidence")).toBeTruthy();
251
+ expect(screen.getByRole("link", { name: "policy-v3.md" })).toBeTruthy();
252
+ expect(screen.getByText("Q2 incident review")).toBeTruthy();
253
+ });
254
+ });
package/src/index.tsx ADDED
@@ -0,0 +1,55 @@
1
+ import "./styles.css";
2
+
3
+ export type {
4
+ AgentSearchSuggestionProps,
5
+ AppCommandItem,
6
+ AppCommandKind,
7
+ AppCommandSearchProps,
8
+ AppDomainSearchProps,
9
+ CommandItem,
10
+ EntitySearchInputProps,
11
+ EntitySuggestion,
12
+ GlobalCommandPaletteProps,
13
+ ProjectCommandPaletteProps,
14
+ RecentItem,
15
+ RecentItemsProps,
16
+ ResourceSearchProps,
17
+ SavedSearch,
18
+ SavedSearchesProps,
19
+ SearchFacet,
20
+ SearchFacetOption,
21
+ SearchFacetPanelProps,
22
+ SearchResultGroup,
23
+ SearchResultItem,
24
+ SearchResultMetadata,
25
+ SearchResultsPanelProps,
26
+ SemanticSearchResultProps
27
+ } from "./components/types";
28
+ export { AgentSearchSuggestion } from "./components/AgentSearchSuggestion";
29
+ export { AppCommandSearch } from "./components/AppCommandSearch";
30
+ export { AppDomainSearch } from "./components/AppDomainSearch";
31
+ export { EntitySearchInput } from "./components/EntitySearchInput";
32
+ export { GlobalCommandPalette } from "./components/GlobalCommandPalette";
33
+ export { ProjectCommandPalette } from "./components/ProjectCommandPalette";
34
+ export { RecentItems } from "./components/RecentItems";
35
+ export { ResourceSearch } from "./components/ResourceSearch";
36
+ export { SavedSearches } from "./components/SavedSearches";
37
+ export { SearchFacetPanel } from "./components/SearchFacetPanel";
38
+ export { SearchResultsPanel } from "./components/SearchResultsPanel";
39
+ export { SemanticSearchResult } from "./components/SemanticSearchResult";
40
+
41
+ export const SearchComponentNames = [
42
+ "GlobalCommandPalette",
43
+ "ProjectCommandPalette",
44
+ "AppCommandSearch",
45
+ "SearchResultsPanel",
46
+ "SearchFacetPanel",
47
+ "SavedSearches",
48
+ "RecentItems",
49
+ "EntitySearchInput",
50
+ "AppDomainSearch",
51
+ "ResourceSearch",
52
+ "SemanticSearchResult",
53
+ "AgentSearchSuggestion"
54
+ ] as const;
55
+ export type SearchComponentName = (typeof SearchComponentNames)[number];