@eventcatalog/core 4.7.11 → 4.7.12

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.
@@ -36,7 +36,7 @@ module.exports = __toCommonJS(analytics_exports);
36
36
  var import_os = __toESM(require("os"), 1);
37
37
 
38
38
  // package.json
39
- var version = "4.7.11";
39
+ var version = "4.7.12";
40
40
 
41
41
  // src/constants.ts
42
42
  var VERSION = version;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  raiseEvent
3
- } from "../chunk-FNV7TXII.js";
4
- import "../chunk-6GGS5ZKX.js";
3
+ } from "../chunk-H6NJQGDB.js";
4
+ import "../chunk-BZAT6KWG.js";
5
5
  export {
6
6
  raiseEvent
7
7
  };
@@ -140,7 +140,7 @@ var verifyRequiredFieldsAreInCatalogConfigFile = async (projectDirectory) => {
140
140
  var import_os = __toESM(require("os"), 1);
141
141
 
142
142
  // package.json
143
- var version = "4.7.11";
143
+ var version = "4.7.12";
144
144
 
145
145
  // src/constants.ts
146
146
  var VERSION = version;
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  log_build_default
3
- } from "../chunk-SQWXIH42.js";
4
- import "../chunk-FNV7TXII.js";
3
+ } from "../chunk-MEP55EFY.js";
4
+ import "../chunk-H6NJQGDB.js";
5
5
  import "../chunk-K2XIENVT.js";
6
- import "../chunk-6GGS5ZKX.js";
6
+ import "../chunk-BZAT6KWG.js";
7
7
  import "../chunk-6QENHZZP.js";
8
8
  export {
9
9
  log_build_default as default
@@ -1,5 +1,5 @@
1
1
  // package.json
2
- var version = "4.7.11";
2
+ var version = "4.7.12";
3
3
 
4
4
  // src/constants.ts
5
5
  var VERSION = version;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  logger
3
- } from "./chunk-45OUURWB.js";
3
+ } from "./chunk-ITXVGX5B.js";
4
4
  import {
5
5
  cleanup,
6
6
  getEventCatalogConfigFile
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-6GGS5ZKX.js";
3
+ } from "./chunk-BZAT6KWG.js";
4
4
 
5
5
  // src/analytics/analytics.js
6
6
  import os from "os";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-6GGS5ZKX.js";
3
+ } from "./chunk-BZAT6KWG.js";
4
4
 
5
5
  // src/utils/cli-logger.ts
6
6
  import pc from "picocolors";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  raiseEvent
