@eventcatalog/core 5.0.0-beta.4 → 5.0.0-beta.6

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 (86) 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 +2 -2
  4. package/dist/analytics/log-build.js +4 -4
  5. package/dist/{chunk-TOMV5YJH.js → chunk-56CD2NH6.js} +1 -1
  6. package/dist/{chunk-M7OVJHPV.js → chunk-DE3QHVJP.js} +1 -1
  7. package/dist/{chunk-Q7FBYUCZ.js → chunk-L2IRFOE7.js} +1 -1
  8. package/dist/{chunk-TQLRG35T.js → chunk-PIYJM642.js} +1 -1
  9. package/dist/{chunk-VA77ENFQ.js → chunk-QBP3BBSV.js} +1 -1
  10. package/dist/{chunk-PL3T5TWF.js → chunk-XMPFHLK4.js} +2 -2
  11. package/dist/constants.cjs +1 -1
  12. package/dist/constants.js +1 -1
  13. package/dist/eventcatalog.cjs +2 -2
  14. package/dist/eventcatalog.js +6 -6
  15. package/dist/generate.cjs +1 -1
  16. package/dist/generate.js +3 -3
  17. package/dist/runtime-dependencies/@hocuspocus__provider.mjs +3 -0
  18. package/dist/runtime-dependencies/@hocuspocus__server.mjs +3 -0
  19. package/dist/runtime-dependencies/@mcp-b__webmcp-local-relay.mjs +3 -0
  20. package/dist/runtime-dependencies/@mcp-b__webmcp-polyfill.mjs +3 -0
  21. package/dist/runtime-dependencies/manifest.json +8 -0
  22. package/dist/runtime-dependencies/react-hotkeys-hook.mjs +3 -0
  23. package/dist/runtime-dependencies/y-protocols.mjs +3 -0
  24. package/dist/runtime-dependencies/y-protocols__awareness.mjs +3 -0
  25. package/dist/runtime-dependencies/yjs.mjs +3 -0
  26. package/dist/utils/cli-logger.cjs +1 -1
  27. package/dist/utils/cli-logger.js +2 -2
  28. package/dist/utils/license-status.cjs +1 -1
  29. package/dist/utils/license-status.js +1 -1
  30. package/eventcatalog/astro.config.mjs +2 -0
  31. package/eventcatalog/integrations/studio-server.mjs +17 -0
  32. package/eventcatalog/src/features/integrations/eventcatalog-features.ts +20 -1
  33. package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +39 -0
  34. package/eventcatalog/src/features/mcp/apps/canvas/styles.css +22 -0
  35. package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +269 -0
  36. package/eventcatalog/src/features/mcp/apps/generated/canvas.html +209 -0
  37. package/eventcatalog/src/features/mcp/apps/generated/viewer.html +2 -2
  38. package/eventcatalog/src/features/mcp/apps/shared/app-view.tsx +2 -1
  39. package/eventcatalog/src/features/mcp/mcp-apps.ts +6 -3
  40. package/eventcatalog/src/features/mcp/mcp-server.ts +17 -4
  41. package/eventcatalog/src/features/studio/agent-choreography.ts +378 -0
  42. package/eventcatalog/src/features/studio/canvas-actions.ts +287 -0
  43. package/eventcatalog/src/features/studio/canvas-doc.ts +280 -0
  44. package/eventcatalog/src/features/studio/catalog-resources.ts +152 -0
  45. package/eventcatalog/src/features/studio/catalog.ts +171 -0
  46. package/eventcatalog/src/features/studio/components/CanvasControls.tsx +153 -0
  47. package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +332 -0
  48. package/eventcatalog/src/features/studio/components/Comments.tsx +455 -0
  49. package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +217 -0
  50. package/eventcatalog/src/features/studio/components/ContainerChooser.tsx +145 -0
  51. package/eventcatalog/src/features/studio/components/JoinForm.tsx +58 -0
  52. package/eventcatalog/src/features/studio/components/LeftPanel.tsx +218 -0
  53. package/eventcatalog/src/features/studio/components/Presence.tsx +284 -0
  54. package/eventcatalog/src/features/studio/components/PropertiesPanel.tsx +75 -0
  55. package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +760 -0
  56. package/eventcatalog/src/features/studio/components/canvas-nodes.tsx +79 -0
  57. package/eventcatalog/src/features/studio/components/icons.ts +48 -0
  58. package/eventcatalog/src/features/studio/components/status.ts +22 -0
  59. package/eventcatalog/src/features/studio/edges.ts +54 -0
  60. package/eventcatalog/src/features/studio/flow-state.ts +92 -0
  61. package/eventcatalog/src/features/studio/grouping.ts +133 -0
  62. package/eventcatalog/src/features/studio/hooks/presence-sender.ts +46 -0
  63. package/eventcatalog/src/features/studio/hooks/presence-store.ts +115 -0
  64. package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +337 -0
  65. package/eventcatalog/src/features/studio/hooks/use-comments.ts +47 -0
  66. package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +99 -0
  67. package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +423 -0
  68. package/eventcatalog/src/features/studio/layout.ts +39 -0
  69. package/eventcatalog/src/features/studio/levels.ts +216 -0
  70. package/eventcatalog/src/features/studio/node-types.ts +173 -0
  71. package/eventcatalog/src/features/studio/pages/[id].astro +38 -0
  72. package/eventcatalog/src/features/studio/pages/index.astro +8 -0
  73. package/eventcatalog/src/features/studio/pages/webmcp-relay.ts +27 -0
  74. package/eventcatalog/src/features/studio/placement.ts +197 -0
  75. package/eventcatalog/src/features/studio/server/canvas-mcp.ts +475 -0
  76. package/eventcatalog/src/features/studio/server/runtime.ts +215 -0
  77. package/eventcatalog/src/features/studio/server/tool-icons.ts +30 -0
  78. package/eventcatalog/src/features/studio/studio.css +89 -0
  79. package/eventcatalog/src/features/studio/tool-sync.ts +75 -0
  80. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +9 -2
  81. package/eventcatalog/src/utils/feature.ts +4 -0
  82. package/eventcatalog/src/utils/trial.ts +1 -1
  83. package/package.json +9 -2
  84. package/eventcatalog/public/studio-bg.png +0 -0
  85. package/eventcatalog/src/components/Studio/StudioPageModal.tsx +0 -150
  86. package/eventcatalog/src/pages/studio.astro +0 -316
