@eventcatalog/core 2.65.0 → 3.0.0-beta.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 (130) hide show
  1. package/README.md +1 -26
  2. package/dist/analytics/analytics.cjs +1 -1
  3. package/dist/analytics/analytics.js +2 -2
  4. package/dist/analytics/log-build.cjs +1 -1
  5. package/dist/analytics/log-build.js +3 -3
  6. package/dist/{chunk-NK6OYMRD.js → chunk-JB4YT5JY.js} +1 -1
  7. package/dist/{chunk-BMDTX5IN.js → chunk-TQ4HZREX.js} +1 -1
  8. package/dist/{chunk-IJRFYF4B.js → chunk-X4W4YC3U.js} +1 -1
  9. package/dist/constants.cjs +1 -1
  10. package/dist/constants.js +1 -1
  11. package/dist/eventcatalog.cjs +1 -21
  12. package/dist/eventcatalog.config.d.cts +10 -0
  13. package/dist/eventcatalog.config.d.ts +10 -0
  14. package/dist/eventcatalog.js +3 -20
  15. package/eventcatalog/src/components/CopyAsMarkdown.tsx +19 -1
  16. package/eventcatalog/src/components/FavoriteButton.tsx +54 -0
  17. package/eventcatalog/src/components/Grids/DomainGrid.tsx +386 -362
  18. package/eventcatalog/src/components/Grids/MessageGrid.tsx +166 -518
  19. package/eventcatalog/src/components/Header.astro +48 -23
  20. package/eventcatalog/src/components/Lists/VersionList.astro +2 -2
  21. package/eventcatalog/src/components/MDX/Design/Design.astro +4 -1
  22. package/eventcatalog/src/components/MDX/Flow/Flow.astro +2 -1
  23. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +3 -3
  24. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +8 -2
  25. package/eventcatalog/src/components/SchemaExplorer/SchemaPageViewer.tsx +37 -0
  26. package/eventcatalog/src/components/Search/Search.astro +48 -28
  27. package/eventcatalog/src/components/Search/SearchModal.tsx +393 -702
  28. package/eventcatalog/src/components/SideNav/NestedSideBar/SearchBar.tsx +298 -0
  29. package/eventcatalog/src/components/SideNav/NestedSideBar/builders/container.ts +66 -0
  30. package/eventcatalog/src/components/SideNav/NestedSideBar/builders/domain.ts +101 -0
  31. package/eventcatalog/src/components/SideNav/NestedSideBar/builders/flow.ts +29 -0
  32. package/eventcatalog/src/components/SideNav/NestedSideBar/builders/message.ts +84 -0
  33. package/eventcatalog/src/components/SideNav/NestedSideBar/builders/service.ts +147 -0
  34. package/eventcatalog/src/components/SideNav/NestedSideBar/builders/shared.ts +146 -0
  35. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +1073 -0
  36. package/eventcatalog/src/components/SideNav/NestedSideBar/sidebar-builder.ts +365 -0
  37. package/eventcatalog/src/components/SideNav/NestedSideBar/storage.ts +90 -0
  38. package/eventcatalog/src/components/SideNav/SideNav.astro +18 -28
  39. package/eventcatalog/src/content.config.ts +2 -0
  40. package/eventcatalog/src/enterprise/custom-documentation/pages/docs/custom/index.astro +10 -4
  41. package/eventcatalog/src/enterprise/eventcatalog-chat/pages/chat/index.astro +3 -3
  42. package/eventcatalog/src/layouts/DirectoryLayout.astro +2 -2
  43. package/eventcatalog/src/layouts/DiscoverLayout.astro +3 -3
  44. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +85 -63
  45. package/eventcatalog/src/layouts/VisualiserLayout.astro +3 -3
  46. package/eventcatalog/src/pages/_index.astro +530 -110
  47. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +64 -0
  48. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +29 -0
  49. package/eventcatalog/src/pages/directory/[type]/_index.data.ts +4 -4
  50. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/_index.data.ts +1 -4
  51. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/_index.data.ts +3 -3
  52. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/index.astro +1 -5
  53. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +362 -190
  54. package/eventcatalog/src/pages/docs/[type]/[id]/[version].md.ts +1 -1
  55. package/eventcatalog/src/pages/docs/[type]/[id]/index.astro +4 -4
  56. package/eventcatalog/src/pages/docs/[type]/[id]/language/_index.data.ts +1 -4
  57. package/eventcatalog/src/pages/docs/[type]/[id]/language/index.astro +3 -27
  58. package/eventcatalog/src/pages/docs/teams/[id]/_index.data.ts +2 -2
  59. package/eventcatalog/src/pages/docs/users/[id]/_index.data.ts +2 -2
  60. package/eventcatalog/src/pages/index.astro +14 -5
  61. package/eventcatalog/src/pages/nav-index.json.ts +30 -0
  62. package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/_index.data.ts +77 -0
  63. package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/index.astro +90 -0
  64. package/eventcatalog/src/pages/schemas/{index.astro → explorer/index.astro} +3 -3
  65. package/eventcatalog/src/pages/studio.astro +3 -3
  66. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/_index.data.ts +4 -3
  67. package/eventcatalog/src/pages/visualiser/[type]/[id]/index.astro +2 -2
  68. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/_index.data.ts +4 -3
  69. package/eventcatalog/src/stores/favorites-store.ts +83 -0
  70. package/eventcatalog/src/stores/sidebar-store.ts +8 -0
  71. package/eventcatalog/src/utils/collections/changelogs.ts +7 -4
  72. package/eventcatalog/src/utils/{channels.ts → collections/channels.ts} +81 -31
  73. package/eventcatalog/src/utils/collections/commands.ts +134 -0
  74. package/eventcatalog/src/utils/collections/containers.ts +44 -33
  75. package/eventcatalog/src/utils/collections/domains.ts +204 -62
  76. package/eventcatalog/src/utils/{entities.ts → collections/entities.ts} +44 -24
  77. package/eventcatalog/src/utils/collections/events.ts +136 -0
  78. package/eventcatalog/src/utils/collections/flows.ts +59 -25
  79. package/eventcatalog/src/utils/{messages.ts → collections/messages.ts} +13 -4
  80. package/eventcatalog/src/utils/{queries.ts → collections/queries.ts} +49 -28
  81. package/eventcatalog/src/utils/collections/services.ts +100 -68
  82. package/eventcatalog/src/utils/collections/teams.ts +94 -0
  83. package/eventcatalog/src/utils/collections/users.ts +122 -0
  84. package/eventcatalog/src/utils/collections/util.ts +57 -1
  85. package/eventcatalog/src/utils/feature.ts +3 -1
  86. package/eventcatalog/src/utils/{collections/file-diffs.ts → file-diffs.ts} +1 -1
  87. package/eventcatalog/src/utils/node-graphs/container-node-graph.ts +2 -0
  88. package/eventcatalog/src/utils/node-graphs/domain-entity-map.ts +16 -6
  89. package/eventcatalog/src/utils/node-graphs/domains-canvas.ts +14 -10
  90. package/eventcatalog/src/utils/node-graphs/domains-node-graph.ts +36 -64
  91. package/eventcatalog/src/utils/node-graphs/flows-node-graph.ts +23 -19
  92. package/eventcatalog/src/utils/node-graphs/message-node-graph.ts +36 -49
  93. package/eventcatalog/src/utils/node-graphs/services-node-graph.ts +22 -18
  94. package/eventcatalog/src/utils/page-loaders/page-data-loader.ts +4 -4
  95. package/eventcatalog/tailwind.config.mjs +14 -0
  96. package/eventcatalog/tsconfig.json +2 -1
  97. package/package.json +7 -4
  98. package/eventcatalog/public/logo_old.png +0 -0
  99. package/eventcatalog/src/components/DiscoverInsight.astro +0 -61
  100. package/eventcatalog/src/components/Grids/ServiceGrid.tsx +0 -534
  101. package/eventcatalog/src/components/Lists/CustomSideBarSectionList.astro +0 -55
  102. package/eventcatalog/src/components/Lists/ProtocolList.tsx +0 -74
  103. package/eventcatalog/src/components/Lists/RepositoryList.astro +0 -37
  104. package/eventcatalog/src/components/Lists/SpecificationsList.astro +0 -67
  105. package/eventcatalog/src/components/SideBars/ChannelSideBar.astro +0 -204
  106. package/eventcatalog/src/components/SideBars/ContainerSideBar.astro +0 -180
  107. package/eventcatalog/src/components/SideBars/DomainSideBar.astro +0 -273
  108. package/eventcatalog/src/components/SideBars/EntitySideBar.astro +0 -139
  109. package/eventcatalog/src/components/SideBars/FlowSideBar.astro +0 -128
  110. package/eventcatalog/src/components/SideBars/MessageSideBar.astro +0 -248
  111. package/eventcatalog/src/components/SideBars/ServiceSideBar.astro +0 -294
  112. package/eventcatalog/src/components/SideNav/ListViewSideBar/components/CollapsibleGroup.tsx +0 -46
  113. package/eventcatalog/src/components/SideNav/ListViewSideBar/components/MessageList.tsx +0 -78
  114. package/eventcatalog/src/components/SideNav/ListViewSideBar/components/SpecificationList.tsx +0 -83
  115. package/eventcatalog/src/components/SideNav/ListViewSideBar/index.tsx +0 -1250
  116. package/eventcatalog/src/components/SideNav/ListViewSideBar/types.ts +0 -91
  117. package/eventcatalog/src/components/SideNav/ListViewSideBar/utils.ts +0 -201
  118. package/eventcatalog/src/components/SideNav/TreeView/getTreeView.ts +0 -190
  119. package/eventcatalog/src/components/SideNav/TreeView/index.tsx +0 -94
  120. package/eventcatalog/src/components/TreeView/index.tsx +0 -328
  121. package/eventcatalog/src/components/TreeView/styles.module.css +0 -264
  122. package/eventcatalog/src/components/TreeView/useSlots.ts +0 -95
  123. package/eventcatalog/src/pages/architecture/[type]/index.astro +0 -14
  124. package/eventcatalog/src/pages/architecture/architecture.astro +0 -101
  125. package/eventcatalog/src/pages/architecture/docs/[type]/index.astro +0 -14
  126. package/eventcatalog/src/utils/commands.ts +0 -112
  127. package/eventcatalog/src/utils/events.ts +0 -108
  128. package/eventcatalog/src/utils/generators/index.ts +0 -10
  129. package/eventcatalog/src/utils/teams.ts +0 -72
  130. package/eventcatalog/src/utils/users.ts +0 -72
