@eventcatalog/core 3.35.1 → 3.36.2

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 (89) hide show
  1. package/dist/analytics/analytics.cjs +1 -1
  2. package/dist/analytics/analytics.js +2 -2
  3. package/dist/analytics/log-build.cjs +1 -1
  4. package/dist/analytics/log-build.js +3 -3
  5. package/dist/chunk-D6IBLY3O.js +320 -0
  6. package/dist/{chunk-R4DR3YAH.js → chunk-H6TGUW5O.js} +1 -1
  7. package/dist/{chunk-VJ357XOI.js → chunk-IO4U4MPC.js} +1 -1
  8. package/dist/{chunk-JEQZWJWP.js → chunk-L723FWAT.js} +1 -1
  9. package/dist/{chunk-B7C4DHFE.js → chunk-R5ZDI2JO.js} +1 -1
  10. package/dist/{chunk-4SNN54V4.js → chunk-SEAN3UND.js} +1 -1
  11. package/dist/{chunk-3KXCGYET.js → chunk-ULZYHF3V.js} +5 -0
  12. package/dist/constants.cjs +1 -1
  13. package/dist/constants.js +1 -1
  14. package/dist/docs/api/02-config.md +22 -0
  15. package/dist/docs/api/_category_.json +1 -1
  16. package/dist/docs/contributing/_category_.json +1 -1
  17. package/dist/docs/development/_category_.json +1 -1
  18. package/dist/docs/development/ask-your-architecture/02-eventcatalog-assistant/_category_.json +1 -1
  19. package/dist/docs/development/ask-your-architecture/03-mcp-server/_category_.json +1 -1
  20. package/dist/docs/development/ask-your-architecture/04-skills/_category_.json +1 -1
  21. package/dist/docs/development/authentication/providers/_category_.json +1 -1
  22. package/dist/docs/development/bring-your-own-documentation/custom-pages/_category_.json +1 -1
  23. package/dist/docs/development/customization/01-customize-landing-page.md +1 -1
  24. package/dist/docs/development/customization/03-search.md +79 -0
  25. package/dist/docs/development/customization/custom-components/_category_.json +1 -1
  26. package/dist/docs/development/customization/customize-sidebars/_category_.json +1 -1
  27. package/dist/docs/development/customization/customize-visualizer/_category_.json +1 -1
  28. package/dist/docs/development/design/_category_.json +1 -1
  29. package/dist/docs/development/guides/changelogs/_category_.json +1 -1
  30. package/dist/docs/development/guides/channels/_category_.json +1 -1
  31. package/dist/docs/development/guides/channels/ownership-and-components/_category_.json +1 -1
  32. package/dist/docs/development/guides/channels/versioning-and-lifecycle/_category_.json +1 -1
  33. package/dist/docs/development/guides/data/_category_.json +1 -1
  34. package/dist/docs/development/guides/data/ownership-and-components/_category_.json +1 -1
  35. package/dist/docs/development/guides/data-products/_category_.json +1 -1
  36. package/dist/docs/development/guides/domains/02-creating-domains/_category_.json +1 -1
  37. package/dist/docs/development/guides/domains/03-ownership-and-language/_category_.json +1 -1
  38. package/dist/docs/development/guides/domains/05-entities/_category_.json +1 -1
  39. package/dist/docs/development/guides/domains/_category_.json +1 -1
  40. package/dist/docs/development/guides/flows/_category_.json +1 -1
  41. package/dist/docs/development/guides/messages/_category_.json +1 -1
  42. package/dist/docs/development/guides/messages/commands/_category_.json +1 -1
  43. package/dist/docs/development/guides/messages/common/_category_.json +1 -1
  44. package/dist/docs/development/guides/messages/events/_category_.json +1 -1
  45. package/dist/docs/development/guides/messages/queries/_category_.json +1 -1
  46. package/dist/docs/development/guides/owners/_category_.json +1 -1
  47. package/dist/docs/development/guides/owners/teams/_category_.json +1 -1
  48. package/dist/docs/development/guides/owners/users/_category_.json +1 -1
  49. package/dist/docs/development/guides/schemas/_category_.json +1 -1
  50. package/dist/docs/development/guides/services/_category_.json +1 -1
  51. package/dist/docs/development/guides/services/adding-to-services/_category_.json +1 -1
  52. package/dist/docs/development/guides/services/ownership-and-components/_category_.json +1 -1
  53. package/dist/docs/development/guides/services/versioning-and-lifecycle/_category_.json +1 -1
  54. package/dist/docs/plugins/_category_.json +1 -1
  55. package/dist/docs/plugins/amazon-apigateway/_category_.json +1 -1
  56. package/dist/docs/plugins/asyncapi/_category_.json +1 -1
  57. package/dist/docs/plugins/aws-glue-registry/_category_.json +1 -1
  58. package/dist/docs/plugins/backstage/_category_.json +1 -1
  59. package/dist/docs/plugins/confluent-schema-registry/_category_.json +1 -1
  60. package/dist/docs/plugins/eventbridge/_category_.json +1 -1
  61. package/dist/docs/plugins/eventcatalog-federation/_category_.json +1 -1
  62. package/dist/docs/plugins/github/_category_.json +1 -1
  63. package/dist/docs/plugins/graphql/_category_.json +1 -1
  64. package/dist/docs/plugins/hookdeck/_category_.json +1 -1
  65. package/dist/docs/plugins/openapi/_category_.json +1 -1
  66. package/dist/eventcatalog.cjs +434 -35
  67. package/dist/eventcatalog.config.d.cts +13 -1
  68. package/dist/eventcatalog.config.d.ts +13 -1
  69. package/dist/eventcatalog.js +88 -11
  70. package/dist/features.cjs +6 -0
  71. package/dist/features.d.cts +2 -1
  72. package/dist/features.d.ts +2 -1
  73. package/dist/features.js +3 -1
  74. package/dist/generate.cjs +1 -1
  75. package/dist/generate.js +3 -3
  76. package/dist/search-indexer.cjs +356 -0
  77. package/dist/search-indexer.d.cts +30 -0
  78. package/dist/search-indexer.d.ts +30 -0
  79. package/dist/search-indexer.js +10 -0
  80. package/dist/utils/cli-logger.cjs +1 -1
  81. package/dist/utils/cli-logger.js +2 -2
  82. package/eventcatalog/astro.config.mjs +28 -32
  83. package/eventcatalog/src/components/Search/SearchModal.tsx +248 -148
  84. package/eventcatalog/src/components/Search/search-utils.spec.ts +138 -1
  85. package/eventcatalog/src/components/Search/search-utils.ts +271 -0
  86. package/eventcatalog/src/env.d.ts +1 -0
  87. package/eventcatalog/src/layouts/BaseLayout.astro +4 -7
  88. package/eventcatalog/src/stores/theme-store.ts +9 -13
  89. package/package.json +3 -2