@@ -0,0 +1,30 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+
3
+ /** A tool icon (MCP 2025-11-25) */
4
+ export type ToolIcon = { src: string; mimeType?: string; sizes?: string[] };
5
+ type ListTools = (request: unknown, extra: unknown) => Promise<{ tools: { name: string }[] }>;
6
+
7
+ /** The SDK's request handlers (private in SDK 1.x, so checked rather than assumed) */
8
+ const getRequestHandlers = (server: McpServer): Map<string, ListTools> | undefined => {
9
+ const handlers: unknown = Reflect.get(server.server, '_requestHandlers');
10
+ return handlers instanceof Map ? handlers : undefined;
11
+ };
12
+
13
+ /**
14
+ * Adds icons to tools in tools/list (MCP 2025-11-25 tool icons, which ChatGPT shows on entrypoints). The MCP
15
+ * TypeScript SDK 1.x doesn't list tool icons, so this wraps its tools/list handler. If a new SDK changes how
16
+ * it keeps handlers, tools are listed without icons and this says so.
17
+ */
18
+ export const addToolIcons = (server: McpServer, icons: Record<string, ToolIcon[]>) => {
19
+ const handlers = getRequestHandlers(server);
20
+ const listTools = handlers?.get('tools/list');
21
+ if (!handlers || !listTools) {
22
+ console.warn('[EventCatalog MCP] Could not add tool icons: the MCP SDK no longer exposes its tools/list handler');
23
+ return false;
24
+ }
25
+ handlers.set('tools/list', async (request, extra) => {
26
+ const result = await listTools(request, extra);
27
+ return { ...result, tools: result.tools.map((tool) => (icons[tool.name] ? { ...tool, icons: icons[tool.name] } : tool)) };
28
+ });
29
+ return true;
30
+ };
@@ -0,0 +1,89 @@
1
+ /*
2
+ * The visualiser hides node handles (it is read only), here they are the connection points,
3
+ * so show them on hover, on selection and while connecting.
4
+ */
5
+ .studio-canvas .react-flow__handle {
6
+ opacity: 0 !important;
7
+ width: 11px;
8
+ height: 11px;
9
+ background: rgb(var(--ec-card-bg));
10
+ border: 2px solid rgb(var(--ec-accent));
11
+ transition: opacity 0.15s ease;
12
+ z-index: 30;
13
+ }
14
+
15
+ .studio-canvas .react-flow__node:hover .react-flow__handle,
16
+ .studio-canvas .react-flow__node.selected .react-flow__handle,
17
+ .studio-canvas .react-flow__pane.connecting ~ * .react-flow__handle,
18
+ .studio-canvas .react-flow__handle.connectingto {
19
+ opacity: 1 !important;
20
+ }
21
+
22
+ .studio-canvas .react-flow__handle:hover,
23
+ .studio-canvas .react-flow__handle.connectingto {
24
+ background: rgb(var(--ec-accent));
25
+ }
26
+
27
+ .studio-canvas .react-flow__handle.connectingto.valid {
28
+ background: rgb(var(--ec-badge-color-green-text));
29
+ border-color: rgb(var(--ec-badge-color-green-text));
30
+ }
31
+
32
+ .studio-canvas .react-flow__edge.selected .react-flow__edge-path,
33
+ .studio-canvas .react-flow__edge:hover .react-flow__edge-path {
34
+ stroke: rgb(var(--ec-accent)) !important;
35
+ stroke-width: 2px !important;
36
+ }
37
+
38
+ /* Comment mode (C): click anywhere to drop a comment */
39
+ .studio-canvas.comment-mode .react-flow__pane,
40
+ .studio-canvas.comment-mode .react-flow__node {
41
+ cursor: crosshair !important;
42
+ }
43
+
44
+ /* After a layout or a level change, nodes glide to their new place */
45
+ .studio-canvas.is-layout-transition .react-flow__node {
46
+ transition: transform 450ms ease;
47
+ }
48
+
49
+ /* Edges are drawn where the nodes end up, so they wait for the glide to finish */
50
+ .studio-canvas .react-flow__edges {
51
+ transition: opacity 200ms ease;
52
+ }
53
+ .studio-canvas.is-layout-transition .react-flow__edges {
54
+ opacity: 0;
55
+ transition: none;
56
+ }
57
+
58
+ /* Nodes pop in when they're added (by anyone, people or agents) */
59
+ @keyframes studio-node-in {
60
+ from {
61
+ opacity: 0;
62
+ scale: 0.85;
63
+ }
64
+ to {
65
+ opacity: 1;
66
+ scale: 1;
67
+ }
68
+ }
69
+ .studio-canvas .react-flow__node {
70
+ animation: studio-node-in 220ms ease-out;
71
+ }
72
+
73
+ /* No React Flow attribution on the canvas */
74
+ .studio-canvas .react-flow__attribution {
75
+ display: none;
76
+ }
77
+
78
+ /*
79
+ * Containers let the pointer through their body to the canvas (so dragging inside one draws a selection box),
80
+ * and are moved, selected and resized by their header and resize handles. React Flow sets pointer-events inline.
81
+ */
82
+ .studio-canvas .react-flow__node-domain-group,
83
+ .studio-canvas .react-flow__node-system-group {
84
+ pointer-events: none !important;
85
+ }
86
+ .studio-canvas .react-flow__node-domain-group :is(.ec-group-grip, .react-flow__resize-control),
87
+ .studio-canvas .react-flow__node-system-group :is(.ec-group-grip, .react-flow__resize-control) {
88
+ pointer-events: auto;
89
+ }
@@ -0,0 +1,75 @@
1
+ import * as Y from 'yjs';
2
+ import { applyAwarenessUpdate, encodeAwarenessUpdate, type Awareness } from 'y-protocols/awareness';
3
+ import type { Status } from './hooks/use-studio-flow';
4
+
5
+ /**
6
+ * Syncing a canvas through MCP tool calls, for when a view can't open the collaboration WebSocket
7
+ * (e.g. an MCP App inside a chat whose sandbox blocks it). Each call sends the server what it doesn't have
8
+ * yet (by its state vector) and our presence, and gets back what we don't have and everyone's presence.
9
+ * Slower than the WebSocket (it polls), but works wherever the chat can call the MCP server's tools.
10
+ */
11
+
12
+ export type SyncRequest = {
13
+ canvasId: string;
14
+ clientId: number;
15
+ /** What we have, so the server sends only what we're missing */
16
+ stateVector: string;
17
+ /** What we have that the server doesn't (as of its last state vector) */
18
+ update: string;
19
+ /** Our presence (name, pointer, selection...) */
20
+ awareness: string;
21
+ };
22
+ export type SyncResponse = { update: string; stateVector: string; awareness?: string };
23
+ export type ToolSync = (request: SyncRequest) => Promise<SyncResponse>;
24
+
25
+ // Binary Yjs updates travel as base64 in tool arguments and results (atob/btoa exist in browsers and Node)
26
+ export const toBase64 = (bytes: Uint8Array) => {
27
+ let text = '';
28
+ for (let i = 0; i < bytes.length; i += 0x8000) text += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
29
+ return btoa(text);
30
+ };
31
+ export const fromBase64 = (text: string) => Uint8Array.from(atob(text), (char) => char.charCodeAt(0));
32
+
33
+ /** Applied with this origin, so undo (which tracks local changes) leaves other people's changes alone */
34
+ const REMOTE = Symbol('tool-sync');
35
+
36
+ export function startToolSync(
37
+ doc: Y.Doc,
38
+ awareness: Awareness,
39
+ {
40
+ canvasId,
41
+ sync,
42
+ onStatus,
43
+ intervalMs = 1000,
44
+ }: { canvasId: string; sync: ToolSync; onStatus: (status: Status) => void; intervalMs?: number }
45
+ ) {
46
+ let serverVector: Uint8Array | undefined;
47
+ let stopped = false;
48
+ let timer: ReturnType<typeof setTimeout> | undefined;
49
+
50
+ const tick = async () => {
51
+ try {
52
+ const response = await sync({
53
+ canvasId,
54
+ clientId: doc.clientID,
55
+ stateVector: toBase64(Y.encodeStateVector(doc)),
56
+ update: toBase64(Y.encodeStateAsUpdate(doc, serverVector)),
57
+ awareness: toBase64(encodeAwarenessUpdate(awareness, [doc.clientID])),
58
+ });
59
+ if (stopped) return;
60
+ Y.applyUpdate(doc, fromBase64(response.update), REMOTE);
61
+ serverVector = fromBase64(response.stateVector);
62
+ if (response.awareness) applyAwarenessUpdate(awareness, fromBase64(response.awareness), REMOTE);
63
+ onStatus('connected');
64
+ } catch {
65
+ if (!stopped) onStatus('disconnected');
66
+ }
67
+ if (!stopped) timer = setTimeout(tick, intervalMs);
68
+ };
69
+ void tick();
70
+
71
+ return () => {
72
+ stopped = true;
73
+ clearTimeout(timer);
74
+ };
75
+ }
@@ -52,7 +52,7 @@ import { buildUrl } from '@utils/url-builder';
52
52
  import { getQueries } from '@utils/collections/queries';
