@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.
- package/dist/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/log-build.cjs +1 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/{chunk-6GGS5ZKX.js → chunk-BZAT6KWG.js} +1 -1
- package/dist/{chunk-RD5TL6VE.js → chunk-GU5QRUIL.js} +1 -1
- package/dist/{chunk-FNV7TXII.js → chunk-H6NJQGDB.js} +1 -1
- package/dist/{chunk-45OUURWB.js → chunk-ITXVGX5B.js} +1 -1
- package/dist/{chunk-SQWXIH42.js → chunk-MEP55EFY.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +1 -1
- package/dist/eventcatalog.js +5 -5
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +4 -3
- package/eventcatalog/src/pages/api/search-index.json.ts +2 -1
- package/eventcatalog/src/stores/sidebar-store/search-index.ts +17 -0
- package/eventcatalog/src/stores/sidebar-store/state.ts +12 -4
- package/package.json +3 -3
- package/eventcatalog/src/components/SideNav/NestedSideBar/SearchBar.tsx +0 -349
|
@@ -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.
|
|
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-
|
|
4
|
-
import "../chunk-
|
|
3
|
+
} from "../chunk-MEP55EFY.js";
|
|
4
|
+
import "../chunk-H6NJQGDB.js";
|
|
5
5
|
import "../chunk-K2XIENVT.js";
|
|
6
|
-
import "../chunk-
|
|
6
|
+
import "../chunk-BZAT6KWG.js";
|
|
7
7
|
import "../chunk-6QENHZZP.js";
|
|
8
8
|
export {
|
|
9
9
|
log_build_default as default
|
package/dist/constants.cjs
CHANGED
package/dist/constants.js
CHANGED
package/dist/eventcatalog.cjs
CHANGED
|
@@ -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.
|
|
147
|
+
var version = "4.7.12";
|
|
148
148
|
|
|
149
149
|
// src/constants.ts
|
|
150
150
|
var VERSION = version;
|
package/dist/eventcatalog.js
CHANGED
|
@@ -4,8 +4,8 @@ import {
|
|
|
4
4
|
import "./chunk-CA4U2JP7.js";
|
|
5
5
|
import {
|
|
6
6
|
log_build_default
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
import "./chunk-
|
|
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-
|
|
34
|
+
} from "./chunk-GU5QRUIL.js";
|
|
35
35
|
import {
|
|
36
36
|
logger
|
|
37
|
-
} from "./chunk-
|
|
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-
|
|
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.
|
|
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-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
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
|
package/dist/utils/cli-logger.js
CHANGED
|
@@ -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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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 =
|
|
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:
|
|
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
|
|
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({
|
|
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.
|
|
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/
|
|
124
|
-
"@eventcatalog/
|
|
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
|
-
}
|