@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.
Files changed (136) 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-ONDFEA5R.js → chunk-DWC5GRFX.js} +1 -1
  6. package/dist/{chunk-LITOQH56.js → chunk-H3324DNT.js} +1 -1
  7. package/dist/{chunk-V2SKOQYF.js → chunk-SW7SJZIX.js} +1 -1
  8. package/dist/{chunk-HERAHLM2.js → chunk-W4LDIMUI.js} +1 -1
  9. package/dist/{chunk-LUW6RDA7.js → chunk-XJOKK5ZO.js} +1 -1
  10. package/dist/constants.cjs +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/docs/api/02-config.md +6 -6
  13. package/dist/docs/development/ask-your-architecture/02-eventcatalog-assistant/02-configuration.md +18 -11
  14. package/dist/docs/development/components/components/13-mermaid-file-loader.md +12 -0
  15. package/dist/docs/development/components/diagram-syntax/06-mermaid.md +45 -6
  16. package/dist/docs/development/customization/05-resource-sidebar.md +1 -1
  17. package/dist/eventcatalog.cjs +1 -1
  18. package/dist/eventcatalog.js +5 -5
  19. package/dist/generate.cjs +1 -1
  20. package/dist/generate.js +3 -3
  21. package/dist/utils/cli-logger.cjs +1 -1
  22. package/dist/utils/cli-logger.js +2 -2
  23. package/eventcatalog/astro.config.mjs +2 -1
  24. package/eventcatalog/public/icons/languages/android.svg +1 -0
  25. package/eventcatalog/public/icons/languages/angular.svg +1 -0
  26. package/eventcatalog/public/icons/languages/apple.svg +1 -0
  27. package/eventcatalog/public/icons/languages/arduino.svg +1 -0
  28. package/eventcatalog/public/icons/languages/bash.svg +1 -0
  29. package/eventcatalog/public/icons/languages/bun.svg +1 -0
  30. package/eventcatalog/public/icons/languages/c.svg +1 -0
  31. package/eventcatalog/public/icons/languages/clojure.svg +1 -0
  32. package/eventcatalog/public/icons/languages/cpp.svg +1 -0
  33. package/eventcatalog/public/icons/languages/crystal.svg +1 -0
  34. package/eventcatalog/public/icons/languages/csharp.svg +1 -0
  35. package/eventcatalog/public/icons/languages/css.svg +1 -0
  36. package/eventcatalog/public/icons/languages/curl.svg +1 -0
  37. package/eventcatalog/public/icons/languages/dart.svg +1 -0
  38. package/eventcatalog/public/icons/languages/deno.svg +1 -0
  39. package/eventcatalog/public/icons/languages/docker.svg +1 -0
  40. package/eventcatalog/public/icons/languages/elm.svg +1 -0
  41. package/eventcatalog/public/icons/languages/erlang.svg +1 -0
  42. package/eventcatalog/public/icons/languages/flutter.svg +1 -0
  43. package/eventcatalog/public/icons/languages/fortran.svg +1 -0
  44. package/eventcatalog/public/icons/languages/fsharp.svg +1 -0
  45. package/eventcatalog/public/icons/languages/git.svg +1 -0
  46. package/eventcatalog/public/icons/languages/gradle.svg +1 -0
  47. package/eventcatalog/public/icons/languages/graphql.svg +1 -0
  48. package/eventcatalog/public/icons/languages/grpc.svg +1 -0
  49. package/eventcatalog/public/icons/languages/haskell.svg +1 -0
  50. package/eventcatalog/public/icons/languages/hasura.svg +1 -0
  51. package/eventcatalog/public/icons/languages/html.svg +1 -0
  52. package/eventcatalog/public/icons/languages/httpie.svg +1 -0
  53. package/eventcatalog/public/icons/languages/javascript.svg +1 -0
  54. package/eventcatalog/public/icons/languages/json-schema.svg +1 -0
  55. package/eventcatalog/public/icons/languages/json.svg +1 -0
  56. package/eventcatalog/public/icons/languages/julia.svg +1 -0
  57. package/eventcatalog/public/icons/languages/jupyter.svg +1 -0
  58. package/eventcatalog/public/icons/languages/kafka.svg +1 -0
  59. package/eventcatalog/public/icons/languages/kotlin.svg +1 -0
  60. package/eventcatalog/public/icons/languages/lua.svg +1 -0
  61. package/eventcatalog/public/icons/languages/markdown.svg +1 -0
  62. package/eventcatalog/public/icons/languages/maven.svg +1 -0
  63. package/eventcatalog/public/icons/languages/mongodb.svg +1 -0
  64. package/eventcatalog/public/icons/languages/mysql.svg +1 -0
  65. package/eventcatalog/public/icons/languages/nginx.svg +1 -0
  66. package/eventcatalog/public/icons/languages/nim.svg +1 -0
  67. package/eventcatalog/public/icons/languages/ocaml.svg +1 -0
  68. package/eventcatalog/public/icons/languages/perl.svg +1 -0
  69. package/eventcatalog/public/icons/languages/php.svg +1 -0
  70. package/eventcatalog/public/icons/languages/postgresql.svg +1 -0
  71. package/eventcatalog/public/icons/languages/prisma.svg +1 -0
  72. package/eventcatalog/public/icons/languages/purescript.svg +1 -0
  73. package/eventcatalog/public/icons/languages/python.svg +1 -0
  74. package/eventcatalog/public/icons/languages/r.svg +1 -0
  75. package/eventcatalog/public/icons/languages/react.svg +1 -0
  76. package/eventcatalog/public/icons/languages/reasonml.svg +1 -0
  77. package/eventcatalog/public/icons/languages/redis.svg +1 -0
  78. package/eventcatalog/public/icons/languages/ruby.svg +1 -0
  79. package/eventcatalog/public/icons/languages/rust.svg +1 -0
  80. package/eventcatalog/public/icons/languages/sass.svg +1 -0
  81. package/eventcatalog/public/icons/languages/scala.svg +1 -0
  82. package/eventcatalog/public/icons/languages/solidity.svg +1 -0
  83. package/eventcatalog/public/icons/languages/spring.svg +1 -0
  84. package/eventcatalog/public/icons/languages/sqlite.svg +1 -0
  85. package/eventcatalog/public/icons/languages/svelte.svg +1 -0
  86. package/eventcatalog/public/icons/languages/swagger.svg +1 -0
  87. package/eventcatalog/public/icons/languages/swift.svg +1 -0
  88. package/eventcatalog/public/icons/languages/tailwind.svg +1 -0
  89. package/eventcatalog/public/icons/languages/terraform.svg +1 -0
  90. package/eventcatalog/public/icons/languages/toml.svg +1 -0
  91. package/eventcatalog/public/icons/languages/typescript.svg +1 -0
  92. package/eventcatalog/public/icons/languages/unity.svg +1 -0
  93. package/eventcatalog/public/icons/languages/vim.svg +1 -0
  94. package/eventcatalog/public/icons/languages/vue.svg +1 -0
  95. package/eventcatalog/public/icons/languages/webassembly.svg +1 -0
  96. package/eventcatalog/public/icons/languages/yaml.svg +1 -0
  97. package/eventcatalog/public/icons/languages/zig.svg +1 -0
  98. package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.astro +12 -0
  99. package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.tsx +176 -0
  100. package/eventcatalog/src/components/MDX/CodeGroup/MarkdownCodeGroup.tsx +22 -0
  101. package/eventcatalog/src/components/MDX/CodeGroup/code-group-icons.ts +345 -0
  102. package/eventcatalog/src/components/MDX/CodeGroup/code-group.css +22 -0
  103. package/eventcatalog/src/components/MDX/CodeGroup/code-group.spec.tsx +89 -0
  104. package/eventcatalog/src/components/MDX/CodeGroup/parse-panels.ts +11 -0
  105. package/eventcatalog/src/components/MDX/Columns/Column.astro +10 -0
  106. package/eventcatalog/src/components/MDX/Columns/Columns.astro +14 -0
  107. package/eventcatalog/src/components/MDX/Columns/MarkdownColumns.tsx +37 -0
  108. package/eventcatalog/src/components/MDX/Columns/columns.spec.tsx +66 -0
  109. package/eventcatalog/src/components/MDX/Columns/layout.ts +16 -0
  110. package/eventcatalog/src/components/MDX/components.tsx +6 -0
  111. package/eventcatalog/src/components/SchemaExplorer/ApiContentViewer.tsx +1 -1
  112. package/eventcatalog/src/components/SchemaExplorer/ExamplesViewer.tsx +151 -114
  113. package/eventcatalog/src/components/SchemaExplorer/JSONSchemaViewer.test.tsx +114 -0
  114. package/eventcatalog/src/components/SchemaExplorer/JSONSchemaViewer.tsx +28 -9
  115. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +510 -248
  116. package/eventcatalog/src/components/SchemaExplorer/SchemaFlows.tsx +76 -0
  117. package/eventcatalog/src/components/SchemaExplorer/SchemaGraph.tsx +47 -0
  118. package/eventcatalog/src/components/SchemaExplorer/SchemaPageViewer.tsx +11 -0
  119. package/eventcatalog/src/components/SchemaExplorer/SchemaUsage.tsx +17 -0
  120. package/eventcatalog/src/components/SchemaExplorer/types.ts +48 -3
  121. package/eventcatalog/src/components/SchemaExplorer/utils.ts +28 -1
  122. package/eventcatalog/src/content.config.ts +28 -0
  123. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/examples/[...filename].astro +38 -7
  124. package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/index.astro +144 -37
  125. package/eventcatalog/src/pages/schemas/explorer/index.astro +5 -0
  126. package/eventcatalog/src/remark-plugins/code-group.ts +39 -0
  127. package/eventcatalog/src/remark-plugins/markdown-code-group.ts +59 -0
  128. package/eventcatalog/src/remark-plugins/markdown-columns.ts +66 -0
  129. package/eventcatalog/src/utils/collections/examples.ts +133 -41
  130. package/eventcatalog/src/utils/collections/flows.ts +43 -0
  131. package/eventcatalog/src/utils/collections/glob-loader.ts +2 -2
  132. package/eventcatalog/src/utils/collections/messages.ts +2 -2
  133. package/eventcatalog/src/utils/collections/schemas.ts +15 -0
  134. package/eventcatalog/src/utils/schema-explorer.ts +73 -7
  135. package/eventcatalog/src/utils/schema-usage-graph.ts +92 -0
  136. package/package.json +8 -8