@@ -1,766 +1,457 @@
1
- import React, { useState, useEffect, useCallback, useMemo } from 'react';
1
+ import React, { Fragment, useState, useEffect, useMemo, useRef } from 'react';
2
+ import { Combobox, Dialog, Transition } from '@headlessui/react';
3
+ import { MagnifyingGlassIcon } from '@heroicons/react/20/solid';
2
4
  import {
3
- MagnifyingGlassIcon,
4
- QueueListIcon,
5
5
  RectangleGroupIcon,
6
+ ServerIcon,
6
7
  BoltIcon,
7
8
  ChatBubbleLeftIcon,
8
- ServerIcon,
9
+ MagnifyingGlassIcon as QueryIcon,
10
+ CubeIcon,
11
+ QueueListIcon,
9
12
  UserGroupIcon,
10
13
  UserIcon,
11
14
  BookOpenIcon,
12
15
  DocumentTextIcon,
13
- CubeIcon,
16
+ ExclamationCircleIcon,
17
+ ArrowRightIcon,
18
+ ArrowUpIcon,
19
+ ArrowDownIcon,
20
+ ArrowUturnLeftIcon,
21
+ StarIcon,
22
+ Square2StackIcon,
14
23
  } from '@heroicons/react/24/outline';
24
+ import { StarIcon as StarIconSolid, CircleStackIcon } from '@heroicons/react/24/solid';
25
+ import { useStore } from '@nanostores/react';
26
+ import { sidebarStore } from '../../stores/sidebar-store';
27
+ import { favoritesStore, toggleFavorite as toggleFavoriteAction } from '../../stores/favorites-store';
28
+
29
+ const typeIcons: any = {
30
+ Domain: RectangleGroupIcon,
31
+ Service: ServerIcon,
32
+ Event: BoltIcon,
33
+ Command: ChatBubbleLeftIcon,
34
+ Query: QueryIcon,
35
+ Entity: CubeIcon,
36
+ Channel: QueueListIcon,
37
+ Team: UserGroupIcon,
38
+ User: UserIcon,
39
+ Language: BookOpenIcon,
40
+ OpenAPI: DocumentTextIcon,
41
+ AsyncAPI: DocumentTextIcon,
42
+ Design: Square2StackIcon,
43
+ Container: CircleStackIcon,
44
+ default: DocumentTextIcon,
45
+ };
46
+
47
+ const typeColors: any = {
48
+ Domain: 'text-orange-500 bg-orange-50 ring-orange-200',
49
+ Service: 'text-pink-500 bg-pink-50 ring-pink-200',
50
+ Event: 'text-orange-500 bg-orange-50 ring-orange-200',
51
+ Command: 'text-blue-500 bg-blue-50 ring-blue-200',
52
+ Query: 'text-green-500 bg-green-50 ring-green-200',
53
+ Entity: 'text-purple-500 bg-purple-50 ring-purple-200',
54
+ Channel: 'text-indigo-500 bg-indigo-50 ring-indigo-200',
55
+ Team: 'text-teal-500 bg-teal-50 ring-teal-200',
56
+ User: 'text-cyan-500 bg-cyan-50 ring-cyan-200',
57
+ Language: 'text-amber-500 bg-amber-50 ring-amber-200',
58
+ OpenAPI: 'text-emerald-500 bg-emerald-50 ring-emerald-200',
59
+ AsyncAPI: 'text-violet-500 bg-violet-50 ring-violet-200',
60
+ Design: 'text-gray-500 bg-gray-50 ring-gray-200',
61
+ Container: 'text-indigo-500 bg-indigo-50 ring-indigo-200',
62
+ default: 'text-gray-500 bg-gray-50 ring-gray-200',
63
+ };
15
64
 