@@ -27,7 +27,18 @@ import { useStore } from '@nanostores/react';
27
27
  import { favoritesStore, toggleFavorite as toggleFavoriteAction } from '../../stores/favorites-store';
28
28
  import { buildUrl } from '@utils/url-builder';
29
29
  import { resolveIconUrl } from '@utils/icon';
30
- import { getUrlForSearchItem } from './search-utils';
30
+ import {
31
+ applyActiveFilter,
32
+ getSearchFilters,
33
+ getUrlForSearchItem,
34
+ highlightQuery,
35
+ mapPagefindResultsToSearchItems,
36
+ type SearchItem,
37
+ type SearchNode,
38
+ } from './search-utils';
39
+
40
+ const INDEXED_RESULT_LOAD_LIMIT = 50;
41
+ const SEARCH_RESULT_DISPLAY_LIMIT = 25;
31
42
 
32
43
  const typeIcons: any = {
33
44
  Domain: RectangleGroupIcon,
@@ -46,6 +57,9 @@ const typeIcons: any = {
46
57
  Container: CircleStackIcon,
47
58
  'Data Product': CubeIcon,
48
59
  Flow: QueueListIcon,
60
+ 'Custom Doc': DocumentTextIcon,
61
+ 'Resource Doc': DocumentTextIcon,
62
+ Changelog: DocumentTextIcon,
49
63
  default: DocumentTextIcon,
50
64
  };
51
65
 
@@ -67,6 +81,9 @@ const typeColors: any = {
67
81
  Container: 'text-indigo-500 dark:text-indigo-400 bg-indigo-50 dark:bg-indigo-500/10 ring-indigo-200 dark:ring-indigo-500/30',
68
82
  'Data Product': 'text-sky-500 dark:text-sky-400 bg-sky-50 dark:bg-sky-500/10 ring-sky-200 dark:ring-sky-500/30',
69
83
  Flow: 'text-fuchsia-500 dark:text-fuchsia-400 bg-fuchsia-50 dark:bg-fuchsia-500/10 ring-fuchsia-200 dark:ring-fuchsia-500/30',
84
+ 'Custom Doc': 'text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-500/10 ring-gray-200 dark:ring-gray-500/30',
85
+ 'Resource Doc': 'text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-500/10 ring-gray-200 dark:ring-gray-500/30',
86
+ Changelog: 'text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-500/10 ring-gray-200 dark:ring-gray-500/30',
70
87
  default: 'text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-500/10 ring-gray-200 dark:ring-gray-500/30',
71
88
  };
72
89
 
@@ -74,15 +91,8 @@ function classNames(...classes: (string | boolean | undefined)[]) {
74
91
  return classes.filter(Boolean).join(' ');
75
92
  }
76
93
 
77
- interface SearchNode {
78
- key: string;
79
- title: string;
80
- badge?: string;
81
- summary?: string;
82
- href?: string;
83
- icon?: string;
84
- leftIcon?: string;
85
- }
94
+ const searchResultHighlightClassName =
95
+ '[&_mark]:bg-transparent [&_mark]:p-0 [&_mark]:font-semibold [&_mark]:text-[rgb(var(--ec-accent))]';
86
96
 
87
97
  interface SearchNodeCompact {
88
98
  k: string;
@@ -99,6 +109,12 @@ interface SearchIndexPayload {
99
109
  items?: SearchNode[];
100
110
  }
101
111
 
112
+ interface PagefindModule {
113
+ init: () => Promise<void>;
114
+ options?: (options: Record<string, unknown>) => Promise<void>;
115
+ debouncedSearch: (term: string) => Promise<{ results: Array<{ id: string; score?: number; data: () => Promise<any> }> } | null>;
116
+ }
117
+
102
118
  const normalizeSearchIndexPayload = (payload: SearchIndexPayload): SearchNode[] => {
103
119
  if (payload.i) {
104
120
  return payload.i.map((item) => ({
@@ -115,11 +131,21 @@ const normalizeSearchIndexPayload = (payload: SearchIndexPayload): SearchNode[]
115
131
  return payload.items || [];
116
132
  };
117
133
 
134
+ const loadPagefindModule = (url: string) => {
135
+ const nativeImport = new Function('url', 'return import(url)') as (url: string) => Promise<PagefindModule>;
136
+ return nativeImport(url);
137
+ };
138
+
118
139
  export default function SearchModal() {
140
+ const searchType = typeof __EC_SEARCH_TYPE__ !== 'undefined' ? __EC_SEARCH_TYPE__ : 'resource';
141
+ const isIndexedSearch = searchType === 'indexed';
119
142
  const [query, setQuery] = useState('');
120
143
  const [open, setOpen] = useState(false);
121
144
  const [activeFilter, setActiveFilter] = useState('all');
122
145
  const [searchNodes, setSearchNodes] = useState<SearchNode[]>([]);
146
+ const [indexedItems, setIndexedItems] = useState<SearchItem[]>([]);
147
+ const [pagefind, setPagefind] = useState<PagefindModule | null>(null);
148
+ const [isSearchingIndexed, setIsSearchingIndexed] = useState(false);
123
149
  const [isLoadingSearchIndex, setIsLoadingSearchIndex] = useState(false);
124
150
  const [searchIndexLoadError, setSearchIndexLoadError] = useState<string | null>(null);
125
151
  const favorites = useStore(favoritesStore);
@@ -141,7 +167,7 @@ export default function SearchModal() {
141
167
  }, []);
142
168
 
143
169
  useEffect(() => {
144
- if (!open || searchNodes.length > 0 || isLoadingSearchIndex) {
170
+ if (isIndexedSearch || !open || searchNodes.length > 0 || isLoadingSearchIndex) {
145
171
  return;
146
172
  }
147
173
 
@@ -166,7 +192,46 @@ export default function SearchModal() {
166
192
  .finally(() => {
167
193
  setIsLoadingSearchIndex(false);
168
194
  });
169
- }, [open, searchNodes.length, isLoadingSearchIndex]);
195
+ }, [isIndexedSearch, open, searchNodes.length, isLoadingSearchIndex]);
196
+
197
+ useEffect(() => {
198
+ if (!isIndexedSearch || !open || pagefind || isLoadingSearchIndex) {
199
+ return;
200
+ }
201
+
202
+ setIsLoadingSearchIndex(true);
203
+ setSearchIndexLoadError(null);
204
+
205
+ const pagefindUrl = buildUrl('/pagefind/pagefind.js', true);
206
+
207
+ loadPagefindModule(pagefindUrl)
208
+ .then(async (module: PagefindModule) => {
209
+ await module.options?.({
210
+ excerptLength: 30,
211
+ ranking: {
212
+ metaWeights: {
213
+ title: 5.0,
214
+ id: 4.0,
215
+ summary: 2.0,
216
+ type: 1.5,
217
+ },
218
+ },
219
+ });
220
+ await module.init();
221
+ setPagefind(module);
222
+ })
223
+ .catch((error) => {
224
+ console.error(error);
225
+ setSearchIndexLoadError(
226
+ import.meta.env.DEV
227
+ ? 'The local indexed search files could not be loaded. Restart the catalog dev server to rebuild the index.'
228
+ : 'Indexed search is enabled, but the generated search index could not be loaded. Run `eventcatalog build` to create it.'
229
+ );
230
+ })
231
+ .finally(() => {
232
+ setIsLoadingSearchIndex(false);
233
+ });
234
+ }, [isIndexedSearch, open, pagefind, isLoadingSearchIndex]);
170
235
 
171
236
  const closeModal = () => {
172
237
  if ((window as any).searchModalState) {
@@ -194,8 +259,55 @@ export default function SearchModal() {
194
259
  .filter((item): item is NonNullable<typeof item> => item !== null);
195
260
  }, [searchNodes]);
196
261
 
262
+ useEffect(() => {
263
+ if (!isIndexedSearch || !pagefind || query.trim() === '') {
264
+ setIndexedItems([]);
265
+ setIsSearchingIndexed(false);
266
+ return;
267
+ }
268
+
269
+ let cancelled = false;
270
+ setIsSearchingIndexed(true);
271
+
272
+ const timeout = window.setTimeout(async () => {
273
+ try {
274
+ const search = await pagefind.debouncedSearch(query);
275
+ if (cancelled || !search?.results) {
276
+ return;
277
+ }
278
+
279
+ const results = await mapPagefindResultsToSearchItems({
280
+ results: search.results,
281
+ query,
282
+ limit: INDEXED_RESULT_LOAD_LIMIT,
283
+ });
284
+
285
+ if (!cancelled) {
286
+ setIndexedItems(results);
287
+ }
288
+ } catch (error) {
289
+ if (!cancelled) {
290
+ setSearchIndexLoadError(error instanceof Error ? error.message : 'Unable to search the indexed catalog');
291
+ }
292
+ } finally {
293
+ if (!cancelled) {
294
+ setIsSearchingIndexed(false);
295
+ }
296
+ }
297
+ }, 120);
298
+
299
+ return () => {
300
+ cancelled = true;
301
+ window.clearTimeout(timeout);
302
+ };
303
+ }, [isIndexedSearch, pagefind, query]);
304
+
197
305
  // Get searchable items (items that match the query but not filtered by type yet)
198
306
  const searchableItems = useMemo(() => {
307
+ if (isIndexedSearch) {
308
+ return indexedItems;
309
+ }
310
+
199
311
  if (query === '') {
200
312
  // When no query, show all items for filter counts
201
313
  return items;
@@ -208,66 +320,20 @@ export default function SearchModal() {
208
320
  // Match against the id so users can find resources by their raw id too.
209
321
  const keyParts = item.key?.split(':') ?? [];
210
322
  const id = keyParts[1];
211
- return !!id && id.toLowerCase().includes(lowerQuery);
323
+ if (id?.toLowerCase().includes(lowerQuery)) return true;
324
+ return !!item.rawNode.summary && item.rawNode.summary.toLowerCase().includes(lowerQuery);
212
325
  });
213
- }, [items, query]);
326
+ }, [indexedItems, isIndexedSearch, items, query]);
214
327
 
215
328
  const filters = useMemo(() => {
216
- // Calculate counts based on current search results (searchableItems)
217
- if (!searchableItems.length && query !== '') {
218
- // If searching and no results, still show filters but with 0 counts
219
- return [{ id: 'all', name: 'All (0)' }];
220
- }
221
-
222
- const itemsToCount = query === '' ? items : searchableItems;
223
-
224
- const counts: Record<string, number> = {
225
- all: itemsToCount.length,
226
- Domain: 0,
227
- Service: 0,
228
- Message: 0,
229
- Team: 0,
230
- Container: 0,
231
- Entity: 0,
232
- Design: 0,
233
- Channel: 0,
234
- Flow: 0,
235
- 'Data Product': 0,
236
- };
237
-
238
- itemsToCount.forEach((item) => {
239
- // Count specific types
240
- if (counts[item.type] !== undefined) {
241
- counts[item.type]++;
242
- }
243
-
244
- // Group counts
245
- if (['Event', 'Command', 'Query'].includes(item.type)) {
246
- counts.Message++;
247
- }
248
- if (['Team', 'User'].includes(item.type)) {
249
- counts.Team++;
250
- }
329
+ return getSearchFilters({
330
+ items: query === '' ? items : searchableItems,
331
+ query,
251
332
  });
252
-
253
- const dynamicFilters = [{ id: 'all', name: `All (${counts.all})` }];
254
-
255
- // Only show filters that have results when searching
256
- if (counts.Domain > 0) dynamicFilters.push({ id: 'Domain', name: `Domains (${counts.Domain})` });
257
- if (counts.Service > 0) dynamicFilters.push({ id: 'Service', name: `Services (${counts.Service})` });
258
- if (counts.Message > 0) dynamicFilters.push({ id: 'Message', name: `Messages (${counts.Message})` });
259
- if (counts.Container > 0) dynamicFilters.push({ id: 'Container', name: `Data Stores (${counts.Container})` });
260
- if (counts.Entity > 0) dynamicFilters.push({ id: 'Entity', name: `Entities (${counts.Entity})` });
261
- if (counts.Channel > 0) dynamicFilters.push({ id: 'Channel', name: `Channels (${counts.Channel})` });
262
- if (counts.Flow > 0) dynamicFilters.push({ id: 'Flow', name: `Flows (${counts.Flow})` });
263
- if (counts['Data Product'] > 0)
264
- dynamicFilters.push({ id: 'Data Product', name: `Data Products (${counts['Data Product']})` });
265
- if (counts.Design > 0) dynamicFilters.push({ id: 'Design', name: `Designs (${counts.Design})` });
266
- if (counts.Team > 0) dynamicFilters.push({ id: 'Team', name: `Teams & Users (${counts.Team})` });
267
-
268
- return dynamicFilters;
269
333
  }, [searchableItems, items, query]);
270
334
 
335
+ const showFilterTabs = filters.some((filter) => filter.count > 0);
336
+
271
337
  // Reset active filter if it no longer has results
272
338
  useEffect(() => {
273
339
  if (activeFilter !== 'all' && !filters.some((f) => f.id === activeFilter)) {
@@ -279,6 +345,8 @@ export default function SearchModal() {
279
345
  e.preventDefault();
280
346
  e.stopPropagation();
281
347
 
348
+ if (!item.key) return;
349
+
282
350
  toggleFavoriteAction({
283
351
  nodeKey: item.key,
284
352
  path: [], // Path is not easily available here, but Sidebar rebuilds it
@@ -293,6 +361,14 @@ export default function SearchModal() {
293
361
  }, [searchNodes]);
294
362
 
295
363
  const filteredItems = useMemo(() => {
364
+ if (isIndexedSearch) {
365
+ if (query === '') {
366
+ return [];
367
+ }
368
+
369
+ return applyActiveFilter(searchableItems, activeFilter).slice(0, SEARCH_RESULT_DISPLAY_LIMIT);
370
+ }
371
+
296
372
  if (query === '') {
297
373
  // Show favorites when search is empty
298
374
  if (favorites.length > 0 && activeFilter === 'all') {
@@ -318,22 +394,8 @@ export default function SearchModal() {
318
394
  return [];
319
395
  }
320
396
 
321
- // Start with searchable items (already filtered by query)
322
- let result = searchableItems;
323
-
324
- // Apply type filter
325
- if (activeFilter !== 'all') {
326
- if (activeFilter === 'Message') {
327
- result = result.filter((item) => ['Event', 'Command', 'Query'].includes(item.type));
328
- } else if (activeFilter === 'Team') {
329
- result = result.filter((item) => ['Team', 'User'].includes(item.type));
330
- } else {
331
- result = result.filter((item) => item.type === activeFilter);
332
- }
333
- }
334
-
335
- return result.slice(0, 50); // Limit results for performance
336
- }, [searchableItems, query, activeFilter, favorites, searchNodeLookup]);
397
+ return applyActiveFilter(searchableItems, activeFilter).slice(0, SEARCH_RESULT_DISPLAY_LIMIT);
398
+ }, [isIndexedSearch, searchableItems, query, activeFilter, favorites, searchNodeLookup]);
337
399
 
338
400
  return (
339
401
  <Transition.Root
@@ -384,58 +446,74 @@ export default function SearchModal() {
384
446
  />
385
447
  <Combobox.Input
386
448
  ref={inputRef}
387
- className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-[rgb(var(--ec-page-text))] placeholder:text-[rgb(var(--ec-icon-color))] focus:ring-0 sm:text-sm focus:outline-hidden"
449
+ className={classNames(
450
+ 'h-12 w-full border-0 bg-transparent pl-11 text-[rgb(var(--ec-page-text))] placeholder:text-[rgb(var(--ec-icon-color))] focus:ring-0 sm:text-sm focus:outline-hidden',
451
+ query.trim() !== '' && filteredItems.length > 0 ? 'pr-20' : 'pr-4'
452
+ )}
388
453
  placeholder="Search..."
389
454
  onChange={(event) => setQuery(event.target.value)}
390
455
  value={query}
391
456
  autoFocus
392
457
  autoComplete="off"
393
458
  />
459
+ {query.trim() !== '' && filteredItems.length > 0 && (
460
+ <kbd className="pointer-events-none absolute right-4 top-2.5 rounded-lg bg-[rgb(var(--ec-content-hover))] px-2.5 py-1 text-xs font-semibold text-[rgb(var(--ec-page-text-muted))] ring-1 ring-inset ring-[rgb(var(--ec-page-border))]">
461
+ ESC
462
+ </kbd>
463
+ )}
394
464
  </div>
395
465
 
396
466
  {/* Filter Tabs */}
397
- <div
398
- className="flex items-center gap-2 px-4 pt-3 pb-3.5 overflow-x-auto overscroll-x-contain border-b border-[rgb(var(--ec-page-border))]"
399
- style={{
400
- scrollbarWidth: 'thin',
401
- scrollbarColor: 'rgb(var(--ec-page-border)) transparent',
402
- }}
403
- >
404
- {filters.map((tab) => (
405
- <button
406
- key={tab.id}
407
- onClick={() => setActiveFilter(tab.id)}
408
- className={classNames(
409
- 'px-3 py-1 text-xs font-medium rounded-full transition-colors whitespace-nowrap',
410
- activeFilter === tab.id
411
- ? 'bg-[rgb(var(--ec-accent-subtle))] text-[rgb(var(--ec-accent-text))]'
412
- : 'bg-[rgb(var(--ec-content-hover))] text-[rgb(var(--ec-page-text-muted))] hover:bg-[rgb(var(--ec-content-active))]'
413
- )}
414
- >
415
- {tab.name}
416
- </button>
417
- ))}
418
- </div>
467
+ {showFilterTabs && (
468
+ <div
469
+ className="flex items-center gap-2 px-4 pt-3 pb-3.5 overflow-x-auto overscroll-x-contain border-b border-[rgb(var(--ec-page-border))]"
470
+ style={{
471
+ scrollbarWidth: 'thin',
472
+ scrollbarColor: 'rgb(var(--ec-page-border)) transparent',
473
+ }}
474
+ >
475
+ {filters.map((tab) => (
476
+ <button
477
+ key={tab.id}
478
+ onClick={() => setActiveFilter(tab.id)}
479
+ className={classNames(
480
+ 'px-3 py-1 text-xs font-medium rounded-full transition-colors whitespace-nowrap',
481
+ activeFilter === tab.id
482
+ ? 'bg-[rgb(var(--ec-accent-subtle))] text-[rgb(var(--ec-accent-text))]'
483
+ : 'bg-[rgb(var(--ec-content-hover))] text-[rgb(var(--ec-page-text-muted))] hover:bg-[rgb(var(--ec-content-active))]'
484
+ )}
485
+ >
486
+ {tab.name}
487
+ </button>
488
+ ))}
489
+ </div>
490
+ )}
419
491
 
420
492
  {isLoadingSearchIndex && (
421
493
  <div className="py-10 px-6 text-center text-sm sm:px-14">
422
494
  <MagnifyingGlassIcon className="mx-auto h-6 w-6 text-[rgb(var(--ec-icon-color))] animate-pulse" />
423
495
  <p className="mt-4 font-semibold text-[rgb(var(--ec-page-text))]">Loading search index…</p>
424
- <p className="mt-2 text-[rgb(var(--ec-page-text-muted))]">Preparing resources for search.</p>
496
+ <p className="mt-2 text-[rgb(var(--ec-page-text-muted))]">
497
+ {isIndexedSearch ? 'Preparing indexed search.' : 'Preparing resources for search.'}
498
+ </p>
425
499
  </div>
426
500
  )}
427
501
 
428
502
  {searchIndexLoadError && !isLoadingSearchIndex && (
429
- <div className="py-10 px-6 text-center text-sm sm:px-14">
430
- <ExclamationCircleIcon className="mx-auto h-6 w-6 text-red-500" />
431
- <p className="mt-4 font-semibold text-[rgb(var(--ec-page-text))]">Search unavailable</p>
432
- <p className="mt-2 text-[rgb(var(--ec-page-text-muted))]">{searchIndexLoadError}</p>
503
+ <div className="px-6 py-12 text-center text-sm sm:px-14">
504
+ <div className="mx-auto flex h-10 w-10 items-center justify-center rounded-lg bg-[rgb(var(--ec-content-hover))] text-[rgb(var(--ec-icon-color))] ring-1 ring-inset ring-[rgb(var(--ec-page-border))]">
505
+ <ExclamationCircleIcon className="h-5 w-5" aria-hidden="true" />
506
+ </div>
507
+ <p className="mt-4 font-semibold text-[rgb(var(--ec-page-text))]">
508
+ {isIndexedSearch ? 'Indexed search is not ready' : 'Search is not ready'}
509
+ </p>
510
+ <p className="mx-auto mt-2 max-w-sm text-[rgb(var(--ec-page-text-muted))]">{searchIndexLoadError}</p>
433
511
  </div>
434
512
  )}
435
513
 
436
514
  {!isLoadingSearchIndex && !searchIndexLoadError && filteredItems.length > 0 && (
437
515
  <>
438
- {query === '' && favorites.length > 0 && (
516
+ {!isIndexedSearch && query === '' && favorites.length > 0 && (
439
517
  <div className="px-6 pt-3 pb-2">
440
518
  <p className="text-xs text-[rgb(var(--ec-page-text-muted))]">Favourites</p>
441
519
  </div>
@@ -445,7 +523,7 @@ export default function SearchModal() {
445
523
  const Icon = typeIcons[item.type] || typeIcons.default;
446
524
  const colors = typeColors[item.type] || typeColors.default;
447
525
 
448
- const isFavorite = favorites.some((fav) => fav.nodeKey === item.key);
526
+ const isFavorite = !!item.key && favorites.some((fav) => fav.nodeKey === item.key);
449
527
 
450
528
  return (
451
529
  <Combobox.Option
@@ -481,48 +559,60 @@ export default function SearchModal() {
481
559
  <p
482
560
  className={classNames(
483
561
  'text-sm font-medium',
562
+ searchResultHighlightClassName,
484
563
  active ? 'text-[rgb(var(--ec-page-text))]' : 'text-[rgb(var(--ec-page-text))]'
485
564
  )}
486
- >
487
- {item.name}
488
- </p>
565
+ dangerouslySetInnerHTML={{ __html: highlightQuery(item.name, query) }}
566
+ />
489
567
  <div className="flex items-center gap-2">
490
- <p
491
- className={classNames(
492
- 'text-sm flex-shrink-0',
493
- active ? 'text-[rgb(var(--ec-page-text))]' : 'text-[rgb(var(--ec-page-text-muted))]'
494
- )}
495
- >
496
- {item.type}
497
- </p>
568
+ {!item.rawNode.matchedExcerpt && (
569
+ <p
570
+ className={classNames(
571
+ 'text-xs flex-shrink-0',
572
+ active ? 'text-[rgb(var(--ec-page-text))]' : 'text-[rgb(var(--ec-page-text-muted))]'
573
+ )}
574
+ >
575
+ {item.type}
576
+ </p>
577
+ )}
498
578
  {item.rawNode.summary && (
499
579
  <p
500
580
  className={classNames(
501
- 'text-sm truncate',
581
+ 'text-xs truncate',
502
582
  active ? 'text-[rgb(var(--ec-page-text-muted))]' : 'text-[rgb(var(--ec-icon-color))]'
503
583
  )}
504
584
  >
505
- {item.rawNode.summary}
585
+ {!item.rawNode.matchedExcerpt && '• '}
586
+ {item.rawNode.matchedExcerpt ? (
587
+ <span
588
+ className={searchResultHighlightClassName}
589
+ dangerouslySetInnerHTML={{ __html: item.rawNode.matchedExcerpt }}
590
+ />
591
+ ) : (
592
+ item.rawNode.summary
593
+ )}
506
594
  </p>
507
595
  )}
508
596
  </div>
509
597
  </div>
510
598
  <div className="flex items-center">
511
- <button
512
- onClick={(e) => handleToggleFavorite(e, item)}
513
- onMouseDown={(e) => {
514
- e.preventDefault();
515
- e.stopPropagation();
516
- }}
517
- className={classNames(
518
- 'p-1 rounded-md transition-colors mr-2',
519
- isFavorite
520
- ? 'text-amber-400 hover:text-amber-500'
521
- : 'text-[rgb(var(--ec-icon-color))] opacity-0 group-hover:opacity-100 hover:text-amber-400'
522
- )}
523
- >
524
- {isFavorite ? <StarIconSolid className="h-5 w-5" /> : <StarIcon className="h-5 w-5" />}
525
- </button>
599
+ {!!item.key && (
600
+ <button
601
+ onClick={(e) => handleToggleFavorite(e, item)}
602
+ onMouseDown={(e) => {
603
+ e.preventDefault();
604
+ e.stopPropagation();
605
+ }}
606
+ className={classNames(
607
+ 'p-1 rounded-md transition-colors mr-2',
608
+ isFavorite
609
+ ? 'text-amber-400 hover:text-amber-500'
610
+ : 'text-[rgb(var(--ec-icon-color))] opacity-0 group-hover:opacity-100 hover:text-amber-400'
611
+ )}
612
+ >
613
+ {isFavorite ? <StarIconSolid className="h-5 w-5" /> : <StarIcon className="h-5 w-5" />}
614
+ </button>
615
+ )}
526
616
  {active && (
527
617
  <ArrowRightIcon className="h-5 w-5 text-[rgb(var(--ec-icon-color))]" aria-hidden="true" />
528
618
  )}
@@ -538,14 +628,22 @@ export default function SearchModal() {
538
628
 
539
629
  {!isLoadingSearchIndex && !searchIndexLoadError && query !== '' && filteredItems.length === 0 && (
540
630
  <div className="py-14 px-6 text-center text-sm sm:px-14">
541
- <ExclamationCircleIcon
542
- type="outline"
543
- name="exclamation-circle"
544
- className="mx-auto h-6 w-6 text-[rgb(var(--ec-icon-color))]"
545
- />
546
- <p className="mt-4 font-semibold text-[rgb(var(--ec-page-text))]">No results found</p>
631
+ {isSearchingIndexed ? (
632
+ <MagnifyingGlassIcon className="mx-auto h-6 w-6 text-[rgb(var(--ec-icon-color))] animate-pulse" />
633
+ ) : (
634
+ <ExclamationCircleIcon
635
+ type="outline"
636
+ name="exclamation-circle"
637
+ className="mx-auto h-6 w-6 text-[rgb(var(--ec-icon-color))]"
638
+ />
639
+ )}
640
+ <p className="mt-4 font-semibold text-[rgb(var(--ec-page-text))]">
641
+ {isSearchingIndexed ? 'Searching…' : 'No results found'}
642
+ </p>
547
643
  <p className="mt-2 text-[rgb(var(--ec-page-text-muted))]">
548
- No components found for this search term. Please try again.
644
+ {isSearchingIndexed
645
+ ? 'Searching the indexed catalog.'
646
+ : 'No components found for this search term. Please try again.'}
549
647
  </p>
550
648
  </div>
551
649
  )}
@@ -555,7 +653,9 @@ export default function SearchModal() {
555
653
  <MagnifyingGlassIcon className="mx-auto h-6 w-6 text-[rgb(var(--ec-icon-color))]" />
556
654
  <p className="mt-4 font-semibold text-[rgb(var(--ec-page-text))]">Search for anything</p>
557
655
  <p className="mt-2 text-[rgb(var(--ec-page-text-muted))]">
558
- Search for domains, services, events, commands, queries, data stores, data products, flows and more.
656
+ {isIndexedSearch
657
+ ? 'Search indexed catalog content, custom docs, resources and more.'
658
+ : 'Search for domains, services, events, commands, queries, data stores, data products, flows and more.'}
559
659
  </p>
560
660
  </div>
561
661
  )}