@eventcatalog/core 2.63.0 → 2.64.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 (47) hide show
  1. package/dist/analytics/analytics.cjs +1 -1
  2. package/dist/analytics/analytics.js +2 -2
  3. package/dist/analytics/log-build.cjs +1 -1
  4. package/dist/analytics/log-build.js +3 -3
  5. package/dist/{chunk-I2FMV7LN.js → chunk-6AMZOBWI.js} +1 -1
  6. package/dist/{chunk-IRFM5IS7.js → chunk-CWGFHLMX.js} +1 -1
  7. package/dist/{chunk-GA274FBN.js → chunk-PLMTJHGH.js} +1 -1
  8. package/dist/constants.cjs +1 -1
  9. package/dist/constants.js +1 -1
  10. package/dist/eventcatalog.cjs +1 -1
  11. package/dist/eventcatalog.js +3 -3
  12. package/eventcatalog/astro.config.mjs +2 -1
  13. package/eventcatalog/public/icons/avro.svg +21 -0
  14. package/eventcatalog/public/icons/json-schema.svg +6 -0
  15. package/eventcatalog/public/icons/proto.svg +10 -0
  16. package/eventcatalog/src/components/Grids/utils.tsx +5 -3
  17. package/eventcatalog/src/components/MDX/RemoteFile.astro +5 -11
  18. package/eventcatalog/src/components/MDX/SchemaViewer/SchemaViewerRoot.astro +41 -6
  19. package/eventcatalog/src/components/SchemaExplorer/ApiAccessSection.tsx +139 -0
  20. package/eventcatalog/src/components/SchemaExplorer/AvroSchemaViewer.tsx +423 -0
  21. package/eventcatalog/src/components/SchemaExplorer/DiffViewer.tsx +102 -0
  22. package/eventcatalog/src/components/SchemaExplorer/JSONSchemaViewer.tsx +740 -0
  23. package/eventcatalog/src/components/SchemaExplorer/OwnersSection.tsx +56 -0
  24. package/eventcatalog/src/components/SchemaExplorer/Pagination.tsx +33 -0
  25. package/eventcatalog/src/components/SchemaExplorer/ProducersConsumersSection.tsx +91 -0
  26. package/eventcatalog/src/components/SchemaExplorer/SchemaCodeModal.tsx +93 -0
  27. package/eventcatalog/src/components/SchemaExplorer/SchemaContentViewer.tsx +130 -0
  28. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsHeader.tsx +181 -0
  29. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +232 -0
  30. package/eventcatalog/src/components/SchemaExplorer/SchemaExplorer.tsx +415 -0
  31. package/eventcatalog/src/components/SchemaExplorer/SchemaFilters.tsx +174 -0
  32. package/eventcatalog/src/components/SchemaExplorer/SchemaListItem.tsx +73 -0
  33. package/eventcatalog/src/components/SchemaExplorer/SchemaViewerModal.tsx +77 -0
  34. package/eventcatalog/src/components/SchemaExplorer/VersionHistoryModal.tsx +72 -0
  35. package/eventcatalog/src/components/SchemaExplorer/types.ts +45 -0
  36. package/eventcatalog/src/components/SchemaExplorer/utils.ts +81 -0
  37. package/eventcatalog/src/components/SideNav/ListViewSideBar/index.tsx +33 -2
  38. package/eventcatalog/src/components/Tables/columns/MessageTableColumns.tsx +2 -2
  39. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +10 -0
  40. package/eventcatalog/src/pages/api/schemas/[collection]/[id]/[version]/index.ts +45 -0
  41. package/eventcatalog/src/pages/api/schemas/services/[id]/[version]/[specification]/index.ts +51 -0
  42. package/eventcatalog/src/pages/docs/llm/schemas.txt.ts +86 -0
  43. package/eventcatalog/src/pages/schemas/index.astro +175 -0
  44. package/eventcatalog/src/utils/files.ts +9 -0
  45. package/package.json +1 -1
  46. package/eventcatalog/src/components/MDX/SchemaViewer/SchemaProperty.astro +0 -204
  47. package/eventcatalog/src/components/MDX/SchemaViewer/SchemaViewer.astro +0 -705