@@ -0,0 +1,176 @@
1
+ import { useEffect, useId, useRef, useState, type ReactNode } from 'react';
2
+ import { Listbox, ListboxButton, ListboxOptions, ListboxOption } from '@headlessui/react';
3
+ import { CheckIcon, ChevronDownIcon, ClipboardIcon } from '@heroicons/react/24/outline';
4
+ import { getCodeLanguageIconUrl, getCodeLanguageName } from './code-group-icons';
5
+ import './code-group.css';
6
+
7
+ export interface CodeGroupItem {
8
+ label: string;
9
+ language?: string;
10
+ content?: ReactNode;
11
+ html?: string;
12
+ }
13
+ const changeEvent = 'eventcatalog:code-group-change';
14
+ const focus = 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-[rgb(var(--ec-accent))]';
15
+ function LanguageIcon({ item }: { item: CodeGroupItem }) {
16
+ const url = getCodeLanguageIconUrl(item.language ?? '', item.label);
17
+ return url ? <img src={url} alt="" className="size-4 shrink-0" /> : null;
18
+ }
19
+ export default function CodeGroup({
20
+ items,
21
+ dropdown = false,
22
+ className = '',
23
+ }: {
24
+ items: CodeGroupItem[];
25
+ dropdown?: boolean;
26
+ className?: string;
27
+ }) {
28
+ const [selected, setSelected] = useState(0);
29
+ const [copied, setCopied] = useState(false);
30
+ const panels = useRef<HTMLDivElement>(null);
31
+ const tabs = useRef<(HTMLButtonElement | null)[]>([]);
32
+ const id = useId();
33
+ const active = Math.min(selected, Math.max(items.length - 1, 0));
34
+ useEffect(() => {
35
+ const sync = (event: Event) => {
36
+ const index = items.findIndex((item) => item.label === (event as CustomEvent<string>).detail);
37
+ if (index >= 0) setSelected(index);
38
+ };
39
+ document.addEventListener(changeEvent, sync);
40
+ return () => document.removeEventListener(changeEvent, sync);
41
+ }, [items]);
42
+ useEffect(() => {
43
+ setCopied(false);
44
+ }, [active]);
45
+ useEffect(() => {
46
+ if (!copied) return;
47
+ const timer = setTimeout(() => setCopied(false), 2000);
48
+ return () => clearTimeout(timer);
49
+ }, [copied]);
50
+ const select = (index: number) => {
51
+ setSelected(index);
52
+ document.dispatchEvent(new CustomEvent(changeEvent, { detail: items[index].label }));
53
+ };
54
+ const copy = async () => {
55
+ const panel = panels.current?.children[active];
56
+ const encoded = panel?.querySelector('[data-code]')?.getAttribute('data-code');
57
+ const code = encoded?.replace(/\u007f/g, '\n') ?? panel?.querySelector('pre')?.textContent ?? '';
58
+ try {
59
+ await navigator.clipboard.writeText(code);
60
+ setCopied(true);
61
+ } catch {
62
+ setCopied(false);
63
+ }
64
+ };
65
+ if (!items.length) return null;
66
+ return (
67
+ <div
68
+ className={`ec-code-group not-prose relative my-6 rounded-xl border border-[rgb(var(--ec-content-border))] bg-[rgb(var(--ec-code-bg,var(--ec-page-bg)))] ${className}`}
69
+ >
70
+ <div
71
+ data-code-controls
72
+ className="flex items-stretch justify-between gap-2 rounded-t-xl border-b border-[rgb(var(--ec-content-border))] bg-[rgb(var(--ec-content-hover))] pl-1 pr-2"
73
+ >
74
+ {dropdown ? (
75
+ <span className="min-w-0 truncate px-3 py-2.5 text-sm">{items[active].label}</span>
76
+ ) : (
77
+ <div role="tablist" aria-label="Code examples" className="flex min-w-0 overflow-x-auto">
78
+ {items.map((item, index) => (
79
+ <button
80
+ key={index}
81
+ ref={(el) => {
82
+ tabs.current[index] = el;
83
+ }}
84
+ type="button"
85
+ role="tab"
86
+ id={`${id}-tab-${index}`}
87
+ aria-controls={`${id}-panel-${index}`}
88
+ aria-selected={index === active}
89
+ tabIndex={index === active ? 0 : -1}
90
+ onClick={() => select(index)}
91
+ onKeyDown={(event) => {
92
+ const next =
93
+ event.key === 'ArrowRight'
94
+ ? (index + 1) % items.length
95
+ : event.key === 'ArrowLeft'
96
+ ? (index - 1 + items.length) % items.length
97
+ : event.key === 'Home'
98
+ ? 0
99
+ : event.key === 'End'
100
+ ? items.length - 1
101
+ : undefined;
102
+ if (next !== undefined) {
103
+ event.preventDefault();
104
+ select(next);
105
+ tabs.current[next]?.focus();
106
+ }
107
+ }}
108
+ className={`-mb-px inline-flex shrink-0 items-center gap-2 border-b-2 border-transparent px-3 py-2.5 text-[13px] font-medium text-[rgb(var(--ec-content-text-muted))] aria-selected:border-[rgb(var(--ec-accent))] aria-selected:text-[rgb(var(--ec-content-text))] ${focus}`}
109
+ >
110
+ <LanguageIcon item={item} />
111
+ {item.label}
112
+ </button>
113
+ ))}
114
+ </div>
115
+ )}
116
+ <div className="flex shrink-0 items-center gap-1 py-1">
117
+ {dropdown && (
118
+ <Listbox value={active} onChange={select}>
119
+ <div className="relative">
120
+ <ListboxButton
121
+ aria-label="Select code example"
122
+ className={`flex items-center gap-2 rounded-lg px-2 py-1 text-[13px] hover:bg-[rgb(var(--ec-page-bg))] ${focus}`}
123
+ >
124
+ <LanguageIcon item={items[active]} />
125
+ {getCodeLanguageName(items[active].language ?? '', items[active].label)}
126
+ <ChevronDownIcon className="size-3" />
127
+ </ListboxButton>
128
+ <ListboxOptions className="absolute right-0 top-full z-20 mt-2 min-w-48 rounded-xl border border-[rgb(var(--ec-content-border))] bg-[rgb(var(--ec-page-bg))] p-1 shadow-lg">
129
+ {items.map((item, index) => (
130
+ <ListboxOption
131
+ key={index}
132
+ value={index}
133
+ className="flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-[13px] data-[focus]:bg-[rgb(var(--ec-content-hover))] data-[selected]:text-[rgb(var(--ec-accent))]"
134
+ >
135
+ {({ selected }) => (
136
+ <>
137
+ <LanguageIcon item={item} />
138
+ <span className="flex-1">{getCodeLanguageName(item.language ?? '', item.label)}</span>
139
+ {selected && <CheckIcon className="size-4" />}
140
+ </>
141
+ )}
142
+ </ListboxOption>
143
+ ))}
144
+ </ListboxOptions>
145
+ </div>
146
+ </Listbox>
147
+ )}
148
+ <button
149
+ type="button"
150
+ aria-label={copied ? 'Copied!' : 'Copy code'}
151
+ onClick={copy}
152
+ className={`flex size-7 items-center justify-center rounded-md text-[rgb(var(--ec-content-text-muted))] hover:bg-[rgb(var(--ec-page-bg))] ${focus}`}
153
+ >
154
+ {copied ? <CheckIcon className="size-4 text-emerald-500" /> : <ClipboardIcon className="size-4" />}
155
+ </button>
156
+ </div>
157
+ </div>
158
+ <div ref={panels} data-code-panels className="overflow-hidden rounded-b-xl">
159
+ {items.map((item, index) => (
160
+ <div
161
+ key={index}
162
+ data-code-panel={item.label}
163
+ id={`${id}-panel-${index}`}
164
+ role={dropdown ? 'region' : 'tabpanel'}
165
+ aria-label={dropdown ? item.label : undefined}
166
+ aria-labelledby={dropdown ? undefined : `${id}-tab-${index}`}
167
+ hidden={index !== active}
168
+ tabIndex={0}
169
+ >
170
+ {item.html !== undefined ? <div dangerouslySetInnerHTML={{ __html: item.html }} /> : item.content}
171
+ </div>
172
+ ))}
173
+ </div>
174
+ </div>
175
+ );
176
+ }
@@ -0,0 +1,22 @@
1
+ import { Children, isValidElement, type ReactNode } from 'react';
2
+ import CodeGroup from './CodeGroup';
3
+
4
+ export function MarkdownCodeGroup({
5
+ children,
6
+ dropdown,
7
+ className,
8
+ }: {
9
+ children?: ReactNode;
10
+ dropdown?: boolean;
11
+ className?: string;
12
+ }) {
13
+ const items = Children.toArray(children).flatMap((child) => {
14
+ if (
15
+ !isValidElement<{ 'data-code-panel'?: string; 'data-code-lang'?: string }>(child) ||
16
+ child.props['data-code-panel'] === undefined
17
+ )
18
+ return [];
19
+ return [{ label: child.props['data-code-panel'], language: child.props['data-code-lang'], content: child }];
20
+ });
21
+ return <CodeGroup items={items} dropdown={dropdown} className={className} />;
22
+ }
@@ -0,0 +1,345 @@
1
+ import { buildUrl } from '@utils/url-builder';
2
+
3
+ /** Maps fence languages and file extensions to an icon in `public/icons/languages`. */
4
+ const LANGUAGE_ICONS: Record<string, string> = {
5
+ js: 'javascript',
6
+ javascript: 'javascript',
7
+ jsx: 'react',
8
+ mjs: 'javascript',
9
+ cjs: 'javascript',
10
+ ts: 'typescript',
11
+ typescript: 'typescript',
12
+ tsx: 'react',
13
+ node: 'nodejs',
14
+ nodejs: 'nodejs',
15
+ py: 'python',
16
+ python: 'python',
17
+ rb: 'ruby',
18
+ ruby: 'ruby',
19
+ php: 'php',
20
+ cs: 'csharp',
21
+ csharp: 'csharp',
22
+ 'c#': 'csharp',
23
+ dotnet: 'dotnet',
24
+ fsharp: 'fsharp',
25
+ vb: 'dotnet',
26
+ go: 'go',
27
+ golang: 'go',
28
+ java: 'java',
29
+ kt: 'kotlin',
30
+ kotlin: 'kotlin',
31
+ gradle: 'gradle',
32
+ swift: 'swift',
33
+ rs: 'rust',
34
+ rust: 'rust',
35
+ c: 'c',
36
+ h: 'c',
37
+ cpp: 'cpp',
38
+ cc: 'cpp',
39
+ cxx: 'cpp',
40
+ hpp: 'cpp',
41
+ 'c++': 'cpp',
42
+ dart: 'dart',
43
+ scala: 'scala',
44
+ r: 'r',
45
+ perl: 'perl',
46
+ pl: 'perl',
47
+ lua: 'lua',
48
+ hs: 'haskell',
49
+ haskell: 'haskell',
50
+ clj: 'clojure',
51
+ clojure: 'clojure',
52
+ erl: 'erlang',
53
+ erlang: 'erlang',
54
+ ml: 'ocaml',
55
+ ocaml: 'ocaml',
56
+ zig: 'zig',
57
+ deno: 'deno',
58
+ bun: 'bun',
59
+ sol: 'solidity',
60
+ solidity: 'solidity',
61
+ sh: 'bash',
62
+ bash: 'bash',
63
+ zsh: 'bash',
64
+ shell: 'bash',
65
+ console: 'bash',
66
+ curl: 'curl',
67
+ yaml: 'yaml',
68
+ yml: 'yaml',
69
+ json: 'json',
70
+ jsonc: 'json',
71
+ json5: 'json',
72
+ jsonschema: 'json-schema',
73
+ 'json-schema': 'json-schema',
74
+ toml: 'toml',
75
+ md: 'markdown',
76
+ mdx: 'markdown',
77
+ markdown: 'markdown',
78
+ html: 'html',
79
+ htm: 'html',
80
+ vue: 'vue',
81
+ svelte: 'svelte',
82
+ angular: 'angular',
83
+ css: 'css',
84
+ scss: 'sass',
85
+ less: 'css',
86
+ graphql: 'graphql',
87
+ gql: 'graphql',
88
+ sql: 'mysql',
89
+ mysql: 'mysql',
90
+ postgres: 'postgresql',
91
+ postgresql: 'postgresql',
92
+ psql: 'postgresql',
93
+ docker: 'docker',
94
+ dockerfile: 'docker',
95
+ git: 'git',
96
+ diff: 'git',
97
+ hcl: 'terraform',
98
+ tf: 'terraform',
99
+ terraform: 'terraform',
100
+ nginx: 'nginx',
101
+ kafka: 'kafka',
102
+ '.net': 'dotnet',
103
+ aspnet: 'dotnet',
104
+ 'asp.net': 'dotnet',
105
+ vbnet: 'dotnet',
106
+ 'vb.net': 'dotnet',
107
+ fs: 'fsharp',
108
+ 'f#': 'fsharp',
109
+ kts: 'kotlin',
110
+ groovy: 'gradle',
111
+ maven: 'maven',
112
+ pom: 'maven',
113
+ spring: 'spring',
114
+ jl: 'julia',
115
+ julia: 'julia',
116
+ f90: 'fortran',
117
+ fortran: 'fortran',
118
+ cr: 'crystal',
119
+ crystal: 'crystal',
120
+ nim: 'nim',
121
+ elm: 'elm',
122
+ purs: 'purescript',
123
+ purescript: 'purescript',
124
+ re: 'reasonml',
125
+ reason: 'reasonml',
126
+ reasonml: 'reasonml',
127
+ flutter: 'flutter',
128
+ grpc: 'grpc',
129
+ sqlite: 'sqlite',
130
+ mongodb: 'mongodb',
131
+ mongo: 'mongodb',
132
+ redis: 'redis',
133
+ prisma: 'prisma',
134
+ wasm: 'webassembly',
135
+ wat: 'webassembly',
136
+ webassembly: 'webassembly',
137
+ sass: 'sass',
138
+ tailwind: 'tailwind',
139
+ tailwindcss: 'tailwind',
140
+ http: 'httpie',
141
+ httpie: 'httpie',
142
+ swagger: 'swagger',
143
+ ipynb: 'jupyter',
144
+ jupyter: 'jupyter',
145
+ android: 'android',
146
+ ios: 'apple',
147
+ objc: 'apple',
148
+ objectivec: 'apple',
149
+ 'objective-c': 'apple',
150
+ ino: 'arduino',
151
+ arduino: 'arduino',
152
+ unity: 'unity',
153
+ vim: 'vim',
154
+ hasura: 'hasura',
155
+ };
156
+
157
+ /** Languages that have a display name but no icon. */
158
+ const NAME_ONLY_LANGUAGES: Record<string, string> = {
159
+ xml: 'XML',
160
+ xsd: 'XML Schema',
161
+ txt: 'Text',
162
+ text: 'Text',
163
+ plaintext: 'Text',
164
+ plain: 'Text',
165
+ ini: 'INI',
166
+ properties: 'Properties',
167
+ env: 'Env',
168
+ dotenv: 'Env',
169
+ makefile: 'Makefile',
170
+ make: 'Makefile',
171
+ cmake: 'CMake',
172
+ tex: 'LaTeX',
173
+ latex: 'LaTeX',
174
+ csv: 'CSV',
175
+ groovy: 'Groovy',
176
+ elixir: 'Elixir',
177
+ ex: 'Elixir',
178
+ exs: 'Elixir',
179
+ matlab: 'MATLAB',
180
+ ps1: 'PowerShell',
181
+ powershell: 'PowerShell',
182
+ pwsh: 'PowerShell',
183
+ bat: 'Batch',
184
+ cmd: 'Batch',
185
+ regex: 'Regex',
186
+ mermaid: 'Mermaid',
187
+ hbs: 'Handlebars',
188
+ handlebars: 'Handlebars',
189
+ ejs: 'EJS',
190
+ pug: 'Pug',
191
+ nix: 'Nix',
192
+ cobol: 'COBOL',
193
+ scheme: 'Scheme',
194
+ racket: 'Racket',
195
+ lisp: 'Lisp',
196
+ ada: 'Ada',
197
+ pascal: 'Pascal',
198
+ prolog: 'Prolog',
199
+ smalltalk: 'Smalltalk',
200
+ vhdl: 'VHDL',
201
+ verilog: 'Verilog',
202
+ asm: 'Assembly',
203
+ assembly: 'Assembly',
204
+ };
205
+
206
+ /** Formats that already have an icon elsewhere under `public/icons`. */
207
+ const SCHEMA_ICONS: Record<string, string> = {
208
+ avro: 'avro',
209
+ avsc: 'avro',
210
+ proto: 'proto',
211
+ protobuf: 'proto',
212
+ openapi: 'openapi',
213
+ asyncapi: 'asyncapi',
214
+ };
215
+
216
+ const LANGUAGE_NAMES: Record<string, string> = {
217
+ javascript: 'JavaScript',
218
+ typescript: 'TypeScript',
219
+ react: 'React',
220
+ nodejs: 'Node.js',
221
+ python: 'Python',
222
+ ruby: 'Ruby',
223
+ php: 'PHP',
224
+ csharp: 'C#',
225
+ dotnet: '.NET',
226
+ go: 'Go',
227
+ java: 'Java',
228
+ kotlin: 'Kotlin',
229
+ gradle: 'Gradle',
230
+ swift: 'Swift',
231
+ rust: 'Rust',
232
+ c: 'C',
233
+ cpp: 'C++',
234
+ dart: 'Dart',
235
+ scala: 'Scala',
236
+ r: 'R',
237
+ perl: 'Perl',
238
+ lua: 'Lua',
239
+ haskell: 'Haskell',
240
+ clojure: 'Clojure',
241
+ erlang: 'Erlang',
242
+ ocaml: 'OCaml',
243
+ zig: 'Zig',
244
+ deno: 'Deno',
245
+ bun: 'Bun',
246
+ solidity: 'Solidity',
247
+ bash: 'Shell',
248
+ curl: 'cURL',
249
+ yaml: 'YAML',
250
+ json: 'JSON',
251
+ 'json-schema': 'JSON Schema',
252
+ toml: 'TOML',
253
+ markdown: 'Markdown',
254
+ html: 'HTML',
255
+ vue: 'Vue',
256
+ svelte: 'Svelte',
257
+ angular: 'Angular',
258
+ css: 'CSS',
259
+ graphql: 'GraphQL',
260
+ mysql: 'SQL',
261
+ postgresql: 'PostgreSQL',
262
+ docker: 'Docker',
263
+ git: 'Git',
264
+ terraform: 'Terraform',
265
+ nginx: 'NGINX',
266
+ kafka: 'Kafka',
267
+ avro: 'Avro',
268
+ proto: 'Protobuf',
269
+ openapi: 'OpenAPI',
270
+ asyncapi: 'AsyncAPI',
271
+ fsharp: 'F#',
272
+ maven: 'Maven',
273
+ spring: 'Spring',
274
+ julia: 'Julia',
275
+ fortran: 'Fortran',
276
+ crystal: 'Crystal',
277
+ nim: 'Nim',
278
+ elm: 'Elm',
279
+ purescript: 'PureScript',
280
+ reasonml: 'ReasonML',
281
+ flutter: 'Flutter',
282
+ grpc: 'gRPC',
283
+ sqlite: 'SQLite',
284
+ mongodb: 'MongoDB',
285
+ redis: 'Redis',
286
+ prisma: 'Prisma',
287
+ webassembly: 'WebAssembly',
288
+ sass: 'Sass',
289
+ tailwind: 'Tailwind CSS',
290
+ httpie: 'HTTP',
291
+ swagger: 'Swagger',
292
+ jupyter: 'Jupyter',
293
+ android: 'Android',
294
+ apple: 'Apple',
295
+ arduino: 'Arduino',
296
+ unity: 'Unity',
297
+ vim: 'Vim',
298
+ hasura: 'Hasura',
299
+ };
300
+
301
+ const normalize = (value: string) => value.trim().toLowerCase();
302
+
303
+ const getExtension = (label?: string) => {
304
+ const match = label?.trim().match(/\.([a-z0-9+#]+)$/i);
305
+ return match?.[1];
306
+ };
307
+
308
+ const resolveIconKey = (language?: string, label?: string) => {
309
+ for (const candidate of [language, getExtension(label)]) {
310
+ if (!candidate) continue;
311
+ const key = normalize(candidate);
312
+ if (LANGUAGE_ICONS[key]) return LANGUAGE_ICONS[key];
313
+ if (SCHEMA_ICONS[key]) return SCHEMA_ICONS[key];
314
+ }
315
+ return undefined;
316
+ };
317
+
318
+ /**
319
+ * Human readable language name for a code panel, for example `Python` for `py`.
320
+ * Falls back to the fence language as written, or the label when there is no language.
321
+ */
322
+ export function getCodeLanguageName(language?: string, label?: string): string {
323
+ const key = resolveIconKey(language, label);
324
+ if (key && LANGUAGE_NAMES[key]) return LANGUAGE_NAMES[key];
325
+ for (const candidate of [language, getExtension(label)]) {
326
+ if (candidate && NAME_ONLY_LANGUAGES[normalize(candidate)]) return NAME_ONLY_LANGUAGES[normalize(candidate)];
327
+ }
328
+ const raw = language?.trim();
329
+ if (raw) return raw.charAt(0).toUpperCase() + raw.slice(1);
330
+ return label ?? '';
331
+ }
332
+
333
+ /**
334
+ * Resolves an icon URL for a code panel from its fence language, falling back to the file
335
+ * extension in the panel label. Returns `undefined` when there is no matching icon.
336
+ */
337
+ export function getCodeLanguageIconUrl(language?: string, label?: string): string | undefined {
338
+ for (const candidate of [language, getExtension(label)]) {
339
+ if (!candidate) continue;
340
+ const key = normalize(candidate);
341
+ if (LANGUAGE_ICONS[key]) return buildUrl(`/icons/languages/${LANGUAGE_ICONS[key]}.svg`, true);
342
+ if (SCHEMA_ICONS[key]) return buildUrl(`/icons/${SCHEMA_ICONS[key]}.svg`, true);
343
+ }
344
+ return undefined;
345
+ }
@@ -0,0 +1,22 @@
1
+ /* Overrides for markup generated by syntax highlighters, outside React's control. */
2
+ .ec-code-group [data-example-code-block] {
3
+ margin: 0;
4
+ border: 0;
5
+ border-radius: 0;
6
+ box-shadow: none;
7
+ }
8
+ .ec-code-group [data-code-panel] .expressive-code {
9
+ margin: 0;
10
+ }
11
+ .ec-code-group [data-code-panel] .frame {
12
+ border: 0;
13
+ border-radius: 0;
14
+ box-shadow: none;
15
+ }
16
+ .ec-code-group [data-code-panel] .frame > .header,
17
+ .ec-code-group [data-code-panel] .frame .copy {
18
+ display: none;
19
+ }
20
+ .ec-code-group [data-code-panel] pre {
21
+ border-radius: 0;
22
+ }
@@ -0,0 +1,89 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, describe, expect, it, vi } from 'vitest';
3
+ import { act } from 'react';
4
+ import { createRoot, type Root } from 'react-dom/client';
5
+ import CodeGroup from './CodeGroup';
6
+ import { MarkdownCodeGroup } from './MarkdownCodeGroup';
7
+ import { parseCodePanels } from './parse-panels';
8
+ import { getCodeLanguageIconUrl, getCodeLanguageName } from './code-group-icons';
9
+ (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
10
+ const roots: Root[] = [];
11
+ function render(element: React.ReactNode) {
12
+ const container = document.createElement('div');
13
+ document.body.append(container);
14
+ const root = createRoot(container);
15
+ roots.push(root);
16
+ act(() => root.render(element));
17
+ return container;
18
+ }
19
+ afterEach(() => {
20
+ act(() => roots.splice(0).forEach((root) => root.unmount()));
21
+ document.body.innerHTML = '';
22
+ });
23
+ const items = [
24
+ { label: 'hello.js', language: 'js', content: <pre>first</pre> },
25
+ { label: 'hello.py', language: 'python', content: <pre>second</pre> },
26
+ ];
27
+ describe('React CodeGroup', () => {
28
+ it('switches panels with keyboard focus and synchronizes matching groups', () => {
29
+ const a = render(<CodeGroup items={items} />);
30
+ const b = render(<CodeGroup items={items} dropdown />);
31
+ const tabs = a.querySelectorAll<HTMLButtonElement>('[role=tab]');
32
+ act(() => tabs[0].dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true })));
33
+ expect(tabs[1].getAttribute('aria-selected')).toBe('true');
34
+ expect(document.activeElement).toBe(tabs[1]);
35
+ expect(a.querySelectorAll<HTMLElement>('[role=tabpanel]')[0].hidden).toBe(true);
36
+ expect(b.querySelectorAll<HTMLElement>('[role=region]')[1].hidden).toBe(false);
37
+ expect(b.querySelector('button')?.textContent).toBe('Python');
38
+ });
39
+ it('copies the selected code', async () => {
40
+ const writeText = vi.fn().mockResolvedValue(undefined);
41
+ Object.assign(navigator, { clipboard: { writeText } });
42
+ const root = render(<CodeGroup items={items} />);
43
+ act(() => root.querySelectorAll<HTMLButtonElement>('[role=tab]')[1].click());
44
+ await act(async () => root.querySelector<HTMLButtonElement>('[aria-label="Copy code"]')!.click());
45
+ expect(writeText).toHaveBeenCalledWith('second');
46
+ expect(root.querySelector('[aria-label="Copied!"]')).not.toBeNull();
47
+ });
48
+ it('preserves highlighted Astro HTML and decodes copy newlines', async () => {
49
+ const parsed = parseCodePanels(
50
+ '<div data-code-panel="file.js" data-code-lang="js"><pre><code><span>one</span>two</code></pre><button data-code="one&#127;two"></button></div>'
51
+ );
52
+ const writeText = vi.fn().mockResolvedValue(undefined);
53
+ Object.assign(navigator, { clipboard: { writeText } });
54
+ const root = render(<CodeGroup items={parsed} />);
55
+ expect(root.querySelector('code span')?.textContent).toBe('one');
56
+ await act(async () => root.querySelector<HTMLButtonElement>('[aria-label="Copy code"]')!.click());
57
+ expect(writeText).toHaveBeenCalledWith('one\ntwo');
58
+ });
59
+ it('accepts Markdown panels and escapes labels', () => {
60
+ const root = render(
61
+ <MarkdownCodeGroup>
62
+ <div data-code-panel="<script>" data-code-lang="js">
63
+ <pre>hello</pre>
64
+ </div>
65
+ </MarkdownCodeGroup>
66
+ );
67
+ expect(root.querySelector('[role=tab]')?.textContent).toBe('<script>');
68
+ expect(root.querySelector('script')).toBeNull();
69
+ expect(root.querySelector('pre')?.textContent).toBe('hello');
70
+ });
71
+ });
72
+
73
+ describe('language names and icons', () => {
74
+ it.each([
75
+ ['cs', undefined, 'C#', '/icons/languages/csharp.svg'],
76
+ ['dotnet', undefined, '.NET', '/icons/languages/dotnet.svg'],
77
+ ['fsharp', undefined, 'F#', '/icons/languages/fsharp.svg'],
78
+ ['java', undefined, 'Java', '/icons/languages/java.svg'],
79
+ ['kts', undefined, 'Kotlin', '/icons/languages/kotlin.svg'],
80
+ [undefined, 'Main.go', 'Go', '/icons/languages/go.svg'],
81
+ ['powershell', undefined, 'PowerShell', undefined],
82
+ ['xml', undefined, 'XML', undefined],
83
+ ['avsc', undefined, 'Avro', '/icons/avro.svg'],
84
+ ['brainfuck', undefined, 'Brainfuck', undefined],
85
+ ])('maps %s / %s to %s', (language, label, name, icon) => {
86
+ expect(getCodeLanguageName(language, label)).toBe(name);
87
+ expect(getCodeLanguageIconUrl(language, label)).toBe(icon);
88
+ });
89
+ });
@@ -0,0 +1,11 @@
1
+ import { parseFragment, serialize } from 'parse5';
2
+
3
+ // Only parse the trusted HTML produced by Astro's rendered MDX slot.
4
+ export function parseCodePanels(html: string) {
5
+ return parseFragment(html).childNodes.flatMap((node) => {
6
+ if (!('attrs' in node)) return [];
7
+ const label = node.attrs.find((attr) => attr.name === 'data-code-panel')?.value;
8
+ if (label === undefined) return [];
9
+ return [{ label, language: node.attrs.find((attr) => attr.name === 'data-code-lang')?.value, html: serialize(node) }];
10
+ });
11
+ }
@@ -0,0 +1,10 @@
1
+ ---
2
+ import { columnClass, stickyClass } from './layout';
3
+ interface Props {
4
+ sticky?: boolean;
5
+ className?: string;
6
+ }
7
+ const { sticky = false, className } = Astro.props;
8
+ ---
9
+
10
+ <div class:list={[columnClass, sticky && stickyClass, className]} data-sticky={String(sticky)}><slot /></div>
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { getColumnsLayout, columnsClass, gridClass } from './layout';
3
+ interface Props {
4
+ cols?: number;
5
+ ratio?: string;
6
+ className?: string;
7
+ }
8
+ const { cols = 2, ratio, className } = Astro.props;
9
+ const { template } = getColumnsLayout(cols, ratio);
10
+ ---
11
+
12
+ <div class:list={[columnsClass, className]} style={{ '--ec-columns-template': template }}>
13
+ <div class={gridClass}><slot /></div>
14
+ </div>