3
- } from "./chunk-FNV7TXII.js";
3
+ } from "./chunk-H6NJQGDB.js";
4
4
  import {
5
5
  countResources,
6
6
  serializeCounts
@@ -25,7 +25,7 @@ __export(constants_exports, {
25
25
  module.exports = __toCommonJS(constants_exports);
26
26
 
27
27
  // package.json
28
- var version = "4.7.11";
28
+ var version = "4.7.12";
29
29
 
30
30
  // src/constants.ts
31
31
  var VERSION = version;
package/dist/constants.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-6GGS5ZKX.js";
3
+ } from "./chunk-BZAT6KWG.js";
4
4
  export {
5
5
  VERSION
6
6
  };
@@ -144,7 +144,7 @@ var verifyRequiredFieldsAreInCatalogConfigFile = async (projectDirectory) => {
144
144
  var import_picocolors = __toESM(require("picocolors"), 1);
145
145
 
146
146
  // package.json
147
- var version = "4.7.11";
147
+ var version = "4.7.12";
148
148
 
149
149
  // src/constants.ts
150
150
  var VERSION = version;
@@ -4,8 +4,8 @@ import {
4
4
  import "./chunk-CA4U2JP7.js";
5
5
  import {
6
6
  log_build_default
7
- } from "./chunk-SQWXIH42.js";
8
- import "./chunk-FNV7TXII.js";
7
+ } from "./chunk-MEP55EFY.js";
8
+ import "./chunk-H6NJQGDB.js";
9
9
  import "./chunk-K2XIENVT.js";
10
10
  import {
11
11
  FederationConflictError,
@@ -31,10 +31,10 @@ import {
31
31
  } from "./chunk-B7HCX5HM.js";
32
32
  import {
33
33
  generate
34
- } from "./chunk-RD5TL6VE.js";
34
+ } from "./chunk-GU5QRUIL.js";
35
35
  import {
36
36
  logger
37
- } from "./chunk-45OUURWB.js";
37
+ } from "./chunk-ITXVGX5B.js";
38
38
  import {
39
39
  resolve_catalog_dependencies_default
40
40
  } from "./chunk-LHR4G2UO.js";
@@ -54,7 +54,7 @@ import {
54
54
  import "./chunk-W5JQON7Z.js";
55
55
  import {
56
56
  VERSION
57
- } from "./chunk-6GGS5ZKX.js";
57
+ } from "./chunk-BZAT6KWG.js";
58
58
  import {
59
59
  linkCoreNodeModules,
60
60
  resolveInstalledCoreNodeModules
package/dist/generate.cjs CHANGED
@@ -108,7 +108,7 @@ var getEventCatalogConfigFile = async (projectDirectory) => {
108
108
  var import_picocolors = __toESM(require("picocolors"), 1);
109
109
 
110
110
  // package.json
111
- var version = "4.7.11";
111
+ var version = "4.7.12";
112
112
 
113
113
  // src/constants.ts
114
114
  var VERSION = version;
package/dist/generate.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  generate
3
- } from "./chunk-RD5TL6VE.js";
4
- import "./chunk-45OUURWB.js";
5
- import "./chunk-6GGS5ZKX.js";
3
+ } from "./chunk-GU5QRUIL.js";
4
+ import "./chunk-ITXVGX5B.js";
5
+ import "./chunk-BZAT6KWG.js";
6
6
  import "./chunk-6QENHZZP.js";
7
7
  export {
8
8
  generate
@@ -36,7 +36,7 @@ module.exports = __toCommonJS(cli_logger_exports);
36
36
  var import_picocolors = __toESM(require("picocolors"), 1);
37
37
 
38
38
  // package.json
39
- var version = "4.7.11";
39
+ var version = "4.7.12";
40
40
 
41
41
  // src/constants.ts
42
42
  var VERSION = version;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  logger
3
- } from "../chunk-45OUURWB.js";
4
- import "../chunk-6GGS5ZKX.js";
3
+ } from "../chunk-ITXVGX5B.js";
4
+ import "../chunk-BZAT6KWG.js";
5
5
  export {
6
6
  logger
7
7
  };
@@ -439,9 +439,10 @@ export default function NestedSideBar() {
439
439
  if (!data || roots.length === 0) {
440
440
  return (
441
441
  <aside className="flex h-full min-h-0 w-full flex-1 flex-col bg-[rgb(var(--ec-rail-bg))] font-sans">
442
- {/* Search skeleton */}
443
- <div className="px-4 py-3 border-b border-[rgb(var(--ec-content-border))] bg-[rgb(var(--ec-rail-bg))]">
444
- <div className="h-10 bg-[rgb(var(--ec-content-hover))] rounded-xl animate-pulse" />
442
+ <div className="flex h-[60px] items-center px-6 bg-[rgb(var(--ec-rail-bg)/0.98)] backdrop-blur-sm border-b border-[rgb(var(--ec-content-border))] sticky top-0 z-10">
443
+ <span className="text-[0.65rem] font-semibold uppercase tracking-[0.18em] text-[rgb(var(--ec-sidebar-text)/0.5)] truncate">
444
+ All resources
445
+ </span>
445
446
  </div>
446
447
  {/* Content skeleton */}
447
448
  <div className="p-4 space-y-3">
@@ -1,5 +1,6 @@
1
1
  import type { APIRoute } from 'astro';
2
2
  import { getNestedSideBarData } from '@stores/sidebar-store/state';
3
+ import { getSearchIndexNodeEntries } from '@stores/sidebar-store/search-index';
3
4
 
4
5
  const isDev = import.meta.env.DEV;
5
6
 
@@ -18,7 +19,7 @@ const SKIPPED_PREFIXES = ['list:'];
18
19
  export const GET: APIRoute = async () => {
19
20
  const sidebarData = await getNestedSideBarData();
20
21
 
21
- const items = Object.entries(sidebarData.nodes).reduce<SearchIndexItemCompact[]>((acc, [key, node]) => {
22
+ const items = getSearchIndexNodeEntries(sidebarData.nodes).reduce<SearchIndexItemCompact[]>((acc, [key, node]) => {
22
23
  if (typeof node === 'string') return acc;
23
24
  if (SKIPPED_PREFIXES.some((prefix) => key.startsWith(prefix))) return acc;
24
25
  if (!node.title) return acc;
@@ -0,0 +1,17 @@
1
+ import type { NavigationData, NavNode } from './builders/shared';
2
+
3
+ const MESSAGE_NODE_PREFIXES = ['event:', 'command:', 'query:'];
4
+
5
+ export const getSearchIndexNodeEntries = (nodes: NavigationData['nodes']): [string, NavNode | string][] => {
6
+ const currentMessageTargets = new Set(
7
+ Object.entries(nodes).flatMap(([key, node]) => {
8
+ const isMessageAlias = MESSAGE_NODE_PREFIXES.some((prefix) => key.startsWith(prefix));
9
+ return typeof node === 'string' && isMessageAlias && node.startsWith(`${key}:`) ? [node] : [];
10
+ })
11
+ );
12
+
13
+ return Object.entries(nodes).filter(([key]) => {
14
+ const isMessageNode = MESSAGE_NODE_PREFIXES.some((prefix) => key.startsWith(prefix));
15
+ return !isMessageNode || currentMessageTargets.has(key);
16
+ });
17
+ };
@@ -281,7 +281,7 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
281
281
  systems,
282
282
  agents,
283
283
  services,
284
- { events, commands, queries },
284
+ { events: allEvents, commands: allCommands, queries: allQueries },
285
285
  containers,
286
286
  flows,
287
287
  users,
@@ -301,7 +301,7 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
301
301
  getSystems({ getAllVersions: false }),
302
302
  getAgents({ getAllVersions: false }),
303
303
  getServices({ getAllVersions: false }),
304
- getMessages({ getAllVersions: false }),
304
+ getMessages({ getAllVersions: true }),
305
305
  getContainers({ getAllVersions: false }),
306
306
  getFlows({ getAllVersions: false }),
307
307
  getUsers(),
@@ -322,7 +322,10 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
322
322
  const allSubDomainIds = new Set(domains.flatMap((d) => (d.data.domains || []).map((sd: any) => sd.data.id)));
323
323
  const rootDomains = domains.filter((d) => !allSubDomainIds.has(d.data.id));
324
324
 
325
- const messages = [...events, ...commands, ...queries];
325
+ const events = allEvents.filter((event) => event.data.version === event.data.latestVersion);
326
+ const commands = allCommands.filter((command) => command.data.version === command.data.latestVersion);
327
+ const queries = allQueries.filter((query) => query.data.version === query.data.latestVersion);
328
+ const messages = [...allEvents, ...allCommands, ...allQueries];
326
329
 
327
330
  const context = {
328
331
  agents,
@@ -595,7 +598,12 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
595
598
  }
596
599
  }
597
600
 
598
- const flowRefsByMessage = buildFlowReferencesByMessage({ flows, events, commands, queries });
601
+ const flowRefsByMessage = buildFlowReferencesByMessage({
602
+ flows,
603
+ events: allEvents,
604
+ commands: allCommands,
605
+ queries: allQueries,
606
+ });
599
607
 
600
608
  const messageNodes = messagesWithOwners.reduce(
601
609
  (acc, { message, owners }) => {
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "license": "SEE LICENSE IN LICENSE",
9
9
  "type": "module",
10
- "version": "4.7.11",
10
+ "version": "4.7.12",
11
11
  "publishConfig": {
12
12
  "access": "public"
13
13
  },
@@ -120,8 +120,8 @@
120
120
  "uuid": "^11.1.1",
121
121
  "zod": "^4.3.6",
122
122
  "@eventcatalog/linter": "1.1.14",
123
- "@eventcatalog/visualiser": "^4.1.3",
124
- "@eventcatalog/sdk": "2.27.4"
123
+ "@eventcatalog/sdk": "2.27.4",
124
+ "@eventcatalog/visualiser": "^4.1.3"
125
125
  },
126
126
  "devDependencies": {
127
127
  "@astrojs/check": "^0.9.10",
@@ -1,349 +0,0 @@
1
- 'use client';
2
-
3
- import { useState, useCallback, useMemo } from 'react';
4
- import {
5
- Search,
6
- X,
7
- SlidersHorizontal,
8
- ChevronRight,
9
- Boxes,
10
- Server,
11
- Zap,
12
- MessageSquare,
13
- Search as SearchIcon,
14
- Database,
15
- Waypoints,
16
- SquareMousePointer,
17
- ListOrdered,
18
- ArrowLeftRight,
19
- Package,
20
- Box,
21
- Bot,
22
- } from 'lucide-react';
23
- import type { NavNode } from '@stores/sidebar-store/state';
24
- import { getBadgeClasses } from './utils';
25
-
26
- const cn = (...classes: (string | false | undefined)[]) => classes.filter(Boolean).join(' ');
27
-
28
- type SearchResult = {
29
- nodeKey: string;
30
- node: NavNode;
31
- matchType: 'name' | 'id';
32
- };
33
-
34
- type Props = {
35
- nodes: Record<string, NavNode | string>;
36
- onSelectResult: (nodeKey: string, node: NavNode) => void;
37
- onSearchChange?: (isSearching: boolean) => void;
38
- };
39
-
40
- export default function SearchBar({ nodes, onSelectResult, onSearchChange }: Props) {
41
- const [searchQuery, setSearchQuery] = useState('');
42
- const [searchFilters, setSearchFilters] = useState<Set<string>>(new Set());
43
- const [showFilterDropdown, setShowFilterDropdown] = useState(false);
44
-
45
- // Pre-process searchable nodes to avoid iterating object on every render
46
- // Filter out unversioned keys (e.g., "domain:OrderService") to avoid duplicates with versioned keys (e.g., "domain:OrderService:1.0.0")
47
- const searchableNodes = useMemo((): Array<[string, NavNode]> => {
48
- const result: Array<[string, NavNode]> = [];
49
- for (const [key, node] of Object.entries(nodes)) {
50
- // Skip string references (unversioned aliases that point to versioned keys)
51
- if (typeof node === 'string') continue;
52
- if (node.type === 'group') continue;
53
- // Only include versioned keys (those with 3+ parts like "type:id:version")
54
- // Unversioned keys (2 parts like "type:id") are aliases to latest version and would cause duplicates
55
- const keyParts = key.split(':');
56
- if (keyParts.length >= 3) {
57
- result.push([key, node]);
58
- }
59
- }
60
- return result;
61
- }, [nodes]);
62
-
63
- // Get available badges from nodes
64
- const availableBadges = useMemo(() => {
65
- const badges = new Set<string>();
66
-
67
- for (const [_, node] of searchableNodes) {
68
- if (node.badge) {
69
- badges.add(node.badge);
70
- }
71
- }
72
- return badges;
73
- }, [searchableNodes]);
74
-
75
- const handleSearchChange = (value: string) => {
76
- setSearchQuery(value);
77
- onSearchChange?.(value.trim().length > 0);
78
- };
79
-
80
- const clearSearch = () => {
81
- setSearchQuery('');
82
- onSearchChange?.(false);
83
- };
84
-
85
- const filterTypes = [
86
- { key: 'channel', label: 'Channels', badge: 'Channel', icon: ArrowLeftRight },
87
- { key: 'agent', label: 'Agents', badge: 'Agent', icon: Bot },
88
- { key: 'command', label: 'Commands', badge: 'Command', icon: MessageSquare },
89
- { key: 'container', label: 'Data Stores', badge: 'Container', icon: Database },
90
- { key: 'data-product', label: 'Data Products', badge: 'Data Product', icon: Package },
91
- { key: 'design', label: 'Designs', badge: 'Design', icon: SquareMousePointer },
92
- { key: 'domain', label: 'Domains', badge: 'Domain', icon: Boxes },
93
- { key: 'entity', label: 'Entities', badge: 'Entity', icon: Box },
94
- { key: 'event', label: 'Events', badge: 'Event', icon: Zap },
95
- { key: 'flow', label: 'Flows', badge: 'Flow', icon: Waypoints },
96
- { key: 'query', label: 'Queries', badge: 'Query', icon: SearchIcon },
97
- { key: 'service', label: 'Services', badge: 'Service', icon: Server },
98
- ].filter((filter) => availableBadges.has(filter.badge));
99
-
100
- const toggleSearchFilter = (filterKey: string) => {
101
- setSearchFilters((prev) => {
102
- const next = new Set(prev);
103
- if (next.has(filterKey)) {
104
- next.delete(filterKey);
105
- } else {
106
- next.add(filterKey);
107
- }
108
- return next;
109
- });
110
- };
111
-
112
- const searchResults = useCallback((): SearchResult[] => {
113
- if (!searchQuery.trim()) return [];
114
-
115
- const query = searchQuery.toLowerCase();
116
- const results: SearchResult[] = [];
117
-
118
- const badgeToFilterKey: Record<string, string> = {
119
- Agent: 'agent',
120
- Domain: 'domain',
121
- Service: 'service',
122
- Event: 'event',
123
- Command: 'command',
124
- Query: 'query',
125
- Container: 'container',
126
- 'Data Product': 'data-product',
127
- Flow: 'flow',
128
- Design: 'design',
129
- Channel: 'channel',
130
- Entity: 'entity',
131
- };
132
-
133
- // Use the memoized array instead of Object.entries(nodes)
134
- for (const [key, node] of searchableNodes) {
135
- if (searchFilters.size > 0) {
136
- const filterKey = node.badge ? badgeToFilterKey[node.badge] : null;
137
- if (!filterKey || !searchFilters.has(filterKey)) continue;
138
- }
139
-
140
- if (node.title.toLowerCase().includes(query)) {
141
- results.push({ nodeKey: key, node, matchType: 'name' });
142
- continue;
143
- }
144
-
145
- const keyParts = key.split(':');
146
- if (keyParts.length >= 3) {
147
- const id = keyParts[1].toLowerCase();
148
- if (id.includes(query)) {
149
- results.push({ nodeKey: key, node, matchType: 'id' });
150
- }
151
- }
152
- }
153
-
154
- return results
155
- .sort((a, b) => {
156
- const aExact = a.node.title.toLowerCase() === query;
157
- const bExact = b.node.title.toLowerCase() === query;
158
- if (aExact && !bExact) return -1;
159
- if (!aExact && bExact) return 1;
160
- return a.node.title.localeCompare(b.node.title);
161
- })
162
- .slice(0, 20);
163
- }, [searchQuery, searchableNodes, searchFilters]);
164
-
165
- const handleSelectResult = (nodeKey: string, node: NavNode) => {
166
- onSelectResult(nodeKey, node);
167
- clearSearch();
168
- };
169
-
170
- const results = searchResults();
171
- const showSearchResults = searchQuery.trim().length > 0;
172
-
173
- // Group results by type (badge), sorted alphabetically
174
- const groupedResults = useMemo(() => {
175
- const groups: Record<string, SearchResult[]> = {};
176
-
177
- for (const result of results) {
178
- const badge = result.node.badge || 'Other';
179
- if (!groups[badge]) {
180
- groups[badge] = [];
181
- }
182
- groups[badge].push(result);
183
- }
184
-
185
- return Object.entries(groups)
186
- .sort(([a], [b]) => a.localeCompare(b))
187
- .map(([type, typeResults]) => ({ type, results: typeResults }));
188
- }, [results]);
189
-
190
- return (
191
- <>
192
- {/* Search Input */}
193
- <div className="px-4 py-3 bg-[rgb(var(--ec-content-bg))] border-b border-[rgb(var(--ec-content-border))]">
194
- <div className="flex gap-2">
195
- <div className="relative flex-1">
196
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[rgb(var(--ec-input-placeholder))]" />
197
- <input
198
- type="text"
199
- placeholder="Search resources..."
200
- value={searchQuery}
201
- onChange={(e) => handleSearchChange(e.target.value)}
202
- className="w-full pl-9! pr-8! py-2.5! text-sm! bg-[rgb(var(--ec-input-bg))]! border! border-[rgb(var(--ec-input-border))]! rounded-xl! focus:outline-hidden! focus:ring-2! focus:ring-[rgb(var(--ec-accent))]! focus:border-transparent! text-[rgb(var(--ec-input-text))]! placeholder:text-[rgb(var(--ec-input-placeholder))]!"
203
- />
204
- {searchQuery && (
205
- <button
206
- onClick={clearSearch}
207
- className="absolute right-3 top-1/2 -translate-y-1/2 text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-icon-hover))]"
208
- aria-label="Clear search"
209
- >
210
- <X className="w-4 h-4" aria-hidden="true" />
211
- </button>
212
- )}
213
- </div>
214
- {/* Filter Button */}
215
- <div className="relative">
216
- <button
217
- onClick={() => setShowFilterDropdown(!showFilterDropdown)}
218
- className={cn(
219
- 'flex items-center justify-center w-10 h-10 rounded-xl border transition-colors',
220
- searchFilters.size > 0
221
- ? 'bg-[rgb(var(--ec-accent)/0.1)] border-[rgb(var(--ec-accent)/0.3)] text-[rgb(var(--ec-accent))]'
222
- : 'bg-[rgb(var(--ec-input-bg))] border-[rgb(var(--ec-input-border))] text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-icon-hover))] hover:bg-[rgb(var(--ec-content-hover))]'
223
- )}
224
- aria-label="Filter search results"
225
- aria-expanded={showFilterDropdown}
226
- >
227
- <SlidersHorizontal className="w-4 h-4" aria-hidden="true" />
228
- {searchFilters.size > 0 && (
229
- <span className="absolute -top-1 -right-1 w-4 h-4 bg-[rgb(var(--ec-accent))] text-white text-[10px] font-bold rounded-full flex items-center justify-center">
230
- {searchFilters.size}
231
- </span>
232
- )}
233
- </button>
234
-
235
- {/* Filter Dropdown */}
236
- {showFilterDropdown && (
237
- <>
238
- <div className="fixed inset-0 z-20" onClick={() => setShowFilterDropdown(false)} />
239
- <div className="absolute right-0 top-full mt-1 w-48 bg-[rgb(var(--ec-dropdown-bg))] rounded-lg shadow-lg border border-[rgb(var(--ec-dropdown-border))] z-30">
240
- <div className="p-2">
241
- <div className="text-[10px] font-medium text-[rgb(var(--ec-content-text-muted))] uppercase tracking-wide px-2 py-1">
242
- Filter by type
243
- </div>
244
- <div className="flex flex-col gap-0.5 mt-1">
245
- {filterTypes.map((filter) => {
246
- const isActive = searchFilters.has(filter.key);
247
- const Icon = filter.icon;
248
- return (
249
- <button
250
- key={filter.key}
251
- onClick={() => toggleSearchFilter(filter.key)}
252
- className={cn(
253
- 'flex items-center justify-between px-2 py-1.5 rounded text-sm transition-colors',
254
- isActive
255
- ? 'bg-[rgb(var(--ec-accent)/0.1)] text-[rgb(var(--ec-accent))]'
256
- : 'text-[rgb(var(--ec-dropdown-text))] hover:bg-[rgb(var(--ec-dropdown-hover))]'
257
- )}
258
- >
259
- <span className="flex items-center gap-2">
260
- <Icon className="w-3 h-3" />
261
- {filter.label}
262
- </span>
263
- {isActive && <span className="text-[rgb(var(--ec-accent))]">✓</span>}
264
- </button>
265
- );
266
- })}
267
- </div>
268
- {searchFilters.size > 0 && (
269
- <>
270
- <div className="border-t border-[rgb(var(--ec-content-border))] my-2" />
271
- <button
272
- onClick={() => {
273
- setSearchFilters(new Set());
274
- setShowFilterDropdown(false);
275
- }}
276
- className="w-full px-2 py-1.5 text-sm text-[rgb(var(--ec-content-text-muted))] hover:text-[rgb(var(--ec-content-text))] hover:bg-[rgb(var(--ec-dropdown-hover))] rounded text-left"
277
- >
278
- Reset filters
279
- </button>
280
- </>
281
- )}
282
- </div>
283
- </div>
284
- </>
285
- )}
286
- </div>
287
- </div>
288
- </div>
289
-
290
- {/* Search Results */}
291
- {showSearchResults && (
292
- <div className="flex-1 overflow-y-auto bg-[rgb(var(--ec-content-bg))] border-b border-[rgb(var(--ec-content-border))]">
293
- <div className="px-3 py-2">
294
- <div className="text-[10px] font-medium text-[rgb(var(--ec-content-text-muted))] uppercase tracking-wide mb-2">
295
- {results.length > 0 ? `${results.length} result${results.length > 1 ? 's' : ''}` : 'No results'}
296
- </div>
297
- {groupedResults.length > 0 && (
298
- <div className="flex flex-col gap-3">
299
- {groupedResults.map(({ type, results: typeResults }) => (
300
- <div key={type}>
301
- <div className="flex items-center gap-2 mb-1">
302
- <span
303
- className={cn(
304
- 'px-1.5 py-0.5 text-[8px] font-semibold uppercase tracking-wide rounded',
305
- getBadgeClasses(type)
306
- )}
307
- >
308
- {type}
309
- </span>
310
- <span className="text-[10px] text-[rgb(var(--ec-content-text-muted))]">({typeResults.length})</span>
311
- </div>
312
- <div className="flex flex-col gap-0.5">
313
- {typeResults.map(({ nodeKey, node, matchType }) => (
314
- <button
315
- key={nodeKey}
316
- onClick={() => handleSelectResult(nodeKey, node)}
317
- className="group flex items-center justify-between w-full px-3 py-2 rounded-lg cursor-pointer text-left transition-colors hover:bg-[rgb(var(--ec-content-hover))]"
318
- >
319
- <div className="flex flex-col min-w-0 flex-1">
320
- <span className="text-sm text-[rgb(var(--ec-content-text))] truncate">{node.title}</span>
321
- {matchType === 'id' && (
322
- <span className="text-xs text-[rgb(var(--ec-content-text-muted))] truncate">
323
- ID: {nodeKey.split(':')[2]}
324
- </span>
325
- )}
326
- </div>
327
- <div className="flex items-center gap-2 flex-shrink-0">
328
- {node.pages && node.pages.length > 0 && (
329
- <ChevronRight className="w-4 h-4 text-[rgb(var(--ec-icon-color))] group-hover:text-[rgb(var(--ec-accent))]" />
330
- )}
331
- </div>
332
- </button>
333
- ))}
334
- </div>
335
- </div>
336
- ))}
337
- </div>
338
- )}
339
- {results.length === 0 && searchQuery.trim() && (
340
- <div className="text-sm text-[rgb(var(--ec-content-text-muted))] py-4 text-center">
341
- No resources found for "{searchQuery}"
342
- </div>
343
- )}
344
- </div>
345
- </div>
346
- )}
347
- </>
348
- );
349
- }