@eventcatalog/core 4.10.14 → 4.11.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.
- 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-ONDFEA5R.js → chunk-DWC5GRFX.js} +1 -1
- package/dist/{chunk-LITOQH56.js → chunk-H3324DNT.js} +1 -1
- package/dist/{chunk-V2SKOQYF.js → chunk-SW7SJZIX.js} +1 -1
- package/dist/{chunk-HERAHLM2.js → chunk-W4LDIMUI.js} +1 -1
- package/dist/{chunk-LUW6RDA7.js → chunk-XJOKK5ZO.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/docs/api/02-config.md +6 -6
- package/dist/docs/development/ask-your-architecture/02-eventcatalog-assistant/02-configuration.md +18 -11
- package/dist/docs/development/components/components/13-mermaid-file-loader.md +12 -0
- package/dist/docs/development/components/diagram-syntax/06-mermaid.md +45 -6
- package/dist/docs/development/customization/05-resource-sidebar.md +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/astro.config.mjs +2 -1
- package/eventcatalog/public/icons/languages/android.svg +1 -0
- package/eventcatalog/public/icons/languages/angular.svg +1 -0
- package/eventcatalog/public/icons/languages/apple.svg +1 -0
- package/eventcatalog/public/icons/languages/arduino.svg +1 -0
- package/eventcatalog/public/icons/languages/bash.svg +1 -0
- package/eventcatalog/public/icons/languages/bun.svg +1 -0
- package/eventcatalog/public/icons/languages/c.svg +1 -0
- package/eventcatalog/public/icons/languages/clojure.svg +1 -0
- package/eventcatalog/public/icons/languages/cpp.svg +1 -0
- package/eventcatalog/public/icons/languages/crystal.svg +1 -0
- package/eventcatalog/public/icons/languages/csharp.svg +1 -0
- package/eventcatalog/public/icons/languages/css.svg +1 -0
- package/eventcatalog/public/icons/languages/curl.svg +1 -0
- package/eventcatalog/public/icons/languages/dart.svg +1 -0
- package/eventcatalog/public/icons/languages/deno.svg +1 -0
- package/eventcatalog/public/icons/languages/docker.svg +1 -0
- package/eventcatalog/public/icons/languages/elm.svg +1 -0
- package/eventcatalog/public/icons/languages/erlang.svg +1 -0
- package/eventcatalog/public/icons/languages/flutter.svg +1 -0
- package/eventcatalog/public/icons/languages/fortran.svg +1 -0
- package/eventcatalog/public/icons/languages/fsharp.svg +1 -0
- package/eventcatalog/public/icons/languages/git.svg +1 -0
- package/eventcatalog/public/icons/languages/gradle.svg +1 -0
- package/eventcatalog/public/icons/languages/graphql.svg +1 -0
- package/eventcatalog/public/icons/languages/grpc.svg +1 -0
- package/eventcatalog/public/icons/languages/haskell.svg +1 -0
- package/eventcatalog/public/icons/languages/hasura.svg +1 -0
- package/eventcatalog/public/icons/languages/html.svg +1 -0
- package/eventcatalog/public/icons/languages/httpie.svg +1 -0
- package/eventcatalog/public/icons/languages/javascript.svg +1 -0
- package/eventcatalog/public/icons/languages/json-schema.svg +1 -0
- package/eventcatalog/public/icons/languages/json.svg +1 -0
- package/eventcatalog/public/icons/languages/julia.svg +1 -0
- package/eventcatalog/public/icons/languages/jupyter.svg +1 -0
- package/eventcatalog/public/icons/languages/kafka.svg +1 -0
- package/eventcatalog/public/icons/languages/kotlin.svg +1 -0
- package/eventcatalog/public/icons/languages/lua.svg +1 -0
- package/eventcatalog/public/icons/languages/markdown.svg +1 -0
- package/eventcatalog/public/icons/languages/maven.svg +1 -0
- package/eventcatalog/public/icons/languages/mongodb.svg +1 -0
- package/eventcatalog/public/icons/languages/mysql.svg +1 -0
- package/eventcatalog/public/icons/languages/nginx.svg +1 -0
- package/eventcatalog/public/icons/languages/nim.svg +1 -0
- package/eventcatalog/public/icons/languages/ocaml.svg +1 -0
- package/eventcatalog/public/icons/languages/perl.svg +1 -0
- package/eventcatalog/public/icons/languages/php.svg +1 -0
- package/eventcatalog/public/icons/languages/postgresql.svg +1 -0
- package/eventcatalog/public/icons/languages/prisma.svg +1 -0
- package/eventcatalog/public/icons/languages/purescript.svg +1 -0
- package/eventcatalog/public/icons/languages/python.svg +1 -0
- package/eventcatalog/public/icons/languages/r.svg +1 -0
- package/eventcatalog/public/icons/languages/react.svg +1 -0
- package/eventcatalog/public/icons/languages/reasonml.svg +1 -0
- package/eventcatalog/public/icons/languages/redis.svg +1 -0
- package/eventcatalog/public/icons/languages/ruby.svg +1 -0
- package/eventcatalog/public/icons/languages/rust.svg +1 -0
- package/eventcatalog/public/icons/languages/sass.svg +1 -0
- package/eventcatalog/public/icons/languages/scala.svg +1 -0
- package/eventcatalog/public/icons/languages/solidity.svg +1 -0
- package/eventcatalog/public/icons/languages/spring.svg +1 -0
- package/eventcatalog/public/icons/languages/sqlite.svg +1 -0
- package/eventcatalog/public/icons/languages/svelte.svg +1 -0
- package/eventcatalog/public/icons/languages/swagger.svg +1 -0
- package/eventcatalog/public/icons/languages/swift.svg +1 -0
- package/eventcatalog/public/icons/languages/tailwind.svg +1 -0
- package/eventcatalog/public/icons/languages/terraform.svg +1 -0
- package/eventcatalog/public/icons/languages/toml.svg +1 -0
- package/eventcatalog/public/icons/languages/typescript.svg +1 -0
- package/eventcatalog/public/icons/languages/unity.svg +1 -0
- package/eventcatalog/public/icons/languages/vim.svg +1 -0
- package/eventcatalog/public/icons/languages/vue.svg +1 -0
- package/eventcatalog/public/icons/languages/webassembly.svg +1 -0
- package/eventcatalog/public/icons/languages/yaml.svg +1 -0
- package/eventcatalog/public/icons/languages/zig.svg +1 -0
- package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.astro +12 -0
- package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.tsx +176 -0
- package/eventcatalog/src/components/MDX/CodeGroup/MarkdownCodeGroup.tsx +22 -0
- package/eventcatalog/src/components/MDX/CodeGroup/code-group-icons.ts +345 -0
- package/eventcatalog/src/components/MDX/CodeGroup/code-group.css +22 -0
- package/eventcatalog/src/components/MDX/CodeGroup/code-group.spec.tsx +89 -0
- package/eventcatalog/src/components/MDX/CodeGroup/parse-panels.ts +11 -0
- package/eventcatalog/src/components/MDX/Columns/Column.astro +10 -0
- package/eventcatalog/src/components/MDX/Columns/Columns.astro +14 -0
- package/eventcatalog/src/components/MDX/Columns/MarkdownColumns.tsx +37 -0
- package/eventcatalog/src/components/MDX/Columns/columns.spec.tsx +66 -0
- package/eventcatalog/src/components/MDX/Columns/layout.ts +16 -0
- package/eventcatalog/src/components/MDX/components.tsx +6 -0
- package/eventcatalog/src/components/SchemaExplorer/ApiContentViewer.tsx +1 -1
- package/eventcatalog/src/components/SchemaExplorer/ExamplesViewer.tsx +151 -114
- package/eventcatalog/src/components/SchemaExplorer/JSONSchemaViewer.test.tsx +114 -0
- package/eventcatalog/src/components/SchemaExplorer/JSONSchemaViewer.tsx +28 -9
- package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +510 -248
- package/eventcatalog/src/components/SchemaExplorer/SchemaFlows.tsx +76 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaGraph.tsx +47 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaPageViewer.tsx +11 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaUsage.tsx +17 -0
- package/eventcatalog/src/components/SchemaExplorer/types.ts +48 -3
- package/eventcatalog/src/components/SchemaExplorer/utils.ts +28 -1
- package/eventcatalog/src/content.config.ts +28 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/examples/[...filename].astro +38 -7
- package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/index.astro +144 -37
- package/eventcatalog/src/pages/schemas/explorer/index.astro +5 -0
- package/eventcatalog/src/remark-plugins/code-group.ts +39 -0
- package/eventcatalog/src/remark-plugins/markdown-code-group.ts +59 -0
- package/eventcatalog/src/remark-plugins/markdown-columns.ts +66 -0
- package/eventcatalog/src/utils/collections/examples.ts +133 -41
- package/eventcatalog/src/utils/collections/flows.ts +43 -0
- package/eventcatalog/src/utils/collections/glob-loader.ts +2 -2
- package/eventcatalog/src/utils/collections/messages.ts +2 -2
- package/eventcatalog/src/utils/collections/schemas.ts +15 -0
- package/eventcatalog/src/utils/schema-explorer.ts +73 -7
- package/eventcatalog/src/utils/schema-usage-graph.ts +92 -0
- package/package.json +8 -8
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from 'react';
|
|
2
|
+
import { getColumnsLayout, columnsClass, gridClass, columnClass, stickyClass } from './layout';
|
|
3
|
+
|
|
4
|
+
export function MarkdownColumns({
|
|
5
|
+
children,
|
|
6
|
+
cols = 2,
|
|
7
|
+
ratio,
|
|
8
|
+
className = '',
|
|
9
|
+
}: {
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
cols?: number;
|
|
12
|
+
ratio?: string;
|
|
13
|
+
className?: string;
|
|
14
|
+
}) {
|
|
15
|
+
const { template } = getColumnsLayout(cols, ratio);
|
|
16
|
+
return (
|
|
17
|
+
<div className={`${columnsClass} ${className}`} style={{ '--ec-columns-template': template } as CSSProperties}>
|
|
18
|
+
<div className={gridClass}>{children}</div>
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function MarkdownColumn({
|
|
24
|
+
children,
|
|
25
|
+
sticky = false,
|
|
26
|
+
className = '',
|
|
27
|
+
}: {
|
|
28
|
+
children?: ReactNode;
|
|
29
|
+
sticky?: boolean;
|
|
30
|
+
className?: string;
|
|
31
|
+
}) {
|
|
32
|
+
return (
|
|
33
|
+
<div className={`${columnClass} ${sticky ? stickyClass : ''} ${className}`} data-sticky={String(sticky)}>
|
|
34
|
+
{children}
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
3
|
+
import ReactMarkdown from 'react-markdown';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
import * as runtime from 'react/jsx-runtime';
|
|
7
|
+
import { getColumnsLayout } from './layout';
|
|
8
|
+
import { MarkdownColumns, MarkdownColumn } from './MarkdownColumns';
|
|
9
|
+
import { remarkMarkdownColumns } from '../../../remark-plugins/markdown-columns';
|
|
10
|
+
import { remarkMarkdownCodeGroup } from '../../../remark-plugins/markdown-code-group';
|
|
11
|
+
|
|
12
|
+
const source =
|
|
13
|
+
'<Columns cols={2} ratio="2:1">\n <Column>\n\n## Publish an order\n\nSome **explanation**.\n\n </Column>\n <Column sticky>\n\n<CodeGroup>\n\n```json OrderCreated\n{"id":"123"}\n```\n\n```js JavaScript\npublish();\n```\n\n</CodeGroup>\n\n </Column>\n</Columns>';
|
|
14
|
+
|
|
15
|
+
describe('Columns', () => {
|
|
16
|
+
it('defaults to two columns and supports one through four', () => {
|
|
17
|
+
expect(getColumnsLayout().template).toBe('repeat(2, minmax(0, 1fr))');
|
|
18
|
+
for (const count of [1, 2, 3, 4]) expect(getColumnsLayout(count).count).toBe(count);
|
|
19
|
+
});
|
|
20
|
+
it('uses valid ratios and safely falls back for invalid options', () => {
|
|
21
|
+
expect(getColumnsLayout(2, '2:1').template).toBe('minmax(0, 2fr) minmax(0, 1fr)');
|
|
22
|
+
for (const ratio of ['0:1', '-1:2', 'NaN:1', '1:2:3', '1fr:2fr', 'Infinity:1']) {
|
|
23
|
+
expect(getColumnsLayout(2, ratio).template).toBe('repeat(2, minmax(0, 1fr))');
|
|
24
|
+
}
|
|
25
|
+
for (const count of [0, 5, 1.5, NaN]) expect(getColumnsLayout(count).count).toBe(2);
|
|
26
|
+
});
|
|
27
|
+
it('renders the same API as real MDX with rich content and nested components', async () => {
|
|
28
|
+
const require = createRequire(import.meta.url);
|
|
29
|
+
const mdxRequire = createRequire(require.resolve('@astrojs/mdx'));
|
|
30
|
+
const { evaluate } = await import(pathToFileURL(mdxRequire.resolve('@mdx-js/mdx')).href);
|
|
31
|
+
const result = await evaluate(source, runtime);
|
|
32
|
+
const html = renderToStaticMarkup(
|
|
33
|
+
runtime.jsx(result.default, {
|
|
34
|
+
components: {
|
|
35
|
+
Columns: MarkdownColumns,
|
|
36
|
+
Column: MarkdownColumn,
|
|
37
|
+
CodeGroup: 'section',
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
);
|
|
41
|
+
expect(html).toContain('--ec-columns-template:minmax(0, 2fr) minmax(0, 1fr)');
|
|
42
|
+
expect(html).toContain('data-sticky="true"');
|
|
43
|
+
expect(html).toContain('<strong>explanation</strong>');
|
|
44
|
+
expect(html).toContain('language-json');
|
|
45
|
+
});
|
|
46
|
+
it('recognizes adjacent layout tags in Markdown and lets CodeGroup transform within columns', () => {
|
|
47
|
+
const html = renderToStaticMarkup(
|
|
48
|
+
<ReactMarkdown remarkPlugins={[remarkMarkdownColumns, remarkMarkdownCodeGroup]}>{source}</ReactMarkdown>
|
|
49
|
+
);
|
|
50
|
+
expect(html).toContain('data-columns="true"');
|
|
51
|
+
expect(html.match(/data-column="true"/g)).toHaveLength(2);
|
|
52
|
+
expect(html).toContain('data-cols="2"');
|
|
53
|
+
expect(html).toContain('data-ratio="2:1"');
|
|
54
|
+
expect(html).toContain('data-sticky="true"');
|
|
55
|
+
expect(html).toContain('data-code-panel="OrderCreated"');
|
|
56
|
+
expect(html).toContain('<strong>explanation</strong>');
|
|
57
|
+
expect(html).not.toContain('<Column');
|
|
58
|
+
});
|
|
59
|
+
it('preserves nested column layouts and leaves unrelated HTML escaped', () => {
|
|
60
|
+
const nested =
|
|
61
|
+
'<Columns>\n\n<Column>\n\n<Columns cols={1}>\n\n<Column>\n\nInner\n\n</Column>\n\n</Columns>\n\n</Column>\n\n</Columns>\n\n<script>alert(1)</script>';
|
|
62
|
+
const html = renderToStaticMarkup(<ReactMarkdown remarkPlugins={[remarkMarkdownColumns]}>{nested}</ReactMarkdown>);
|
|
63
|
+
expect(html.match(/data-columns="true"/g)).toHaveLength(2);
|
|
64
|
+
expect(html).not.toContain('<script>');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function getColumnsLayout(cols: number = 2, ratio?: string) {
|
|
2
|
+
const count = Number.isInteger(cols) && cols >= 1 && cols <= 4 ? cols : 2;
|
|
3
|
+
const weights = ratio?.split(':').map(Number);
|
|
4
|
+
const validRatio = weights?.length === count && weights.every((weight) => Number.isFinite(weight) && weight > 0);
|
|
5
|
+
return {
|
|
6
|
+
count,
|
|
7
|
+
template: validRatio ? weights.map((weight) => `minmax(0, ${weight}fr)`).join(' ') : `repeat(${count}, minmax(0, 1fr))`,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const columnsClass = 'ec-columns @container/ec-columns my-6';
|
|
12
|
+
export const gridClass =
|
|
13
|
+
'ec-columns-grid grid grid-cols-1 items-start gap-8 [&>*]:min-w-0 @min-[640px]/ec-columns:grid-cols-[var(--ec-columns-template)]';
|
|
14
|
+
export const columnClass = 'ec-column min-w-0 [&>:first-child]:mt-0 [&>:last-child]:mb-0';
|
|
15
|
+
export const stickyClass =
|
|
16
|
+
'@min-[640px]/ec-columns:sticky @min-[640px]/ec-columns:top-[var(--ec-column-sticky-top,5rem)] print:static';
|
|
@@ -22,6 +22,9 @@ import ResourceGroupTable from '@components/MDX/ResourceGroupTable/ResourceGroup
|
|
|
22
22
|
import EntityPropertiesTable from '@components/MDX/EntityPropertiesTable/EntityPropertiesTable.astro';
|
|
23
23
|
import Tabs from '@components/MDX/Tabs/Tabs.astro';
|
|
24
24
|
import TabItem from '@components/MDX/Tabs/TabItem.astro';
|
|
25
|
+
import CodeGroup from '@components/MDX/CodeGroup/CodeGroup.astro';
|
|
26
|
+
import Columns from '@components/MDX/Columns/Columns.astro';
|
|
27
|
+
import Column from '@components/MDX/Columns/Column.astro';
|
|
25
28
|
import ResourceLink from '@components/MDX/ResourceLink/ResourceLink.astro';
|
|
26
29
|
import ResourceRef from '@components/MDX/ResourceRef/ResourceRef.astro';
|
|
27
30
|
import Link from '@components/MDX/Link/Link.astro';
|
|
@@ -85,6 +88,9 @@ const components = (props: any) => {
|
|
|
85
88
|
Step,
|
|
86
89
|
Steps,
|
|
87
90
|
TabItem,
|
|
91
|
+
CodeGroup,
|
|
92
|
+
Columns,
|
|
93
|
+
Column,
|
|
88
94
|
Tabs,
|
|
89
95
|
Tile,
|
|
90
96
|
Tiles,
|
|
@@ -134,7 +134,7 @@ export default function ApiContentViewer({ message, onCopy, copiedId, apiAccessE
|
|
|
134
134
|
className="rounded-md border bg-[rgb(var(--ec-content-hover)/0.45)] px-2.5 py-1 text-xs font-medium text-[rgb(var(--ec-page-text-muted))]"
|
|
135
135
|
style={{ borderColor: boxBorderColor }}
|
|
136
136
|
>
|
|
137
|
-
|
|
137
|
+
text/plain
|
|
138
138
|
</code>
|
|
139
139
|
</div>
|
|
140
140
|
<div className="flex items-center justify-between gap-4 border-t px-4 py-3" style={{ borderColor: dividerColor }}>
|
|
@@ -1,146 +1,183 @@
|
|
|
1
|
-
import { useState,
|
|
2
|
-
import
|
|
1
|
+
import { createContext, useContext, useState, type ReactNode } from 'react';
|
|
2
|
+
import ReactMarkdown from 'react-markdown';
|
|
3
|
+
import remarkGfm from 'remark-gfm';
|
|
4
|
+
import { CheckIcon, ClipboardDocumentIcon } from '@heroicons/react/24/outline';
|
|
3
5
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
4
6
|
import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
|
|
5
7
|
import { oneLight } from 'react-syntax-highlighter/dist/cjs/styles/prism';
|
|
6
|
-
import { getLanguageForHighlight } from './utils';
|
|
7
8
|
import { copyToClipboard } from './utils';
|
|
8
9
|
import type { MessageExample } from './types';
|
|
9
10
|
import { useDarkMode } from './useDarkMode';
|
|
11
|
+
import { remarkMarkdownCodeGroup } from '../../remark-plugins/markdown-code-group';
|
|
12
|
+
import { MarkdownCodeGroup } from '../MDX/CodeGroup/MarkdownCodeGroup';
|
|
13
|
+
import { MarkdownColumns, MarkdownColumn } from '../MDX/Columns/MarkdownColumns';
|
|
14
|
+
import { remarkMarkdownColumns } from '../../remark-plugins/markdown-columns';
|
|
15
|
+
import { getLanguageForHighlight } from './utils';
|
|
10
16
|
|
|
11
17
|
interface ExamplesViewerProps {
|
|
12
18
|
examples: MessageExample[];
|
|
13
19
|
}
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
const PROSE_CLASS = [
|
|
22
|
+
'prose prose-sm max-w-none',
|
|
23
|
+
'prose-headings:text-[rgb(var(--ec-page-text))] prose-h1:mt-0 prose-h1:text-4xl prose-h1:font-bold prose-h1:tracking-tight',
|
|
24
|
+
'prose-p:text-[rgb(var(--ec-page-text-muted))] prose-li:text-[rgb(var(--ec-page-text-muted))] prose-strong:text-[rgb(var(--ec-page-text))]',
|
|
25
|
+
'prose-a:text-[rgb(var(--ec-accent))] prose-table:text-[rgb(var(--ec-page-text-muted))] prose-th:text-[rgb(var(--ec-page-text))]',
|
|
26
|
+
'prose-blockquote:border-[rgb(var(--ec-page-border))] prose-blockquote:text-[rgb(var(--ec-page-text-muted))] prose-hr:border-[rgb(var(--ec-page-border))]',
|
|
27
|
+
].join(' ');
|
|
19
28
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}, [examples]);
|
|
29
|
+
const getCodeText = (children: ReactNode): string =>
|
|
30
|
+
Array.isArray(children) ? children.map(getCodeText).join('') : typeof children === 'string' ? children : '';
|
|
23
31
|
|
|
24
|
-
|
|
32
|
+
/** True while rendering inside a CodeGroup panel, where the group supplies the header and copy button. */
|
|
33
|
+
const CodeGroupPanelContext = createContext(false);
|
|
34
|
+
|
|
35
|
+
/** Fenced code block rendered as the same themed code panel used across the schema page. */
|
|
36
|
+
function CodeBlock({ language, code }: { language?: string; code: string }) {
|
|
37
|
+
const isDarkMode = useDarkMode();
|
|
38
|
+
const [isCopied, setIsCopied] = useState(false);
|
|
39
|
+
const inCodeGroup = useContext(CodeGroupPanelContext);
|
|
40
|
+
const content = code.replace(/\s+$/, '');
|
|
41
|
+
const highlighter = (
|
|
42
|
+
<SyntaxHighlighter
|
|
43
|
+
language={language || 'text'}
|
|
44
|
+
style={isDarkMode ? oneDark : oneLight}
|
|
45
|
+
customStyle={{
|
|
46
|
+
margin: 0,
|
|
47
|
+
padding: '1rem 1.25rem',
|
|
48
|
+
borderRadius: 0,
|
|
49
|
+
fontSize: '0.8125rem',
|
|
50
|
+
lineHeight: '1.65',
|
|
51
|
+
background: 'transparent',
|
|
52
|
+
}}
|
|
53
|
+
showLineNumbers={content.includes('\n')}
|
|
54
|
+
wrapLines={true}
|
|
55
|
+
wrapLongLines={true}
|
|
56
|
+
>
|
|
57
|
+
{content}
|
|
58
|
+
</SyntaxHighlighter>
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Inside a group the panel chrome (file name, language picker, copy) belongs to the group.
|
|
62
|
+
// Keep the raw source on a data attribute so the group's copy button gets clean text.
|
|
63
|
+
if (inCodeGroup) {
|
|
25
64
|
return (
|
|
26
|
-
<div className="
|
|
27
|
-
|
|
65
|
+
<div data-code={content} className="not-prose max-h-[70vh] overflow-auto">
|
|
66
|
+
{highlighter}
|
|
28
67
|
</div>
|
|
29
68
|
);
|
|
30
69
|
}
|
|
31
70
|
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const success = await copyToClipboard(content);
|
|
37
|
-
if (success) {
|
|
38
|
-
setCopiedId(id);
|
|
39
|
-
setTimeout(() => setCopiedId(null), 2000);
|
|
71
|
+
const handleCopy = async () => {
|
|
72
|
+
if (await copyToClipboard(content)) {
|
|
73
|
+
setIsCopied(true);
|
|
74
|
+
setTimeout(() => setIsCopied(false), 2000);
|
|
40
75
|
}
|
|
41
76
|
};
|
|
42
77
|
|
|
43
78
|
return (
|
|
44
|
-
<div
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
<span className="block text-[11px] text-[rgb(var(--ec-page-text-muted))] mt-0.5 line-clamp-2 leading-relaxed">
|
|
66
|
-
{ex.summary}
|
|
67
|
-
</span>
|
|
68
|
-
)}
|
|
69
|
-
</button>
|
|
70
|
-
))}
|
|
79
|
+
<div
|
|
80
|
+
data-example-code-block
|
|
81
|
+
className="not-prose my-4 overflow-hidden rounded-xl border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-code-bg))] shadow-sm"
|
|
82
|
+
>
|
|
83
|
+
<div className="flex items-center justify-between gap-3 border-b border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-content-hover)/0.45)] px-3 py-1.5">
|
|
84
|
+
<span className="rounded-md bg-[rgb(var(--ec-content-hover))] px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-[rgb(var(--ec-page-text-muted))]">
|
|
85
|
+
{language || 'text'}
|
|
86
|
+
</span>
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
onClick={handleCopy}
|
|
90
|
+
className={`inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium transition-colors ${
|
|
91
|
+
isCopied
|
|
92
|
+
? 'text-emerald-500'
|
|
93
|
+
: 'text-[rgb(var(--ec-page-text-muted))] hover:bg-[rgb(var(--ec-content-hover))] hover:text-[rgb(var(--ec-page-text))]'
|
|
94
|
+
}`}
|
|
95
|
+
title="Copy code"
|
|
96
|
+
>
|
|
97
|
+
{isCopied ? <CheckIcon className="h-3.5 w-3.5" /> : <ClipboardDocumentIcon className="h-3.5 w-3.5" />}
|
|
98
|
+
{isCopied ? 'Copied' : 'Copy'}
|
|
99
|
+
</button>
|
|
71
100
|
</div>
|
|
101
|
+
<div className="max-h-[70vh] overflow-auto">{highlighter}</div>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
72
105
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
106
|
+
const markdownComponents = {
|
|
107
|
+
div: ({ node: _node, children, ...props }: any) =>
|
|
108
|
+
props['data-columns'] ? (
|
|
109
|
+
<MarkdownColumns cols={Number(props['data-cols'])} ratio={props['data-ratio']} className={props.className}>
|
|
110
|
+
{children}
|
|
111
|
+
</MarkdownColumns>
|
|
112
|
+
) : props['data-column'] ? (
|
|
113
|
+
<MarkdownColumn sticky={props['data-sticky'] === 'true'} className={props.className}>
|
|
114
|
+
{children}
|
|
115
|
+
</MarkdownColumn>
|
|
116
|
+
) : props['data-code-group'] ? (
|
|
117
|
+
<MarkdownCodeGroup dropdown={props['data-dropdown'] === 'true'} className={props.className}>
|
|
118
|
+
{children}
|
|
119
|
+
</MarkdownCodeGroup>
|
|
120
|
+
) : props['data-code-panel'] ? (
|
|
121
|
+
<div {...props}>
|
|
122
|
+
<CodeGroupPanelContext.Provider value={true}>{children}</CodeGroupPanelContext.Provider>
|
|
123
|
+
</div>
|
|
124
|
+
) : (
|
|
125
|
+
<div {...props}>{children}</div>
|
|
126
|
+
),
|
|
127
|
+
// Fenced blocks arrive as <pre><code class="language-x">. Render the panel from `code` and drop the <pre>.
|
|
128
|
+
pre: ({ children }: { children?: ReactNode }) => <>{children}</>,
|
|
129
|
+
code: ({ className, children }: { className?: string; children?: ReactNode }) => {
|
|
130
|
+
const language = /language-(\w+)/.exec(className || '')?.[1];
|
|
131
|
+
const text = getCodeText(children);
|
|
132
|
+
if (language || text.includes('\n')) return <CodeBlock language={language} code={text} />;
|
|
133
|
+
return (
|
|
134
|
+
<code className="rounded bg-[rgb(var(--ec-content-hover)/0.6)] px-1.5 py-0.5 font-mono text-[12px] font-normal text-[rgb(var(--ec-page-text))] before:content-none after:content-none">
|
|
135
|
+
{children}
|
|
136
|
+
</code>
|
|
137
|
+
);
|
|
138
|
+
},
|
|
139
|
+
};
|
|
97
140
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
fontSize: '0.8125rem',
|
|
108
|
-
lineHeight: '1.625',
|
|
109
|
-
overflow: 'auto',
|
|
110
|
-
background: 'transparent',
|
|
111
|
-
}}
|
|
112
|
-
showLineNumbers={true}
|
|
113
|
-
wrapLines={true}
|
|
114
|
-
wrapLongLines={true}
|
|
115
|
-
>
|
|
116
|
-
{selected.content}
|
|
117
|
-
</SyntaxHighlighter>
|
|
118
|
-
</div>
|
|
119
|
-
</div>
|
|
141
|
+
/** Lists every usage example, rendering Markdown as prose and other formats as source. */
|
|
142
|
+
export default function ExamplesViewer({ examples }: ExamplesViewerProps) {
|
|
143
|
+
if (examples.length === 0) {
|
|
144
|
+
return (
|
|
145
|
+
<div className="flex h-full items-center justify-center text-[rgb(var(--ec-page-text-muted))]">
|
|
146
|
+
<p className="text-sm">No examples available</p>
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
120
150
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
151
|
+
return (
|
|
152
|
+
<div className="h-full overflow-auto pr-1">
|
|
153
|
+
<div className="divide-y divide-[rgb(var(--ec-page-border))]">
|
|
154
|
+
{examples.map((example) => (
|
|
155
|
+
<section
|
|
156
|
+
key={example.fileName}
|
|
157
|
+
id={`example-${example.fileName.replace(/[^a-zA-Z0-9]+/g, '-')}`}
|
|
158
|
+
className="w-full py-8 pr-8 first:pt-0 last:pb-0"
|
|
159
|
+
>
|
|
160
|
+
{example.summary && <p className="mb-4 text-sm text-[rgb(var(--ec-page-text-muted))]">{example.summary}</p>}
|
|
161
|
+
{example.renderMode === 'markdown' ? (
|
|
162
|
+
<div className={PROSE_CLASS}>
|
|
163
|
+
<ReactMarkdown
|
|
164
|
+
remarkPlugins={[remarkGfm, remarkMarkdownColumns, remarkMarkdownCodeGroup]}
|
|
165
|
+
components={markdownComponents}
|
|
131
166
|
>
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
</button>
|
|
167
|
+
{example.content}
|
|
168
|
+
</ReactMarkdown>
|
|
135
169
|
</div>
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
170
|
+
) : (
|
|
171
|
+
<CodeBlock language={getLanguageForHighlight(example.extension)} code={example.content} />
|
|
172
|
+
)}
|
|
173
|
+
{example.usage && (
|
|
174
|
+
<div className="mt-5">
|
|
175
|
+
<h3 className="text-xs font-semibold uppercase tracking-wider text-[rgb(var(--ec-page-text-muted))]">Usage</h3>
|
|
176
|
+
<CodeBlock language="bash" code={example.usage} />
|
|
140
177
|
</div>
|
|
141
|
-
|
|
142
|
-
</
|
|
143
|
-
)}
|
|
178
|
+
)}
|
|
179
|
+
</section>
|
|
180
|
+
))}
|
|
144
181
|
</div>
|
|
145
182
|
</div>
|
|
146
183
|
);
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act, createElement } from 'react';
|
|
3
|
+
import { createRoot, type Root } from 'react-dom/client';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import JSONSchemaViewer from './JSONSchemaViewer';
|
|
6
|
+
|
|
7
|
+
const constraintOnlyVariantSchema = {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {
|
|
10
|
+
detail: { $ref: '#/$defs/detail' },
|
|
11
|
+
},
|
|
12
|
+
$defs: {
|
|
13
|
+
detail: {
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
data: { $ref: '#/$defs/data' },
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
data: {
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
userId: { type: 'string' },
|
|
23
|
+
customerId: { type: 'string' },
|
|
24
|
+
correlationId: { type: 'string' },
|
|
25
|
+
},
|
|
26
|
+
required: ['correlationId'],
|
|
27
|
+
oneOf: [
|
|
28
|
+
{
|
|
29
|
+
title: 'Authenticated user',
|
|
30
|
+
required: ['userId'],
|
|
31
|
+
not: { required: ['customerId'] },
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
title: 'Customer account',
|
|
35
|
+
required: ['customerId'],
|
|
36
|
+
not: { required: ['userId'] },
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
describe('JSONSchemaViewer variants', () => {
|
|
44
|
+
let container: HTMLDivElement;
|
|
45
|
+
let root: Root;
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
|
49
|
+
container = document.createElement('div');
|
|
50
|
+
document.body.appendChild(container);
|
|
51
|
+
root = createRoot(container);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
act(() => root.unmount());
|
|
56
|
+
container.remove();
|
|
57
|
+
vi.unstubAllGlobals();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('renders parent properties selected by constraint-only oneOf branches', () => {
|
|
61
|
+
act(() => {
|
|
62
|
+
root.render(createElement(JSONSchemaViewer, { schema: constraintOnlyVariantSchema, expand: true, search: false }));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(container.textContent).toContain('Authenticated user');
|
|
66
|
+
expect(container.textContent).toContain('userId');
|
|
67
|
+
expect(container.textContent).not.toContain('customerId');
|
|
68
|
+
expect(container.textContent).toContain('correlationId');
|
|
69
|
+
|
|
70
|
+
const selector = container.querySelector<HTMLSelectElement>('select');
|
|
71
|
+
expect(selector).not.toBeNull();
|
|
72
|
+
|
|
73
|
+
act(() => {
|
|
74
|
+
selector!.value = '1';
|
|
75
|
+
selector!.dispatchEvent(new Event('change', { bubbles: true }));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(container.textContent).toContain('customerId');
|
|
79
|
+
expect(container.textContent).not.toContain('userId');
|
|
80
|
+
expect(container.textContent).toContain('correlationId');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('applies constraint-only oneOf branches at the schema root', () => {
|
|
84
|
+
const rootSchema = constraintOnlyVariantSchema.$defs.data;
|
|
85
|
+
|
|
86
|
+
act(() => {
|
|
87
|
+
root.render(createElement(JSONSchemaViewer, { schema: rootSchema, expand: true, search: false }));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(container.textContent).toContain('userId');
|
|
91
|
+
expect(container.textContent).not.toContain('customerId');
|
|
92
|
+
expect(container.textContent).toContain('correlationId');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('does not render array item parent properties twice when variants are present', () => {
|
|
96
|
+
const arraySchema = {
|
|
97
|
+
type: 'object',
|
|
98
|
+
properties: {
|
|
99
|
+
identities: {
|
|
100
|
+
type: 'array',
|
|
101
|
+
items: constraintOnlyVariantSchema.$defs.data,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
act(() => {
|
|
107
|
+
root.render(createElement(JSONSchemaViewer, { schema: arraySchema, expand: true, search: false }));
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(container.textContent?.match(/userId/g)).toHaveLength(1);
|
|
111
|
+
expect(container.textContent).not.toContain('customerId');
|
|
112
|
+
expect(container.textContent?.match(/correlationId/g)).toHaveLength(1);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -31,6 +31,15 @@ function formatType(type: any): string {
|
|
|
31
31
|
return type ?? '';
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
function getForbiddenVariantProperties(variant: any): string[] {
|
|
35
|
+
const forbiddenRequired = variant?.not?.required;
|
|
36
|
+
|
|
37
|
+
// `not: { required: ['field'] }` forbids that field from being present. When
|
|
38
|
+
// multiple fields are listed, JSON Schema only forbids them being present
|
|
39
|
+
// together, so none of them can safely be hidden individually.
|
|
40
|
+
return Array.isArray(forbiddenRequired) && forbiddenRequired.length === 1 ? forbiddenRequired : [];
|
|
41
|
+
}
|
|
42
|
+
|
|
34
43
|
// Helper function to count properties recursively
|
|
35
44
|
function countProperties(obj: any): number {
|
|
36
45
|
if (!obj || typeof obj !== 'object') return 0;
|
|
@@ -173,26 +182,35 @@ function processSchema(schema: any, rootSchema?: any): any {
|
|
|
173
182
|
const variantsArray = schema.oneOf || schema.anyOf;
|
|
174
183
|
const variantType = schema.oneOf ? 'oneOf' : 'anyOf';
|
|
175
184
|
|
|
185
|
+
const processedProperties = schema.properties
|
|
186
|
+
? Object.fromEntries(
|
|
187
|
+
Object.entries(schema.properties).map(([key, prop]: [string, any]) => [key, processSchema(prop, root)])
|
|
188
|
+
)
|
|
189
|
+
: {};
|
|
190
|
+
|
|
176
191
|
const processedVariants = variantsArray.map((variant: any, index: number) => {
|
|
177
192
|
const processedVariant = processSchema(variant, root);
|
|
193
|
+
const forbiddenProperties = new Set(getForbiddenVariantProperties(processedVariant));
|
|
194
|
+
const properties = Object.fromEntries(
|
|
195
|
+
Object.entries({ ...processedProperties, ...(processedVariant.properties || {}) }).filter(
|
|
196
|
+
([propertyName]) => !forbiddenProperties.has(propertyName)
|
|
197
|
+
)
|
|
198
|
+
);
|
|
199
|
+
|
|
178
200
|
return {
|
|
201
|
+
...processedVariant,
|
|
179
202
|
title: processedVariant.title || variant.title || processedVariant.$id || `Option ${index + 1}`,
|
|
180
203
|
description: processedVariant.description || variant.description,
|
|
181
|
-
required: processedVariant.required || variant.required || [],
|
|
182
|
-
properties
|
|
204
|
+
required: [...new Set([...(schema.required || []), ...(processedVariant.required || variant.required || [])])],
|
|
205
|
+
properties,
|
|
183
206
|
type: processedVariant.type || variant.type || 'object',
|
|
184
|
-
...processedVariant,
|
|
185
207
|
};
|
|
186
208
|
});
|
|
187
209
|
|
|
188
210
|
return {
|
|
189
211
|
...schema,
|
|
190
212
|
type: schema.type || 'object',
|
|
191
|
-
properties:
|
|
192
|
-
? Object.fromEntries(
|
|
193
|
-
Object.entries(schema.properties).map(([key, prop]: [string, any]) => [key, processSchema(prop, root)])
|
|
194
|
-
)
|
|
195
|
-
: {},
|
|
213
|
+
properties: processedProperties,
|
|
196
214
|
variants: processedVariants,
|
|
197
215
|
variantType,
|
|
198
216
|
};
|
|
@@ -462,6 +480,7 @@ const SchemaProperty = ({ name, details, isRequired, level, isListItem = false,
|
|
|
462
480
|
<div className="mt-1 border-l border-dashed border-[rgb(var(--ec-page-border))] pl-3 ml-1.5">
|
|
463
481
|
<span className="text-xs italic text-[rgb(var(--ec-page-text-muted))] block mb-1">Item Details:</span>
|
|
464
482
|
{details.items.properties &&
|
|
483
|
+
!details.items.variants &&
|
|
465
484
|
Object.entries(details.items.properties).map(([itemPropName, itemPropDetails]: [string, any]) => (
|
|
466
485
|
<SchemaProperty
|
|
467
486
|
key={itemPropName}
|
|
@@ -823,7 +842,7 @@ export default function JSONSchemaViewer({
|
|
|
823
842
|
// Determine which properties to display
|
|
824
843
|
const propsToDisplay =
|
|
825
844
|
variants && variants.length > 0 && variants[selectedVariantIndex]?.properties
|
|
826
|
-
?
|
|
845
|
+
? variants[selectedVariantIndex].properties
|
|
827
846
|
: properties;
|
|
828
847
|
const requiredProps = variants && variants.length > 0 ? variants[selectedVariantIndex]?.required || [] : required;
|
|
829
848
|
|