@@ -0,0 +1,740 @@
1
+ import { useState, useEffect, useRef, useMemo } from 'react';
2
+
3
+ interface JSONSchemaViewerProps {
4
+ schema: any;
5
+ title?: string;
6
+ maxHeight?: string;
7
+ expand?: boolean | string;
8
+ search?: boolean | string;
9
+ id?: string;
10
+ onOpenFullscreen?: () => void;
11
+ }
12
+
13
+ interface SchemaPropertyProps {
14
+ name: string;
15
+ details: any;
16
+ isRequired: boolean;
17
+ level: number;
18
+ isListItem?: boolean;
19
+ expand: boolean;
20
+ }
21
+
22
+ // Helper function to count properties recursively
23
+ function countProperties(obj: any): number {
24
+ if (!obj || typeof obj !== 'object') return 0;
25
+
26
+ let count = 0;
27
+ if (obj.properties) {
28
+ count += Object.keys(obj.properties).length;
29
+ Object.values(obj.properties).forEach((prop: any) => {
30
+ count += countProperties(prop);
31
+ });
32
+ }
33
+ if (obj.items) {
34
+ count += countProperties(obj.items);
35
+ }
36
+ if (obj._isRootArrayItem && obj._rootArraySchema?.items) {
37
+ // Don't double count
38
+ }
39
+ return count;
40
+ }
41
+
42
+ // Schema processing functions
43
+ function mergeAllOfSchemas(schemaWithProcessor: any): any {
44
+ const { processSchema: processor, ...schema } = schemaWithProcessor;
45
+ if (!schema.allOf) return schema;
46
+
47
+ const mergedSchema: {
48
+ type: string;
49
+ properties: Record<string, any>;
50
+ required: string[];
51
+ description?: string;
52
+ [key: string]: any;
53
+ } = {
54
+ type: schema.type || 'object',
55
+ properties: {},
56
+ required: [],
57
+ description: schema.description,
58
+ };
59
+
60
+ // Copy base schema properties first (excluding allOf)
61
+ Object.keys(schema).forEach((key) => {
62
+ if (key !== 'allOf' && key !== 'properties' && key !== 'required' && key !== 'description') {
63
+ mergedSchema[key] = schema[key];
64
+ }
65
+ });
66
+
67
+ // Copy base properties if they exist
68
+ if (schema.properties) {
69
+ mergedSchema.properties = { ...schema.properties };
70
+ }
71
+ if (schema.required) {
72
+ mergedSchema.required = [...schema.required];
73
+ }
74
+
75
+ schema.allOf.forEach((subSchema: any) => {
76
+ const processedSubSchema = processor ? processor(subSchema) : subSchema;
77
+
78
+ if (processedSubSchema.properties) {
79
+ mergedSchema.properties = {
80
+ ...mergedSchema.properties,
81
+ ...processedSubSchema.properties,
82
+ };
83
+ }
84
+ if (processedSubSchema.required) {
85
+ mergedSchema.required = [...new Set([...mergedSchema.required, ...processedSubSchema.required])];
86
+ }
87
+ if (processedSubSchema.description && !mergedSchema.description) {
88
+ mergedSchema.description = processedSubSchema.description;
89
+ }
90
+
91
+ Object.keys(processedSubSchema).forEach((key) => {
92
+ if (key !== 'properties' && key !== 'required' && key !== 'description' && key !== 'type') {
93
+ if (!mergedSchema[key]) {
94
+ mergedSchema[key] = processedSubSchema[key];
95
+ }
96
+ }
97
+ });
98
+ });
99
+
100
+ return mergedSchema;
101
+ }
102
+
103
+ function processSchema(schema: any, rootSchema?: any): any {
104
+ if (!schema) return schema;
105
+
106
+ const root = rootSchema || schema;
107
+
108
+ // Handle $ref
109
+ if (schema.$ref) {
110
+ const refPath = schema.$ref;
111
+ let resolvedSchema = null;
112
+ let defName = '';
113
+
114
+ // Try draft-7 style first: #/definitions/
115
+ if (refPath.startsWith('#/definitions/')) {
116
+ defName = refPath.replace('#/definitions/', '');
117
+ if (root.definitions && root.definitions[defName]) {
118
+ resolvedSchema = root.definitions[defName];
119
+ }
120
+ }
121
+ // Try 2020-12 style: #/$defs/
122
+ else if (refPath.startsWith('#/$defs/')) {
123
+ defName = refPath.replace('#/$defs/', '');
124
+ if (root.$defs && root.$defs[defName]) {
125
+ resolvedSchema = root.$defs[defName];
126
+ }
127
+ }
128
+ // Try other common patterns
129
+ else if (refPath.startsWith('#/components/schemas/')) {
130
+ defName = refPath.replace('#/components/schemas/', '');
131
+ if (root.components && root.components.schemas && root.components.schemas[defName]) {
132
+ resolvedSchema = root.components.schemas[defName];
133
+ }
134
+ }
135
+
136
+ if (resolvedSchema) {
137
+ const processedSchema = processSchema(resolvedSchema, root);
138
+ return {
139
+ ...processedSchema,
140
+ _refPath: refPath,
141
+ _refName: defName,
142
+ _originalRef: schema.$ref,
143
+ };
144
+ }
145
+
146
+ return {
147
+ type: 'string',
148
+ description: `Reference to ${refPath} (definition not found in root schema)`,
149
+ title: defName || refPath.split('/').pop(),
150
+ _refPath: refPath,
151
+ _refName: defName,
152
+ _refNotFound: true,
153
+ };
154
+ }
155
+
156
+ if (schema.allOf) {
157
+ return mergeAllOfSchemas({ ...schema, processSchema: (s: any) => processSchema(s, root) });
158
+ }
159
+
160
+ if (schema.oneOf) {
161
+ const processedVariants = schema.oneOf.map((variant: any) => {
162
+ const processedVariant = processSchema(variant, root);
163
+ return {
164
+ title: processedVariant.title || variant.title || 'Unnamed Variant',
165
+ required: processedVariant.required || variant.required || [],
166
+ properties: processedVariant.properties || {},
167
+ ...processedVariant,
168
+ };
169
+ });
170
+
171
+ const allProperties: Record<string, any> = {};
172
+ processedVariants.forEach((variant: any) => {
173
+ if (variant.properties) {
174
+ Object.assign(allProperties, variant.properties);
175
+ }
176
+ });
177
+
178
+ return {
179
+ ...schema,
180
+ type: schema.type || 'object',
181
+ properties: {
182
+ ...(schema.properties || {}),
183
+ ...allProperties,
184
+ },
185
+ variants: processedVariants,
186
+ };
187
+ }
188
+
189
+ // Process nested schemas in properties
190
+ if (schema.properties) {
191
+ const processedProperties: Record<string, any> = {};
192
+ Object.entries(schema.properties).forEach(([key, prop]: [string, any]) => {
193
+ processedProperties[key] = processSchema(prop, root);
194
+ });
195
+ schema = { ...schema, properties: processedProperties };
196
+ }
197
+
198
+ // Process array items
199
+ if (schema.type === 'array' && schema.items) {
200
+ schema = { ...schema, items: processSchema(schema.items, root) };
201
+ }
202
+
203
+ return schema;
204
+ }
205
+
206
+ // SchemaProperty component
207
+ const SchemaProperty = ({ name, details, isRequired, level, isListItem = false, expand }: SchemaPropertyProps) => {
208
+ const [isExpanded, setIsExpanded] = useState(expand);
209
+ const contentId = useRef(`prop-${name}-${level}-${Math.random().toString(36).substring(2, 7)}`).current;
210
+
211
+ useEffect(() => {
212
+ setIsExpanded(expand);
213
+ }, [expand]);
214
+
215
+ const hasNestedProperties = details.type === 'object' && details.properties && Object.keys(details.properties).length > 0;
216
+ const hasArrayItems = details.type === 'array' && details.items;
217
+ const hasArrayItemProperties =
218
+ hasArrayItems &&
219
+ ((details.items.type === 'object' && details.items.properties) ||
220
+ details.items.allOf ||
221
+ details.items.oneOf ||
222
+ details.items.$ref);
223
+ const isCollapsible = hasNestedProperties || hasArrayItemProperties;
224
+
225
+ const indentationClass = `pl-${level * 3}`;
226
+
227
+ return (
228
+ <div className={`property-container mb-1.5 border-l border-gray-100 relative ${indentationClass}`}>
229
+ <div className="flex items-start space-x-1.5">
230
+ {isCollapsible ? (
231
+ <button
232
+ type="button"
233
+ aria-expanded={isExpanded}
234
+ aria-controls={contentId}
235
+ onClick={() => setIsExpanded(!isExpanded)}
236
+ className="property-toggle text-gray-500 hover:text-gray-700 pt-0.5 focus:outline-none w-3 text-center flex-shrink-0"
237
+ >
238
+ <span className={`icon-collapsed font-mono text-xs ${isExpanded ? 'hidden' : ''}`}>&gt;</span>
239
+ <span className={`icon-expanded font-mono text-xs ${!isExpanded ? 'hidden' : ''}`}>v</span>
240
+ </button>
241
+ ) : (
242
+ <div className="w-3 h-4 flex-shrink-0" />
243
+ )}
244
+
245
+ <div className="flex-grow">
246
+ <div className="flex justify-between items-baseline">
247
+ <div>
248
+ <span className="font-semibold text-gray-800 text-sm">{name}</span>
249
+ <span className="ml-1.5 text-purple-600 font-mono text-xs">
250
+ {details.type}
251
+ {details.type === 'array' && details.items?.type ? `[${details.items.type}]` : ''}
252
+ {details.format ? `<${details.format}>` : ''}
253
+ {details._refPath && <span className="text-blue-600 ml-1">→ {details._refName || details._refPath}</span>}
254
+ {details._refNotFound && <span className="text-red-600 ml-1">❌ ref not found</span>}
255
+ {details.const !== undefined && (
256
+ <span>
257
+ constant: <code>{details.const}</code>
258
+ </span>
259
+ )}
260
+ </span>
261
+ </div>
262
+ {isRequired && <span className="text-red-600 text-xs ml-3 flex-shrink-0">required</span>}
263
+ </div>
264
+
265
+ {details.description && <p className="text-gray-500 text-xs mt-0.5">{details.description}</p>}
266
+ {details.title && details.title !== details.description && (
267
+ <p className="text-gray-500 text-xs mt-0.5 italic">Title: {details.title}</p>
268
+ )}
269
+
270
+ <div className="text-xs text-gray-500 mt-0.5 space-y-0">
271
+ {details.pattern && (
272
+ <div>
273
+ Match pattern: <code className="bg-gray-100 px-1 rounded text-gray-800 font-thin py-0.5">{details.pattern}</code>
274
+ </div>
275
+ )}
276
+ {details.minimum !== undefined && (
277
+ <div>
278
+ Minimum: <code className="bg-gray-100 px-1 rounded text-gray-800 font-thin py-0.5">{details.minimum}</code>
279
+ </div>
280
+ )}
281
+ {details.maximum !== undefined && (
282
+ <div>
283
+ Maximum: <code className="bg-gray-100 px-1 rounded text-gray-800 font-thin py-0.5">{details.maximum}</code>
284
+ </div>
285
+ )}
286
+ {details.minLength !== undefined && (
287
+ <div>
288
+ Min length: <code className="bg-gray-100 px-1 rounded text-gray-800 font-thin py-0.5">{details.minLength}</code>
289
+ </div>
290
+ )}
291
+ {details.maxLength !== undefined && (
292
+ <div>
293
+ Max length: <code className="bg-gray-100 px-1 rounded text-gray-800 font-thin py-0.5">{details.maxLength}</code>
294
+ </div>
295
+ )}
296
+ {details.enum && (
297
+ <div>
298
+ <span className="text-xs inline-block">Allowed values:</span>
299
+ {details.enum.map((val: any, idx: number) => (
300
+ <span key={idx} className="text-xs">
301
+ {' '}
302
+ <code className="bg-gray-100 px-1 rounded text-gray-800 font-thin py-0.5">{val}</code>
303
+ </span>
304
+ ))}
305
+ </div>
306
+ )}
307
+ </div>
308
+
309
+ {(hasNestedProperties || hasArrayItems) && (
310
+ <div id={contentId} className={`nested-content mt-1 ${isCollapsible && !isExpanded ? 'hidden' : ''}`}>
311
+ {hasNestedProperties &&
312
+ details.properties &&
313
+ Object.entries(details.properties).map(([nestedName, nestedDetails]: [string, any]) => (
314
+ <SchemaProperty
315
+ key={nestedName}
316
+ name={nestedName}
317
+ details={nestedDetails}
318
+ isRequired={details.required?.includes(nestedName) ?? false}
319
+ level={level + 1}
320
+ expand={expand}
321
+ />
322
+ ))}
323
+
324
+ {hasArrayItemProperties && (
325
+ <div className="mt-1 border-l border-dashed border-gray-400 pl-3 ml-1.5">
326
+ <span className="text-xs italic text-gray-500 block mb-1">Item Details:</span>
327
+ {details.items.properties &&
328
+ Object.entries(details.items.properties).map(([itemPropName, itemPropDetails]: [string, any]) => (
329
+ <SchemaProperty
330
+ key={itemPropName}
331
+ name={itemPropName}
332
+ details={itemPropDetails}
333
+ isRequired={details.items.required?.includes(itemPropName) ?? false}
334
+ level={level + 1}
335
+ isListItem={true}
336
+ expand={expand}
337
+ />
338
+ ))}
339
+ {(details.items.allOf || details.items.oneOf || details.items.$ref) && !details.items.properties && (
340
+ <div className="text-xs text-gray-500 mt-1">
341
+ Complex array item schema detected. The properties should be processed by the parent SchemaViewer.
342
+ </div>
343
+ )}
344
+ </div>
345
+ )}
346
+ </div>
347
+ )}
348
+ </div>
349
+ </div>
350
+ </div>
351
+ );
352
+ };
353
+
354
+ // Main JSONSchemaViewer component
355
+ export default function JSONSchemaViewer({
356
+ schema,
357
+ title,
358
+ maxHeight,
359
+ expand = false,
360
+ search = true,
361
+ id,
362
+ onOpenFullscreen,
363
+ }: JSONSchemaViewerProps) {
364
+ // Convert string props to booleans (MDX passes strings)
365
+ const expandBool = expand === true || expand === 'true';
366
+ const searchBool = search !== false && search !== 'false';
367
+
368
+ const [searchQuery, setSearchQuery] = useState('');
369
+ const [expandAll, setExpandAll] = useState(expandBool);
370
+ const [selectedVariantIndex, setSelectedVariantIndex] = useState(0);
371
+ const [currentMatches, setCurrentMatches] = useState<HTMLElement[]>([]);
372
+ const [currentMatchIndex, setCurrentMatchIndex] = useState(-1);
373
+ const searchInputRef = useRef<HTMLInputElement>(null);
374
+ const propertiesContainerRef = useRef<HTMLDivElement>(null);
375
+
376
+ const processedSchema = useMemo(() => processSchema(schema), [schema]);
377
+
378
+ // Handle root-level array schemas
379
+ const { displaySchema, isRootArray } = useMemo(() => {
380
+ let display = processedSchema;
381
+ let isArray = false;
382
+
383
+ if (processedSchema.type === 'array' && processedSchema.items) {
384
+ isArray = true;
385
+ if (processedSchema.items.type === 'object' && processedSchema.items.properties) {
386
+ display = {
387
+ ...processedSchema.items,
388
+ description: processedSchema.description || processedSchema.items.description,
389
+ _isRootArrayItem: true,
390
+ _rootArraySchema: processedSchema,
391
+ };
392
+ } else if (processedSchema.items.allOf || processedSchema.items.oneOf || processedSchema.items.$ref) {
393
+ display = {
394
+ ...processSchema(processedSchema.items),
395
+ description: processedSchema.description || processedSchema.items.description,
396
+ _isRootArrayItem: true,
397
+ _rootArraySchema: processedSchema,
398
+ };
399
+ }
400
+ }
401
+
402
+ return { displaySchema: display, isRootArray: isArray };
403
+ }, [processedSchema]);
404
+
405
+ const { description, properties, required = [], variants } = displaySchema;
406
+ const totalProperties = useMemo(() => countProperties(displaySchema), [displaySchema]);
407
+
408
+ // Search functionality
409
+ useEffect(() => {
410
+ if (!propertiesContainerRef.current) return;
411
+
412
+ const propertyContainers = propertiesContainerRef.current.querySelectorAll('.property-container');
413
+ const matches: HTMLElement[] = [];
414
+
415
+ if (searchQuery === '') {
416
+ // Reset search
417
+ propertyContainers.forEach((container) => {
418
+ container.classList.remove('search-match', 'search-no-match', 'search-current-match', 'search-dimmed');
419
+ const nameEl = container.querySelector('.font-semibold');
420
+ if (nameEl) {
421
+ nameEl.innerHTML = nameEl.textContent || '';
422
+ }
423
+ });
424
+ setCurrentMatches([]);
425
+ setCurrentMatchIndex(-1);
426
+ return;
427
+ }
428
+
429
+ const query = searchQuery.toLowerCase().trim();
430
+
431
+ propertyContainers.forEach((container) => {
432
+ const nameEl = container.querySelector('.font-semibold');
433
+ if (!nameEl) return;
434
+
435
+ const propName = (nameEl.textContent || '').toLowerCase();
436
+
437
+ if (propName.includes(query)) {
438
+ container.classList.add('search-match');
439
+ container.classList.remove('search-dimmed');
440
+ matches.push(container as HTMLElement);
441
+
442
+ // Highlight the search term
443
+ const regex = new RegExp(`(${query})`, 'gi');
444
+ nameEl.innerHTML = (nameEl.textContent || '').replace(regex, '<mark class="bg-yellow-200 rounded px-0.5">$1</mark>');
445
+
446
+ // Expand parent containers and remove dimming from them
447
+ let parent = container.parentElement;
448
+ while (parent && parent !== propertiesContainerRef.current) {
449
+ if (parent.classList.contains('nested-content') && parent.classList.contains('hidden')) {
450
+ const parentPropertyContainer = parent.closest('.property-container');
451
+ if (parentPropertyContainer) {
452
+ const toggleBtn = parentPropertyContainer.querySelector('.property-toggle');
453
+ if (toggleBtn && toggleBtn.getAttribute('aria-expanded') === 'false') {
454
+ (toggleBtn as HTMLButtonElement).click();
455
+ }
456
+ }
457
+ }
458
+ // Remove dimming from parent property containers so they're fully visible
459
+ if (parent.classList.contains('property-container')) {
460
+ parent.classList.remove('search-dimmed');
461
+ }
462
+ parent = parent.parentElement;
463
+ }
464
+ } else {
465
+ container.classList.remove('search-match', 'search-current-match');
466
+ container.classList.add('search-dimmed');
467
+ nameEl.innerHTML = nameEl.textContent || '';
468
+ }
469
+ });
470
+
471
+ setCurrentMatches(matches);
472
+ if (matches.length > 0) {
473
+ setCurrentMatchIndex(0);
474
+ matches[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
475
+ } else {
476
+ setCurrentMatchIndex(-1);
477
+ }
478
+ }, [searchQuery]);
479
+
480
+ // Update match highlighting
481
+ useEffect(() => {
482
+ currentMatches.forEach((match, index) => {
483
+ if (index === currentMatchIndex) {
484
+ match.classList.add('search-current-match');
485
+ } else {
486
+ match.classList.remove('search-current-match');
487
+ }
488
+ });
489
+
490
+ if (currentMatchIndex >= 0 && currentMatches[currentMatchIndex]) {
491
+ currentMatches[currentMatchIndex].scrollIntoView({ behavior: 'smooth', block: 'center' });
492
+ }
493
+ }, [currentMatchIndex, currentMatches]);
494
+
495
+ const handleExpandAll = () => {
496
+ setExpandAll(true);
497
+ };
498
+
499
+ const handleCollapseAll = () => {
500
+ setExpandAll(false);
501
+ };
502
+
503
+ const handlePrevMatch = () => {
504
+ if (currentMatchIndex > 0) {
505
+ setCurrentMatchIndex(currentMatchIndex - 1);
506
+ }
507
+ };
508
+
509
+ const handleNextMatch = () => {
510
+ if (currentMatchIndex < currentMatches.length - 1) {
511
+ setCurrentMatchIndex(currentMatchIndex + 1);
512
+ }
513
+ };
514
+
515
+ if (!schema) {
516
+ return (
517
+ <div className="flex items-center justify-center h-full text-gray-500 p-8">
518
+ <p className="text-sm">Unable to parse JSON schema</p>
519
+ </div>
520
+ );
521
+ }
522
+
523
+ const containerStyle = maxHeight
524
+ ? {
525
+ maxHeight: maxHeight.includes('px') ? maxHeight : `${maxHeight}px`,
526
+ minHeight: '15em',
527
+ }
528
+ : {};
529
+
530
+ // Use h-full when no maxHeight (SchemaExplorer context), otherwise size based on content (MDX context)
531
+ const heightClass = maxHeight ? '' : 'h-full';
532
+ const overflowClass = maxHeight ? 'overflow-hidden' : '';
533
+
534
+ return (
535
+ <div
536
+ id={id}
537
+ className={`${heightClass} ${overflowClass} flex flex-col bg-white border border-gray-100 rounded-md shadow-sm`}
538
+ style={containerStyle}
539
+ >
540
+ {/* Toolbar */}
541
+ {searchBool && (
542
+ <div className="flex-shrink-0 bg-white pt-4 px-4 mb-4 pb-3 border-b border-gray-100 shadow-sm">
543
+ <div className="flex flex-col sm:flex-row gap-3">
544
+ <div className="flex-1 relative">
545
+ <input
546
+ ref={searchInputRef}
547
+ type="text"
548
+ value={searchQuery}
549
+ onChange={(e) => setSearchQuery(e.target.value)}
550
+ placeholder="Search properties..."
551
+ className="w-full px-3 py-1.5 pr-20 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
552
+ onKeyDown={(e) => {
553
+ if (e.key === 'Enter') {
554
+ e.preventDefault();
555
+ if (e.shiftKey) {
556
+ handlePrevMatch();
557
+ } else {
558
+ handleNextMatch();
559
+ }
560
+ }
561
+ }}
562
+ />
563
+ <div className="absolute right-2 top-1/2 transform -translate-y-1/2 flex items-center gap-1">
564
+ <button
565
+ onClick={handlePrevMatch}
566
+ disabled={currentMatches.length === 0 || currentMatchIndex <= 0}
567
+ className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-30 disabled:cursor-not-allowed"
568
+ title="Previous match"
569
+ >
570
+ <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
571
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 19l-7-7 7-7" />
572
+ </svg>
573
+ </button>
574
+ <button
575
+ onClick={handleNextMatch}
576
+ disabled={currentMatches.length === 0 || currentMatchIndex >= currentMatches.length - 1}
577
+ className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-30 disabled:cursor-not-allowed"
578
+ title="Next match"
579
+ >
580
+ <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
581
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7" />
582
+ </svg>
583
+ </button>
584
+ </div>
585
+ </div>
586
+ <div className="flex items-center gap-2">
587
+ {onOpenFullscreen && (
588
+ <button
589
+ onClick={onOpenFullscreen}
590
+ className="px-3 py-1.5 text-xs font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500"
591
+ title="Open in fullscreen"
592
+ >
593
+ <svg
594
+ xmlns="http://www.w3.org/2000/svg"
595
+ className="h-3.5 w-3.5 inline-block"
596
+ fill="none"
597
+ viewBox="0 0 24 24"
598
+ stroke="currentColor"
599
+ >
600
+ <path
601
+ strokeLinecap="round"
602
+ strokeLinejoin="round"
603
+ strokeWidth={2}
604
+ d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"
605
+ />
606
+ </svg>
607
+ </button>
608
+ )}
609
+ <button
610
+ onClick={handleExpandAll}
611
+ className="px-3 py-1.5 text-xs font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500"
612
+ >
613
+ Expand All
614
+ </button>
615
+ <button
616
+ onClick={handleCollapseAll}
617
+ className="px-3 py-1.5 text-xs font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500"
618
+ >
619
+ Collapse All
620
+ </button>
621
+ <div className="text-xs text-gray-500">
622
+ {totalProperties} {totalProperties === 1 ? 'property' : 'properties'}
623
+ </div>
624
+ </div>
625
+ </div>
626
+ {searchQuery && (
627
+ <div className="mt-2 text-xs text-gray-600">
628
+ {currentMatches.length > 0
629
+ ? `${currentMatchIndex + 1} of ${currentMatches.length} ${currentMatches.length === 1 ? 'match' : 'matches'}`
630
+ : 'No properties found'}
631
+ </div>
632
+ )}
633
+ </div>
634
+ )}
635
+
636
+ {/* Content */}
637
+ <div className="flex-1 px-4 pb-4 overflow-auto">
638
+ {isRootArray && (
639
+ <div className="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-md">
640
+ <div className="flex items-center space-x-2">
641
+ <span className="text-blue-600 font-medium text-sm">Array Schema</span>
642
+ <span className="text-blue-500 font-mono text-xs">array[object]</span>
643
+ </div>
644
+ <p className="text-blue-700 text-xs mt-1">
645
+ This schema defines an array of objects. Each item in the array has the properties shown below.
646
+ </p>
647
+ </div>
648
+ )}
649
+ {description && <p className="text-gray-600 text-xs mb-5">{description}</p>}
650
+
651
+ {variants && (
652
+ <div className="mb-4">
653
+ <div className="flex items-center space-x-2">
654
+ <span className="text-sm text-gray-600">(one of)</span>
655
+ <select
656
+ value={selectedVariantIndex}
657
+ onChange={(e) => setSelectedVariantIndex(parseInt(e.target.value))}
658
+ className="form-select text-sm border-gray-300 rounded-md shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
659
+ >
660
+ {variants.map((variant: any, index: number) => (
661
+ <option key={index} value={index}>
662
+ {variant.title}
663
+ </option>
664
+ ))}
665
+ </select>
666
+ </div>
667
+ </div>
668
+ )}
669
+
670
+ {properties ? (
671
+ <div ref={propertiesContainerRef}>
672
+ {Object.entries(properties).map(([name, details]: [string, any]) => (
673
+ <SchemaProperty
674
+ key={name}
675
+ name={name}
676
+ details={details}
677
+ isRequired={
678
+ variants ? variants[selectedVariantIndex]?.required?.includes(name) || false : required.includes(name)
679
+ }
680
+ level={0}
681
+ expand={expandAll}
682
+ />
683
+ ))}
684
+ </div>
685
+ ) : !isRootArray ? (
686
+ <p className="text-gray-500 text-sm">Schema does not contain any properties.</p>
687
+ ) : (
688
+ <div className="text-center py-8">
689
+ <div className="text-gray-500 text-sm">
690
+ <p>
691
+ This array contains items of type:{' '}
692
+ <span className="font-mono text-blue-600">{processedSchema.items?.type || 'unknown'}</span>
693
+ </p>
694
+ {processedSchema.items?.description && (
695
+ <p className="text-xs mt-2 text-gray-600">{processedSchema.items.description}</p>
696
+ )}
697
+ </div>
698
+ </div>
699
+ )}
700
+
701
+ {searchQuery && currentMatches.length === 0 && (
702
+ <div className="text-center py-8">
703
+ <div className="text-gray-400 text-sm">
704
+ <svg className="mx-auto h-12 w-12 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
705
+ <path
706
+ strokeLinecap="round"
707
+ strokeLinejoin="round"
708
+ strokeWidth="2"
709
+ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
710
+ />
711
+ </svg>
712
+ <p>No properties match your search</p>
713
+ <p className="text-xs mt-1">Try a different search term or clear the search to see all properties</p>
714
+ </div>
715
+ </div>
716
+ )}
717
+ </div>
718
+
719
+ <style>{`
720
+ .search-dimmed {
721
+ opacity: 0.4;
722
+ transition: opacity 0.2s ease;
723
+ }
724
+
725
+ .search-match {
726
+ opacity: 1;
727
+ transition: opacity 0.2s ease;
728
+ }
729
+
730
+ .search-current-match {
731
+ background-color: rgba(59, 130, 246, 0.1);
732
+ border-left: 3px solid #3b82f6;
733
+ padding-left: 8px;
734
+ margin-left: -11px;
735
+ transition: all 0.2s ease;
736
+ }
737
+ `}</style>
738
+ </div>
739
+ );
740
+ }