16
- interface SearchResult {
17
- id: string;
18
- name: string;
19
- type: string;
20
- description: string;
21
- url: string;
22
- tags: string[];
65
+ function classNames(...classes: (string | boolean | undefined)[]) {
66
+ return classes.filter(Boolean).join(' ');
23
67
  }
24
68
 
25
- // Memoized SearchResult component for better performance
26
- const SearchResultItem = React.memo<{
27
- item: SearchResult;
28
- typeConfig: any;
29
- currentSearch: string;
30
- }>(({ item, typeConfig, currentSearch }) => {
31
- const config = typeConfig[item.type as keyof typeof typeConfig] || typeConfig.other;
32
- const getDisplayType = (type: string) => {
33
- if (type === 'other') return 'Page';
34
- if (type === 'asyncapi') return 'AsyncAPI';
35
- if (type === 'openapi') return 'OpenAPI';
36
- if (type === 'language') return 'Language';
37
- if (type === 'entities') return 'Entity';
38
- if (type === 'queries') return 'Query';
39
- // For plurals, remove 's' at the end, otherwise just capitalize
40
- if (type.endsWith('s')) {
41
- return type.charAt(0).toUpperCase() + type.slice(1, -1);
42
- }
43
- return type.charAt(0).toUpperCase() + type.slice(1);
44
- };
45
- const displayType = getDisplayType(item.type);
46
- const IconComponent = config.icon;
69
+ // Helper to construct URL from key if href is missing
70
+ const getUrlForItem = (node: any, key: string) => {
71
+ if (node.href) return node.href;
72
+
73
+ const parts = key.split(':');
74
+ if (parts.length < 2) return null; // Need at least type:id
75
+
76
+ const type = parts[0];
77
+ const id = parts[1];
78
+ const version = parts[2]; // May be undefined
79
+
80
+ // Skip list items and other special keys
81
+ if (type === 'list') return null;
82
+
83
+ // Only show items that have a version to avoid duplicates
84
+ if (!version) return null;
85
+
86
+ // Pluralize type for URL if needed
87
+ const pluralType = ['event', 'command', 'query', 'domain', 'service', 'flow', 'container'].includes(type) ? type + 's' : type; // users/teams already have href usually, but safe fallback
88
+
89
+ return `/docs/${pluralType}/${id}/${version}`;
90
+ };
91
+
92
+ export default function SearchModal() {
93
+ const [query, setQuery] = useState('');
94
+ const [open, setOpen] = useState(false);
95
+ const [activeFilter, setActiveFilter] = useState('all');
96
+ const data = useStore(sidebarStore);
97
+ const favorites = useStore(favoritesStore);
98
+ const inputRef = useRef<HTMLInputElement>(null);
47
99
 
48
- return (
49
- <a href={item.url} className="block group">
50
- <div
51
- className={`bg-gradient-to-br ${config.bg} to-white border rounded-lg p-3 hover:border-purple-300 hover:shadow-md transition-all duration-200 group-hover:shadow-lg ${config.border}`}
52
- >
53
- <div className="flex items-start justify-between mb-2">
54
- <h3 className="text-base font-semibold text-gray-900 group-hover:text-purple-700 transition-colors">{item.name}</h3>
55
- <span
56
- className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${config.badgeBg} ${config.text} ${config.border} border`}
57
- >
58
- <IconComponent className="h-3 w-3" />
59
- {displayType}
60
- </span>
61
- </div>
62
- {item.description && (
63
- <div className="text-xs text-gray-500 mb-2 line-clamp-2 opacity-80">
64
- {currentSearch.trim() ? (
65
- <span dangerouslySetInnerHTML={{ __html: item.description }} />
66
- ) : (
67
- <span>{item.description.replace(/<[^>]*>/g, '')}</span>
68
- )}
69
- </div>
70
- )}
71
- {item.tags.length > 0 && (
72
- <div className="flex flex-wrap gap-1">
73
- {item.tags.map((tag, index) => (
74
- <span
75
- key={index}
76
- className="inline-flex items-center px-1.5 py-0.5 rounded-md text-xs font-medium bg-gray-100 text-gray-700"
77
- >
78
- {tag}
79
- </span>
80
- ))}
81
- </div>
82
- )}
83
- </div>
84
- </a>
85
- );
86
- });
87
-
88
- // Memoized SearchResults component
89
- const SearchResults = React.memo<{
90
- results: SearchResult[];
91
- typeConfig: any;
92
- currentSearch: string;
93
- }>(({ results, typeConfig, currentSearch }) => {
94
- return (
95
- <>
96
- {results.map((item) => (
97
- <SearchResultItem key={item.id} item={item} typeConfig={typeConfig} currentSearch={currentSearch} />
98
- ))}
99
- </>
100
- );
101
- });
102
-
103
- const SearchModal: React.FC = () => {
104
- const [isOpen, setIsOpen] = useState(false);
105
- const [pagefind, setPagefind] = useState<any>(null);
106
- const [pagefindLoadError, setPagefindLoadError] = useState(false);
107
- const [currentSearch, setCurrentSearch] = useState('');
108
- const [currentFilter, setCurrentFilter] = useState('all');
109
- const [allResults, setAllResults] = useState<SearchResult[]>([]);
110
- const [isLoading, setIsLoading] = useState(false);
111
- const [exactMatch, setExactMatch] = useState(false);
112
-
113
- // Listen for modal state changes from Astro component
114
100
  useEffect(() => {
115
101
  const handleModalToggle = (event: CustomEvent) => {
116
- setIsOpen(event.detail.isOpen);
102
+ setOpen(event.detail.isOpen);
103
+ if (event.detail.isOpen) {
104
+ // Focus input when modal opens
105
+ setTimeout(() => {
106
+ inputRef.current?.focus();
107
+ }, 50);
108
+ }
117
109
  };
118
110
 
119
111
  window.addEventListener('searchModalToggle', handleModalToggle as EventListener);
120
112
  return () => window.removeEventListener('searchModalToggle', handleModalToggle as EventListener);
121
113
  }, []);
122
114
 
123
- const onClose = () => {
115
+ const closeModal = () => {
124
116
  if ((window as any).searchModalState) {
125
117
  (window as any).searchModalState.close();
118
+ } else {
119
+ setOpen(false);
126
120
  }
127
121
  };
128
122
 
129
- // Type colors and icons - memoized to prevent recreating on every render
130
- const typeConfig = useMemo(
131
- () => ({
132
- domains: {
133
- bg: 'bg-orange-50/10',
134
- badgeBg: 'bg-orange-500/20',
135
- text: 'text-orange-800',
136
- border: 'border-orange-200',
137
- icon: RectangleGroupIcon,
138
- },
139
- services: {
140
- bg: 'bg-pink-50/10',
141
- badgeBg: 'bg-pink-500/20',
142
- text: 'text-pink-800',
143
- border: 'border-pink-200',
144
- icon: ServerIcon,
145
- },
146
- events: {
147
- bg: 'bg-orange-50/10',
148
- badgeBg: 'bg-orange-500/20',
149
- text: 'text-orange-800',
150
- border: 'border-orange-200',
151
- icon: BoltIcon,
152
- },
153
- commands: {
154
- bg: 'bg-blue-50/10',
155
- badgeBg: 'bg-blue-500/20',
156
- text: 'text-blue-800',
157
- border: 'border-blue-200',
158
- icon: ChatBubbleLeftIcon,
159
- },
160
- queries: {
161
- bg: 'bg-green-50/10',
162
- badgeBg: 'bg-green-500/20',
163
- text: 'text-green-800',
164
- border: 'border-green-200',
165
- icon: MagnifyingGlassIcon,
166
- },
167
- entities: {
168
- bg: 'bg-purple-50/10',
169
- badgeBg: 'bg-purple-500/20',
170
- text: 'text-purple-800',
171
- border: 'border-purple-200',
172
- icon: CubeIcon,
173
- },
174
- channels: {
175
- bg: 'bg-indigo-50/10',
176
- badgeBg: 'bg-indigo-500/20',
177
- text: 'text-indigo-800',
178
- border: 'border-indigo-200',
179
- icon: QueueListIcon,
180
- },
181
- teams: {
182
- bg: 'bg-teal-50/10',
183
- badgeBg: 'bg-teal-500/20',
184
- text: 'text-teal-800',
185
- border: 'border-teal-200',
186
- icon: UserGroupIcon,
187
- },
188
- users: {
189
- bg: 'bg-cyan-50/10',
190
- badgeBg: 'bg-cyan-500/20',
191
- text: 'text-cyan-800',
192
- border: 'border-cyan-200',
193
- icon: UserIcon,
194
- },
195
- language: {
196
- bg: 'bg-amber-50/10',
197
- badgeBg: 'bg-amber-500/20',
198
- text: 'text-amber-800',
199
- border: 'border-amber-200',
200
- icon: BookOpenIcon,
201
- },
202
- openapi: {
203
- bg: 'bg-emerald-50/10',
204
- badgeBg: 'bg-emerald-500/20',
205
- text: 'text-emerald-800',
206
- border: 'border-emerald-200',
207
- icon: DocumentTextIcon,
208
- },
209
- asyncapi: {
210
- bg: 'bg-violet-50/10',
211
- badgeBg: 'bg-violet-500/20',
212
- text: 'text-violet-800',
213
- border: 'border-violet-200',
214
- icon: DocumentTextIcon,
215
- },
216
- other: {
217
- bg: 'bg-gray-50/10',
218
- badgeBg: 'bg-gray-500/20',
219
- text: 'text-gray-800',
220
- border: 'border-gray-200',
221
- icon: DocumentTextIcon,
222
- },
223
- }),
224
- []
225
- );
226
-
227
- // Initialize Pagefind
228
- useEffect(() => {
229
- if (typeof window !== 'undefined') {
230
- const initPagefind = async () => {
231
- try {
232
- // Wait for Pagefind to be loaded by the Astro script
233
- const waitForPagefind = () =>
234
- new Promise<any>((resolve, reject) => {
235
- if ((window as any).pagefind) {
236
- resolve((window as any).pagefind);
237
- } else {
238
- const handler = () => {
239
- window.removeEventListener('pagefindLoaded', handler);
240
- resolve((window as any).pagefind);
241
- };
242
-
243
- // Set a timeout to detect if pagefind fails to load
244
- const timeout = setTimeout(() => {
245
- window.removeEventListener('pagefindLoaded', handler);
246
- reject(new Error('Pagefind failed to load - catalog may not be indexed'));
247
- }, 2000); // 5 second timeout
248
-
249
- window.addEventListener('pagefindLoaded', () => {
250
- clearTimeout(timeout);
251
- handler();
252
- });
253
- }
254
- });
255
-
256
- const pagefindModule = await waitForPagefind();
257
- await pagefindModule.init();
258
- setPagefind(pagefindModule);
259
- } catch (error) {
260
- console.error('Failed to initialize Pagefind:', error);
261
- setPagefindLoadError(true);
262
- }
263
- };
264
-
265
- initPagefind();
123
+ const items = useMemo(() => {
124
+ if (!data?.nodes) return [];
125
+
126
+ // Extract all items from nodes
127
+ const allItems = Object.entries(data.nodes)
128
+ .map(([key, node]) => {
129
+ const url = getUrlForItem(node, key);
130
+ if (!url) return null;
131
+
132
+ return {
133
+ id: url, // Use URL as unique ID
134
+ name: node.title,
135
+ url: url,
136
+ type: node.badge || 'Page',
137
+ key: key,
138
+ rawNode: node,
139
+ };
140
+ })
141
+ .filter((item): item is NonNullable<typeof item> => item !== null);
142
+
143
+ return allItems;
144
+ }, [data]);
145
+
146
+ // Get searchable items (items that match the query but not filtered by type yet)
147
+ const searchableItems = useMemo(() => {
148
+ if (query === '') {
149
+ // When no query, show all items for filter counts
150
+ return items;
266
151
  }
267
- }, []);
268
152
 
269
- // Type mapping based on URL patterns - memoized callback
270
- const getTypeFromUrl = useCallback((url: string): string => {
271
- // Check for language first since it can be nested under other paths
272
- if (url.includes('/language/')) return 'language';
273
- // Check for spec types after language but before other types since they can be nested
274
- if (url.includes('/spec/')) return 'openapi';
275
- if (url.includes('/asyncapi')) return 'asyncapi';
276
- if (url.includes('/domains/')) return 'domains';
277
- if (url.includes('/services/')) return 'services';
278
- if (url.includes('/events/')) return 'events';
279
- if (url.includes('/commands/')) return 'commands';
280
- if (url.includes('/queries/')) return 'queries';
281
- if (url.includes('/entities/')) return 'entities';
282
- if (url.includes('/channels/')) return 'channels';
283
- if (url.includes('/teams/')) return 'teams';
284
- if (url.includes('/users/')) return 'users';
285
- return 'other';
286
- }, []);
153
+ const lowerQuery = query.toLowerCase();
154
+ return items.filter((item) => item.name.toLowerCase().includes(lowerQuery));
155
+ }, [items, query]);
287
156
 
288
- // Perform search
289
- const performSearch = useCallback(
290
- async (searchTerm: string): Promise<SearchResult[]> => {
291
- if (!pagefind || !searchTerm.trim()) {
292
- return [];
293
- }
157
+ const filters = useMemo(() => {
158
+ // Calculate counts based on current search results (searchableItems)
159
+ if (!searchableItems.length && query !== '') {
160
+ // If searching and no results, still show filters but with 0 counts
161
+ return [{ id: 'all', name: 'All (0)' }];
162
+ }
294
163
 
295
- setIsLoading(true);
296
-
297
- try {
298
- const search = await pagefind.debouncedSearch(searchTerm);
299
- if (!search || !search.results) {
300
- return [];
301
- }
302
- const processedResults: SearchResult[] = [];
303
-
304
- for (const result of search.results) {
305
- const data = await result.data();
306
- const type = getTypeFromUrl(data.url);
307
-
308
- // Clean the title by removing any "Type | " prefix if it exists
309
- let cleanTitle = data.meta?.title || 'Untitled';
310
-
311
- // Use regex for more efficient prefix removal
312
- cleanTitle = cleanTitle.replace(
313
- /^(Domains?|Services?|Events?|Commands?|Queries?|Entities?|Channels?|Teams?|Users?|Language) \| /,
314
- ''
315
- );
316
-
317
- processedResults.push({
318
- id: result.id,
319
- name: cleanTitle,
320
- type: type,
321
- description: data.excerpt || '',
322
- url: data.url,
323
- tags: data.meta?.tags ? data.meta.tags.split(',').map((tag: string) => tag.trim()) : [],
324
- });
325
- }
326
-
327
- return processedResults;
328
- } catch (error) {
329
- console.error('Search error:', error);
330
- return [];
331
- } finally {
332
- setIsLoading(false);
333
- }
334
- },
335
- [pagefind]
336
- );
164
+ const itemsToCount = query === '' ? items : searchableItems;
337
165
 
338
- // Filter results - memoized callback
339
- const filterResults = useCallback(
340
- (results: SearchResult[], filterType: string): SearchResult[] => {
341
- let filteredResults = results;
166
+ const counts: Record<string, number> = {
167
+ all: itemsToCount.length,
168
+ Domain: 0,
169
+ Service: 0,
170
+ Message: 0,
171
+ Team: 0,
172
+ Container: 0,
173
+ Design: 0,
174
+ };
342
175
 
343
- // Apply type filter
344
- if (filterType !== 'all') {
345
- filteredResults = filteredResults.filter((item) => item.type === filterType);
176
+ itemsToCount.forEach((item) => {
177
+ // Count specific types
178
+ if (counts[item.type] !== undefined) {
179
+ counts[item.type]++;
346
180
  }
347
181
 
348
- // Apply exact match filter if enabled
349
- if (exactMatch && currentSearch.trim()) {
350
- filteredResults = filteredResults.filter((item) => item.name.toLowerCase().includes(currentSearch.toLowerCase()));
182
+ // Group counts
183
+ if (['Event', 'Command', 'Query'].includes(item.type)) {
184
+ counts.Message++;
185
+ }
186
+ if (['Team', 'User'].includes(item.type)) {
187
+ counts.Team++;
351
188
  }
189
+ });
352
190
 
353
- return filteredResults;
354
- },
355
- [exactMatch, currentSearch]
356
- );
191
+ const dynamicFilters = [{ id: 'all', name: `All (${counts.all})` }];
357
192
 
358
- // Update results with debouncing
359
- const updateResults = useCallback(async () => {
360
- if (currentSearch.trim()) {
361
- const results = await performSearch(currentSearch);
362
- setAllResults(results);
363
- } else {
364
- setAllResults([]);
365
- }
366
- }, [currentSearch, performSearch]);
193
+ // Only show filters that have results when searching
194
+ if (counts.Domain > 0) dynamicFilters.push({ id: 'Domain', name: `Domains (${counts.Domain})` });
195
+ if (counts.Service > 0) dynamicFilters.push({ id: 'Service', name: `Services (${counts.Service})` });
196
+ if (counts.Message > 0) dynamicFilters.push({ id: 'Message', name: `Messages (${counts.Message})` });
197
+ if (counts.Container > 0) dynamicFilters.push({ id: 'Container', name: `Containers (${counts.Container})` });
198
+ if (counts.Design > 0) dynamicFilters.push({ id: 'Design', name: `Designs (${counts.Design})` });
199
+ if (counts.Team > 0) dynamicFilters.push({ id: 'Team', name: `Teams & Users (${counts.Team})` });
367
200
 
368
- // Search on input change with debouncing
201
+ return dynamicFilters;
202
+ }, [searchableItems, items, query]);
203
+
204
+ // Reset active filter if it no longer has results
369
205
  useEffect(() => {
370
- if (!currentSearch.trim()) {
371
- setAllResults([]);
372
- return;
206
+ if (activeFilter !== 'all' && !filters.some((f) => f.id === activeFilter)) {
207
+ setActiveFilter('all');
373
208
  }
374
-
375
- const debounceTimer = setTimeout(() => {
376
- updateResults();
377
- }, 300); // 300ms debounce
378
-
379
- return () => clearTimeout(debounceTimer);
380
- }, [currentSearch, updateResults]);
381
-
382
- // Handle input change
383
- const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
384
- setCurrentSearch(e.target.value);
385
- };
386
-
387
- // Handle filter change
388
- const handleFilterChange = (filter: string) => {
389
- setCurrentFilter(filter);
209
+ }, [filters, activeFilter]);
210
+
211
+ const handleToggleFavorite = (e: React.MouseEvent, item: any) => {
212
+ e.preventDefault();
213
+ e.stopPropagation();
214
+
215
+ toggleFavoriteAction({
216
+ nodeKey: item.key,
217
+ path: [], // Path is not easily available here, but Sidebar rebuilds it
218
+ title: item.name,
219
+ badge: item.rawNode.badge,
220
+ href: item.url,
221
+ });
390
222
  };
391
223
 
392
- // Get filtered results - memoized to prevent recalculation
393
- const filteredResults = useMemo(() => {
394
- return filterResults(allResults, currentFilter);
395
- }, [allResults, currentFilter, exactMatch, currentSearch]);
396
-
397
- // Get filter counts - memoized to prevent recalculation on every render
398
- const getFilterCounts = useMemo(() => {
399
- return {
400
- all: allResults.length,
401
- domains: allResults.filter((r) => r.type === 'domains').length,
402
- services: allResults.filter((r) => r.type === 'services').length,
403
- events: allResults.filter((r) => r.type === 'events').length,
404
- commands: allResults.filter((r) => r.type === 'commands').length,
405
- queries: allResults.filter((r) => r.type === 'queries').length,
406
- entities: allResults.filter((r) => r.type === 'entities').length,
407
- channels: allResults.filter((r) => r.type === 'channels').length,
408
- teams: allResults.filter((r) => r.type === 'teams').length,
409
- users: allResults.filter((r) => r.type === 'users').length,
410
- language: allResults.filter((r) => r.type === 'language').length,
411
- openapi: allResults.filter((r) => r.type === 'openapi').length,
412
- asyncapi: allResults.filter((r) => r.type === 'asyncapi').length,
413
- };
414
- }, [allResults]);
415
-
416
- const counts = getFilterCounts;
417
-
418
- // Handle escape key
419
- useEffect(() => {
420
- const handleEscape = (e: KeyboardEvent) => {
421
- if (e.key === 'Escape') {
422
- onClose();
224
+ const filteredItems = useMemo(() => {
225
+ if (query === '') {
226
+ // Show favorites when search is empty
227
+ if (favorites.length > 0) {
228
+ return favorites
229
+ .slice(0, 5)
230
+ .map((fav) => {
231
+ const node = data?.nodes[fav.nodeKey];
232
+ if (!node) return null;
233
+ const url = getUrlForItem(node, fav.nodeKey);
234
+ if (!url) return null;
235
+
236
+ return {
237
+ id: url,
238
+ name: fav.title,
239
+ url: url,
240
+ type: fav.badge || 'Page',
241
+ key: fav.nodeKey,
242
+ rawNode: node,
243
+ isFavorite: true,
244
+ };
245
+ })
246
+ .filter((item): item is NonNullable<typeof item> => item !== null);
423
247
  }
424
- };
248
+ return [];
249
+ }
425
250
 
426
- if (isOpen) {
427
- document.addEventListener('keydown', handleEscape);
428
- return () => document.removeEventListener('keydown', handleEscape);
251
+ // Start with searchable items (already filtered by query)
252
+ let result = searchableItems;
253
+
254
+ // Apply type filter
255
+ if (activeFilter !== 'all') {
256
+ if (activeFilter === 'Message') {
257
+ result = result.filter((item) => ['Event', 'Command', 'Query'].includes(item.type));
258
+ } else if (activeFilter === 'Team') {
259
+ result = result.filter((item) => ['Team', 'User'].includes(item.type));
260
+ } else {
261
+ result = result.filter((item) => item.type === activeFilter);
262
+ }
429
263
  }
430
- }, [isOpen, onClose]);
431
264
 
432
- if (!isOpen) return null;
265
+ return result.slice(0, 50); // Limit results for performance
266
+ }, [searchableItems, query, activeFilter, favorites, data]);
433
267
 
434
268
  return (
435
- <div>
436
- <style>{`
437
- .search-results mark {
438
- background-color: #fef3c7;
439
- color: #92400e;
440
- padding: 0.125rem 0.25rem;
441
- border-radius: 0.25rem;
442
- font-weight: 500;
443
- }
444
- `}</style>
445
- <div className="fixed inset-0 z-[9999] overflow-y-auto" role="dialog" aria-modal="true">
446
- <div
447
- className="fixed inset-0 bg-gray-500 bg-opacity-25 transition-opacity backdrop-blur-sm bg-black/10 z-[9998]"
448
- onClick={onClose}
449
- ></div>
450
- <div className="fixed inset-0 z-[10000] w-screen overflow-y-auto p-4 sm:p-6 md:p-10" onClick={onClose}>
451
- <div
452
- className="mx-auto max-w-6xl divide-y divide-gray-100 overflow-hidden rounded-xl bg-white shadow-2xl ring-1 ring-black ring-opacity-5 transition-all"
453
- onClick={(e) => e.stopPropagation()}
269
+ <Transition.Root
270
+ show={open}
271
+ as={Fragment}
272
+ afterLeave={() => {
273
+ setQuery('');
274
+ setActiveFilter('all');
275
+ }}
276
+ appear
277
+ >
278
+ <Dialog as="div" className="relative z-50" onClose={closeModal}>
279
+ <Transition.Child
280
+ as={Fragment}
281
+ enter="ease-out duration-300"
282
+ enterFrom="opacity-0"
283
+ enterTo="opacity-100"
284
+ leave="ease-in duration-200"
285
+ leaveFrom="opacity-100"
286
+ leaveTo="opacity-0"
287
+ >
288
+ <div className="fixed inset-0 bg-gray-500 bg-opacity-25 transition-opacity backdrop-blur-sm" />
289
+ </Transition.Child>
290
+
291
+ <div className="fixed inset-0 z-10 w-screen overflow-y-auto p-4 sm:p-6 md:p-20">
292
+ <Transition.Child
293
+ as={Fragment}
294
+ enter="ease-out duration-300"
295
+ enterFrom="opacity-0 scale-95"
296
+ enterTo="opacity-100 scale-100"
297
+ leave="ease-in duration-200"
298
+ leaveFrom="opacity-100 scale-100"
299
+ leaveTo="opacity-0 scale-95"
454
300
  >
455
- {pagefindLoadError ? (
456
- // Show indexing required message when Pagefind fails to load - full modal content
457
- <div className="flex items-center justify-center py-10 px-8">
458
- <div className="text-left max-w-lg">
459
- <div className="mb-8">
460
- <h2 className="text-2xl font-bold text-gray-900 mb-3">Search Index Not Found</h2>
461
- <p className="text-gray-600 mb-8 leading-relaxed text-sm">
462
- Your EventCatalog needs to be built to generate the search index. This enables fast searching across all
463
- your domains, services, events, and documentation.
464
- </p>
465
- </div>
466
-
467
- <div className="bg-gradient-to-r from-gray-50 to-gray-100 rounded-xl p-6 mb-6 border border-gray-200">
468
- <h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center justify-center">
469
- <svg className="h-5 w-5 text-green-600 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
470
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
471
- </svg>
472
- Build Your Catalog
473
- </h3>
474
- <div className="bg-gray-900 rounded-lg p-4 mb-4">
475
- <code className="text-green-400 font-mono text-sm">npm run build</code>
476
- </div>
477
- <p className="text-sm text-gray-600">This will generate your catalog and create the search index</p>
478
- </div>
479
-
480
- <div className="flex items-start text-left bg-blue-50 rounded-lg p-4 border border-blue-200">
481
- <svg
482
- className="h-5 w-5 text-blue-600 mr-3 mt-0.5 flex-shrink-0"
483
- fill="none"
484
- stroke="currentColor"
485
- viewBox="0 0 24 24"
486
- >
487
- <path
488
- strokeLinecap="round"
489
- strokeLinejoin="round"
490
- strokeWidth="2"
491
- d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
492
- />
493
- </svg>
494
- <div>
495
- <h4 className="font-medium text-blue-900 mb-1">Need to update search results?</h4>
496
- <p className="text-sm text-blue-700">
497
- Run <code className="bg-blue-100 px-1 py-0.5 rounded text-xs font-mono">npm run build</code> again after
498
- making changes to your catalog content.
499
- </p>
500
- </div>
501
- </div>
502
- </div>
503
- </div>
504
- ) : (
505
- <>
506
- {/* Search Input */}
507
- <div className="relative px-6 pt-4 pb-2">
508
- <MagnifyingGlassIcon className="pointer-events-none absolute left-10 top-[25px] h-5 w-5 text-gray-400" />
509
- <input
510
- type="text"
511
- placeholder="Search for domains, services, events..."
512
- className="w-full border border-gray-200 rounded-lg bg-white pl-12 pr-4 py-2 text-gray-900 placeholder:text-gray-400 focus:ring-2 focus:ring-purple-500 focus:border-purple-500 text-sm"
513
- value={currentSearch}
514
- onChange={handleSearchChange}
301
+ <Dialog.Panel className="mx-auto max-w-2xl transform divide-y divide-gray-100 overflow-hidden rounded-xl bg-white shadow-2xl ring-1 ring-black ring-opacity-5 transition-all">
302
+ <Combobox
303
+ onChange={(item: any) => {
304
+ if (item?.url) {
305
+ window.location.href = item.url;
306
+ closeModal();
307
+ }
308
+ }}
309
+ >
310
+ <div className="relative border-b border-gray-100">
311
+ <MagnifyingGlassIcon
312
+ className="pointer-events-none absolute left-4 top-3.5 h-5 w-5 text-gray-400"
313
+ aria-hidden="true"
314
+ />
315
+ <Combobox.Input
316
+ ref={inputRef}
317
+ className="h-12 w-full border-0 bg-transparent pl-11 pr-4 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm focus:outline-none"
318
+ placeholder="Search..."
319
+ onChange={(event) => setQuery(event.target.value)}
320
+ value={query}
515
321
  autoFocus
322
+ autoComplete="off"
516
323
  />
517
324
  </div>
518
325
 
519
- {/* Main Content Area */}
520
- <div className="flex h-[500px]">
521
- {/* Left Filters */}
522
- <div className="w-56 p-3 border-r border-gray-200 bg-gray-50 overflow-y-auto">
523
- {/* All Resources */}
524
- <div className="mb-4">
525
- <div className="space-y-1">
526
- <button
527
- className={`w-full px-2 py-1 text-xs rounded-md transition-colors ${
528
- currentFilter === 'all'
529
- ? 'bg-purple-200 text-purple-900 font-semibold'
530
- : 'hover:bg-purple-100 text-gray-700 hover:text-purple-800'
531
- }`}
532
- onClick={() => handleFilterChange('all')}
533
- >
534
- <div className="flex items-center justify-between">
535
- <div className="flex items-center gap-1.5">
536
- <span>All Resources</span>
537
- </div>
538
- <span className="text-xs text-gray-700 font-thin">{counts.all}</span>
539
- </div>
540
- </button>
541
- </div>
542
- </div>
543
-
544
- {/* Resources Section */}
545
- <div className="mb-4">
546
- <h3 className="text-xs font-bold text-gray-600 mb-2">Resources</h3>
547
- <div className="space-y-1">
548
- {Object.entries({
549
- domains: 'Domains',
550
- services: 'Services',
551
- entities: 'Entities',
552
- language: 'Ubiquitous Language',
553
- }).map(([key, label]) => {
554
- const config = typeConfig[key as keyof typeof typeConfig];
555
- const IconComponent = config?.icon;
556
-
557
- return (
558
- <button
559
- key={key}
560
- className={`w-full px-2 py-1 text-xs rounded-md transition-colors ${
561
- currentFilter === key
562
- ? 'bg-purple-200 text-purple-900 font-semibold'
563
- : 'hover:bg-purple-100 text-gray-700 hover:text-purple-800'
564
- }`}
565
- onClick={() => handleFilterChange(key)}
566
- >
567
- <div className="flex items-center justify-between">
568
- <div className="flex items-center gap-1.5">
569
- {IconComponent && <IconComponent className="h-3 w-3" />}
570
- <span className="font-thin">{label}</span>
571
- </div>
572
- <span className="text-xs text-gray-700 font-thin">{counts[key as keyof typeof counts]}</span>
573
- </div>
574
- </button>
575
- );
576
- })}
326
+ {/* Filter Tabs */}
327
+ <div className="flex items-center gap-2 px-4 py-3 overflow-x-auto no-scrollbar border-b border-gray-100">
328
+ {filters.map((tab) => (
329
+ <button
330
+ key={tab.id}
331
+ onClick={() => setActiveFilter(tab.id)}
332
+ className={classNames(
333
+ 'px-3 py-1 text-xs font-medium rounded-full transition-colors whitespace-nowrap',
334
+ activeFilter === tab.id ? 'bg-purple-100 text-purple-700' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
335
+ )}
336
+ >
337
+ {tab.name}
338
+ </button>
339
+ ))}
340
+ </div>
341
+
342
+ {filteredItems.length > 0 && (
343
+ <>
344
+ {query === '' && favorites.length > 0 && (
345
+ <div className="px-6 pt-3 pb-2">
346
+ <p className="text-xs text-gray-500">Favourites</p>
577
347
  </div>
578
- </div>
579
-
580
- {/* Messages Section */}
581
- <div className="mb-4">
582
- <h3 className="text-xs font-bold text-gray-600 mb-2">Messages</h3>
583
- <div className="space-y-1">
584
- {Object.entries({
585
- events: 'Events',
586
- commands: 'Commands',
587
- queries: 'Queries',
588
- channels: 'Channels',
589
- }).map(([key, label]) => {
590
- const config = typeConfig[key as keyof typeof typeConfig];
591
- const IconComponent = config?.icon;
592
-
593
- return (
594
- <button
595
- key={key}
596
- className={`w-full px-2 py-1 text-xs rounded-md transition-colors ${
597
- currentFilter === key
598
- ? 'bg-purple-200 text-purple-900 font-semibold'
599
- : 'hover:bg-purple-100 text-gray-700 hover:text-purple-800'
600
- }`}
601
- onClick={() => handleFilterChange(key)}
602
- >
603
- <div className="flex items-center justify-between">
604
- <div className="flex items-center gap-1.5">
605
- {IconComponent && <IconComponent className="h-3 w-3" />}
606
- <span className="font-thin">{label}</span>
348
+ )}
349
+ <Combobox.Options static className="max-h-96 scroll-py-3 overflow-y-auto p-3">
350
+ {filteredItems.map((item) => {
351
+ const Icon = typeIcons[item.type] || typeIcons.default;
352
+ const colors = typeColors[item.type] || typeColors.default;
353
+
354
+ const isFavorite = favorites.some((fav) => fav.nodeKey === item.key);
355
+
356
+ return (
357
+ <Combobox.Option
358
+ key={item.id}
359
+ value={item}
360
+ className={({ active }) =>
361
+ classNames('flex cursor-default select-none rounded-xl p-3 group', active && 'bg-gray-100')
362
+ }
363
+ >
364
+ {({ active }) => (
365
+ <>
366
+ <div
367
+ className={classNames(
368
+ 'flex h-10 w-10 flex-none items-center justify-center rounded-lg ring-1 ring-inset',
369
+ colors
370
+ )}
371
+ >
372
+ <Icon className="h-6 w-6" aria-hidden="true" />
607
373
  </div>
608
- <span className="text-xs text-gray-700 font-thin">{counts[key as keyof typeof counts]}</span>
609
- </div>
610
- </button>
611
- );
612
- })}
613
- </div>
614
- </div>
615
-
616
- {/* Organization Section */}
617
- <div className="mb-4">
618
- <h3 className="text-xs font-bold text-gray-600 mb-2">Organization</h3>
619
- <div className="space-y-1">
620
- {Object.entries({
621
- teams: 'Teams',
622
- users: 'Users',
623
- }).map(([key, label]) => {
624
- const config = typeConfig[key as keyof typeof typeConfig];
625
- const IconComponent = config?.icon;
626
-
627
- return (
628
- <button
629
- key={key}
630
- className={`w-full px-2 py-1 text-xs rounded-md transition-colors ${
631
- currentFilter === key
632
- ? 'bg-purple-200 text-purple-900 font-semibold'
633
- : 'hover:bg-purple-100 text-gray-700 hover:text-purple-800'
634
- }`}
635
- onClick={() => handleFilterChange(key)}
636
- >
637
- <div className="flex items-center justify-between">
638
- <div className="flex items-center gap-1.5">
639
- {IconComponent && <IconComponent className="h-3 w-3" />}
640
- <span className="font-thin">{label}</span>
374
+ <div className="ml-4 flex-auto min-w-0">
375
+ <p className={classNames('text-sm font-medium', active ? 'text-gray-900' : 'text-gray-700')}>
376
+ {item.name}
377
+ </p>
378
+ <div className="flex items-start gap-2">
379
+ <p
380
+ className={classNames('text-sm flex-shrink-0', active ? 'text-gray-700' : 'text-gray-500')}
381
+ >
382
+ {item.type}
383
+ </p>
384
+ {item.rawNode.summary && (
385
+ <p className={classNames('text-xs truncate', active ? 'text-gray-600' : 'text-gray-400')}>
386
+ {item.rawNode.summary}
387
+ </p>
388
+ )}
389
+ </div>
641
390
  </div>
642
- <span className="text-xs text-gray-700 font-thin">{counts[key as keyof typeof counts]}</span>
643
- </div>
644
- </button>
645
- );
646
- })}
647
- </div>
648
- </div>
649
-
650
- {/* Specifications Section */}
651
- <div className="mb-4">
652
- <h3 className="text-xs font-bold text-gray-600 mb-2">Specifications</h3>
653
- <div className="space-y-1">
654
- {Object.entries({
655
- openapi: 'OpenAPI Specification',
656
- asyncapi: 'AsyncAPI Specification',
657
- }).map(([key, label]) => {
658
- const config = typeConfig[key as keyof typeof typeConfig];
659
- const IconComponent = config.icon;
660
-
661
- return (
662
- <button
663
- key={key}
664
- className={`w-full px-2 py-1 text-xs rounded-md transition-colors ${
665
- currentFilter === key
666
- ? 'bg-purple-200 text-purple-900 font-semibold'
667
- : 'hover:bg-purple-100 text-gray-700 hover:text-purple-800'
668
- }`}
669
- onClick={() => handleFilterChange(key)}
670
- >
671
- <div className="flex items-center justify-between">
672
- <div className="flex items-center gap-1.5">
673
- <IconComponent className="h-3 w-3" />
674
- <span className="font-thin">{label}</span>
391
+ <div className="flex items-center">
392
+ <button
393
+ onClick={(e) => handleToggleFavorite(e, item)}
394
+ onMouseDown={(e) => {
395
+ e.preventDefault();
396
+ e.stopPropagation();
397
+ }}
398
+ className={classNames(
399
+ 'p-1 rounded-md transition-colors mr-2',
400
+ isFavorite
401
+ ? 'text-amber-400 hover:text-amber-500'
402
+ : 'text-gray-300 opacity-0 group-hover:opacity-100 hover:text-amber-400'
403
+ )}
404
+ >
405
+ {isFavorite ? <StarIconSolid className="h-5 w-5" /> : <StarIcon className="h-5 w-5" />}
406
+ </button>
407
+ {active && <ArrowRightIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />}
675
408
  </div>
676
- <span className="text-xs text-gray-700 font-thin">{counts[key as keyof typeof counts]}</span>
677
- </div>
678
- </button>
679
- );
680
- })}
681
- </div>
682
- </div>
409
+ </>
410
+ )}
411
+ </Combobox.Option>
412
+ );
413
+ })}
414
+ </Combobox.Options>
415
+ </>
416
+ )}
417
+
418
+ {query !== '' && filteredItems.length === 0 && (
419
+ <div className="py-14 px-6 text-center text-sm sm:px-14">
420
+ <ExclamationCircleIcon type="outline" name="exclamation-circle" className="mx-auto h-6 w-6 text-gray-400" />
421
+ <p className="mt-4 font-semibold text-gray-900">No results found</p>
422
+ <p className="mt-2 text-gray-500">No components found for this search term. Please try again.</p>
683
423
  </div>
424
+ )}
684
425
 
685
- {/* Right Results */}
686
- <div className="flex-1 flex flex-col overflow-hidden">
687
- {/* Show stats and exact match toggle - only when there are results or search term */}
688
- {currentSearch.trim() && (filteredResults.length > 0 || isLoading) && (
689
- <div className="p-4 pb-2 flex items-center justify-between border-b border-gray-100">
690
- <div className="text-sm text-gray-500 font-thin">
691
- <span>{filteredResults.length} results</span> for "{currentSearch}"
692
- {isLoading && <span className="ml-2">Loading...</span>}
693
- </div>
694
-
695
- {/* Exact Match Checkbox */}
696
- <div className="flex items-center">
697
- <input
698
- id="exact-match-results"
699
- type="checkbox"
700
- checked={exactMatch}
701
- onChange={(e) => setExactMatch(e.target.checked)}
702
- className="h-4 w-4 text-purple-600 focus:ring-purple-500 border-gray-300 rounded"
703
- />
704
- <label htmlFor="exact-match-results" className="ml-2 text-sm text-gray-600">
705
- Exact match in title
706
- </label>
707
- </div>
708
- </div>
709
- )}
426
+ {query === '' && filteredItems.length === 0 && (
427
+ <div className="py-14 px-6 text-center text-sm sm:px-14">
428
+ <MagnifyingGlassIcon className="mx-auto h-6 w-6 text-gray-400" />
429
+ <p className="mt-4 font-semibold text-gray-900">Search for anything</p>
430
+ <p className="mt-2 text-gray-500">Search for domains, services, events, commands, queries and more.</p>
431
+ </div>
432
+ )}
710
433
 
711
- {/* Main content area */}
712
- <div className="flex-1 overflow-y-auto">
713
- {!currentSearch.trim() ? (
714
- // Show when no search term is entered - centered
715
- <div className="h-full flex items-center justify-center">
716
- <div className="text-center">
717
- <MagnifyingGlassIcon className="mx-auto h-8 w-8 text-gray-300" />
718
- <h3 className="mt-4 text-lg font-medium text-gray-900">Discover your EventCatalog</h3>
719
- <p className="mt-2 text-sm text-gray-500 font-thin">
720
- Start typing to search for domains, services, events, and more.
721
- </p>
722
- </div>
723
- </div>
724
- ) : filteredResults.length === 0 && !isLoading ? (
725
- // Show when search term exists but no results and not loading - centered
726
- <div className="h-full flex items-center justify-center">
727
- <div className="text-center">
728
- <MagnifyingGlassIcon className="mx-auto h-8 w-8 text-gray-300" />
729
- <h3 className="mt-2 text-sm font-bold text-gray-900">No results found</h3>
730
- <p className="mt-1 text-sm text-gray-500 font-thin">
731
- No results found for "<span className="font-medium">{currentSearch}</span>".
732
- </p>
733
- </div>
734
- </div>
735
- ) : isLoading ? (
736
- // Show loading state - centered
737
- <div className="h-full flex items-center justify-center">
738
- <div className="text-center">
739
- <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600 mx-auto"></div>
740
- <h3 className="mt-4 text-sm font-medium text-gray-900">Searching...</h3>
741
- <p className="mt-2 text-sm text-gray-500 font-thin">
742
- Finding results for "<span className="font-medium">{currentSearch}</span>"
743
- </p>
744
- </div>
745
- </div>
746
- ) : (
747
- // Show results in a grid with padding
748
- <div className="p-4">
749
- <div className="search-results grid grid-cols-1 lg:grid-cols-2 gap-3">
750
- <SearchResults results={filteredResults} typeConfig={typeConfig} currentSearch={currentSearch} />
751
- </div>
752
- </div>
753
- )}
754
- </div>
434
+ {/* Footer */}
435
+ <div className="flex flex-wrap items-center bg-gray-50 py-2.5 px-4 text-xs text-gray-500 border-t border-gray-100">
436
+ <div className="flex items-center mr-4">
437
+ <ArrowUturnLeftIcon className="h-3 w-3 mr-1" />
438
+ to select
439
+ </div>
440
+ <div className="flex items-center mr-4">
441
+ <ArrowDownIcon className="h-3 w-3 mr-1" />
442
+ <ArrowUpIcon className="h-3 w-3 mr-1" />
443
+ to navigate
444
+ </div>
445
+ <div className="flex items-center">
446
+ <span className="mr-1">esc</span>
447
+ to close
755
448
  </div>
756
449
  </div>
757
- </>
758
- )}
759
- </div>
450
+ </Combobox>
451
+ </Dialog.Panel>
452
+ </Transition.Child>
760
453
  </div>
761
- </div>
762
- </div>
454
+ </Dialog>
455
+ </Transition.Root>
763
456
  );
764
- };
765
-
766
- export default SearchModal;
457
+ }