53
53
  import { getFirstNavigationPage } from '@features/custom-documentation/utils/custom-docs';
54
54
 
55
- import { isSSR } from '@utils/feature';
55
+ import { isCanvasEnabled, isSSR } from '@utils/feature';
56
56
 
57
57
  const customDocs = await getCollection('customPages');
58
58
  const firstCustomDocsPage = customDocs.length > 0 ? await getFirstNavigationPage() : null;
@@ -292,7 +292,12 @@ const builtinNavigationItems: Record<string, Omit<VerticalNavigationItem, 'curre
292
292
  const defaultNavigationGroups: NavigationGroupConfig[] = [
293
293
  {
294
294
  id: 'main',
295
- items: [{ id: '/' }, { id: '/docs/custom', visible: customDocs.length > 0 }],
295
+ items: [
296
+ { id: '/' },
297
+ { id: '/docs/custom', visible: customDocs.length > 0 },
298
+ // Collaborative canvases, where they're on
299
+ { id: '/studio', visible: isCanvasEnabled() },
300
+ ],
296
301
  },
297
302
  {
298
303
  id: 'browse',
@@ -371,6 +376,8 @@ const resolveNavigationItem = (item: NavigationItemConfig): VerticalNavigationIt
371
376
  if (item.visible === false) return null;
372
377
 
373
378
  const builtinId = navigationBuiltinAliases[item.id] || item.id;
379
+ // Studio is only there where canvases are on
380
+ if (builtinId === '/studio' && !item.href && !isCanvasEnabled()) return null;
374
381
  const builtin = builtinNavigationItems[builtinId];
375
382
  const href = item.href ? normalizeNavigationHref(item.href) : builtin?.href;
376
383
 
@@ -27,4 +27,8 @@ export const isAuthEnabled = () =>
27
27
  (config?.auth?.enabled ?? false) && fs.existsSync(join(projectDirectory(), 'eventcatalog.auth.js')) && isSSR();
28
28
 
29
29
  export const isEventCatalogMCPEnabled = () => isSSR() && (config?.mcp?.enabled ?? true);
30
+
31
+ // Collaborative canvases (EventCatalog Studio, /studio) are an experiment: only on the dev server for now. Canvases live in memory, and
32
+ // the collaboration socket and canvas tools don't check sessions yet, so they're off when authentication is on.
33
+ export const isCanvasEnabled = () => isDevMode() && !isAuthEnabled();
30
34
  export const isEventCatalogMCPAuthEnabled = () => isEventCatalogMCPEnabled() && (config?.mcp?.auth?.enabled ?? false);
@@ -1,4 +1,4 @@
1
- export const TRIAL_LENGTH_DAYS = 90;
1
+ export const TRIAL_LENGTH_DAYS = 60;
2
2
 
3
3
  export const LICENSE_FAQ_URL = 'https://www.eventcatalog.dev/license-faq';
4
4
 
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "license": "BUSL-1.1",
9
9
  "type": "module",
10
- "version": "5.0.0-beta.4",
10
+ "version": "5.0.0-beta.6",
11
11
  "publishConfig": {
12
12
  "access": "public"
13
13
  },
@@ -60,7 +60,11 @@
60
60
  "@fontsource/inter": "^5.2.5",
61
61
  "@headlessui/react": "^2.0.3",
62
62
  "@heroicons/react": "^2.1.3",
63
+ "@hocuspocus/provider": "^4.7.0",
64
+ "@hocuspocus/server": "^4.7.0",
63
65
  "@iconify-json/logos": "^1.2.4",
66
+ "@mcp-b/webmcp-local-relay": "5.1.0",
67
+ "@mcp-b/webmcp-polyfill": "5.1.0",
64
68
  "@mermaid-js/layout-elk": "^0.2.0",
65
69
  "@modelcontextprotocol/ext-apps": "^1.7.5",
66
70
  "@modelcontextprotocol/sdk": "^1.30.0",
@@ -113,6 +117,7 @@
113
117
  "picomatch": "^4.0.4",
114
118
  "react": "^19.2.0",
115
119
  "react-dom": "^19.2.0",
120
+ "react-hotkeys-hook": "^5.3.3",
116
121
  "react-markdown": "^10.1.0",
117
122
  "react-syntax-highlighter": "^16.1.1",
118
123
  "recast": "^0.23.11",
@@ -132,9 +137,11 @@
132
137
  "unist-util-visit": "^5.0.0",
133
138
  "update-notifier": "^7.3.1",
134
139
  "uuid": "^11.1.1",
140
+ "y-protocols": "^1.0.7",
141
+ "yjs": "^13.6.33",
135
142
  "zod": "^4.3.6",
136
143
  "@eventcatalog/linter": "1.2.0-beta.1",
137
- "@eventcatalog/visualiser": "^5.0.0-beta.2",
144
+ "@eventcatalog/visualiser": "^5.0.0-beta.3",
138
145
  "@eventcatalog/sdk": "2.30.0-beta.0"
139
146
  },
140
147
  "devDependencies": {
Binary file
@@ -1,150 +0,0 @@
1
- import React, { useState, useEffect } from 'react';
2
- import * as Dialog from '@radix-ui/react-dialog';
3
- import { ExternalLinkIcon, CheckIcon, ClipboardIcon, Loader2 } from 'lucide-react';
4
-
5
- const StudioPageModal: React.FC = () => {
6
- const [isOpen, setIsOpen] = useState(false);
7
- const [copySuccess, setCopySuccess] = useState(false);
8
- const [hasCopied, setHasCopied] = useState(false);
9
- const [isLoading, setIsLoading] = useState(false);
10
- const [error, setError] = useState<string | null>(null);
11
-
12
- useEffect(() => {
13
- const handleOpen = () => setIsOpen(true);
14
- window.addEventListener('openStudioModal', handleOpen);
15
- return () => window.removeEventListener('openStudioModal', handleOpen);
16
- }, []);
17
-
18
- const handleCopyResources = async () => {
19
- setIsLoading(true);
20
- setError(null);
21
-
22
- try {
23
- const response = await fetch('/api/catalog');
24
-
25
- if (!response.ok) {
26
- throw new Error('Failed to fetch catalog data');
27
- }
28
-
29
- const catalogData = await response.json();
30
-
31
- await navigator.clipboard.writeText(JSON.stringify(catalogData, null, 2));
32
- setCopySuccess(true);
33
- setHasCopied(true);
34
- setTimeout(() => setCopySuccess(false), 2000);
35
- } catch (err) {
36
- console.error('Failed to copy resources:', err);
37
- setError('Failed to copy resources. Please try again.');
38
- } finally {
39
- setIsLoading(false);
40
- }
41
- };
42
-
43
- const handleOpenStudio = () => {
44
- window.open(
45
- 'https://app.eventcatalog.studio/playground?import-resources=true&utm_source=eventcatalog&utm_medium=referral&utm_campaign=studio-page',
46
- '_blank'
47
- );
48
- setIsOpen(false);
49
- };
50
-
51
- return (
52
- <Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
53
- <Dialog.Portal>
54
- <Dialog.Overlay className="fixed inset-0 bg-black/50 data-[state=open]:animate-overlayShow z-50" />
55
- <Dialog.Content className="fixed top-1/2 left-1/2 w-[90vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-lg bg-white p-6 shadow-xl focus:outline-hidden data-[state=open]:animate-contentShow z-[100]">
56
- <Dialog.Title className="text-lg font-semibold text-gray-900 mb-3">Open EventCatalog Studio</Dialog.Title>
57
-
58
- <Dialog.Description className="text-sm text-gray-600 mb-6">
59
- Import your catalog resources into{' '}
60
- <a
61
- href="https://eventcatalog.studio"
62
- className="text-gray-900 hover:text-gray-700 underline font-semibold"
63
- target="_blank"
64
- rel="noopener noreferrer"
65
- >
66
- EventCatalog Studio
67
- </a>{' '}
68
- to create architecture diagrams.
69
- </Dialog.Description>
70
-
71
- <div className="space-y-4">
72
- {/* Step 1: Copy Resources */}
73
- <div className="bg-gray-50 rounded-lg p-4 border border-gray-200">
74
- <h4 className="text-sm font-bold text-gray-900 mb-2">Step 1: Copy your resources</h4>
75
- <p className="text-xs text-gray-600 mb-3">Copy your EventCatalog resources to your clipboard.</p>
76
-
77
- <button
78
- onClick={handleCopyResources}
79
- disabled={isLoading}
80
- className={`w-full flex items-center justify-center space-x-2 px-4 py-2 text-sm font-medium rounded-md border transition-colors ${
81
- copySuccess
82
- ? 'bg-green-50 border-green-200 text-green-700'
83
- : isLoading
84
- ? 'bg-gray-100 border-gray-300 text-gray-400 cursor-not-allowed'
85
- : 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50'
86
- }`}
87
- >
88
- {isLoading ? (
89
- <>
90
- <Loader2 className="w-4 h-4 animate-spin" />
91
- <span>Loading resources...</span>
92
- </>
93
- ) : copySuccess ? (
94
- <>
95
- <CheckIcon className="w-4 h-4" />
96
- <span>Copied!</span>
97
- </>
98
- ) : (
99
- <>
100
- <ClipboardIcon className="w-4 h-4" />
101
- <span>Copy resources to clipboard</span>
102
- </>
103
- )}
104
- </button>
105
-
106
- {error && <p className="text-xs text-red-600 mt-2">{error}</p>}
107
- </div>
108
-
109
- {/* Step 2: Open Studio */}
110
- <div className="bg-gray-50 rounded-lg p-4 border border-gray-200">
111
- <h4 className="text-sm font-bold text-gray-900 mb-2">Step 2: Open EventCatalog Studio</h4>
112
- <p className="text-xs text-gray-600 mb-3">
113
- Go to EventCatalog Studio and paste your resources into the modal dialog.
114
- </p>
115
-
116
- <button
117
- onClick={handleOpenStudio}
118
- disabled={!hasCopied}
119
- className={`w-full flex items-center justify-center space-x-2 px-4 py-2 text-sm font-medium rounded-md transition-colors ${
120
- hasCopied ? 'bg-gray-900 text-white hover:bg-gray-800' : 'bg-gray-300 text-gray-500 cursor-not-allowed'
121
- }`}
122
- >
123
- <ExternalLinkIcon className="w-4 h-4" />
124
- <span>Open EventCatalog Studio</span>
125
- </button>
126
-
127
- <p className="text-[12px] text-gray-500 italic mt-4 mb-0">
128
- All data is stored locally in your browser. Nothing is sent to external servers.
129
- </p>
130
- </div>
131
- </div>
132
-
133
- <div className="mt-6 flex justify-end">
134
- <Dialog.Close asChild>
135
- <button
136
- type="button"
137
- className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-hidden focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-colors"
138
- onClick={() => setIsOpen(false)}
139
- >
140
- Close
141
- </button>
142
- </Dialog.Close>
143
- </div>
144
- </Dialog.Content>
145
- </Dialog.Portal>
146
- </Dialog.Root>
147
- );
148
- };
149
-
150
- export default StudioPageModal;