@eventcatalog/core 5.0.0-beta.0 → 5.0.0-beta.10
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/LICENSE +227 -128
- package/dist/analytics/analytics.cjs +19 -3
- package/dist/analytics/analytics.d.cts +1 -1
- package/dist/analytics/analytics.d.ts +1 -1
- package/dist/analytics/analytics.js +3 -2
- package/dist/analytics/log-build.cjs +72 -35
- package/dist/analytics/log-build.js +5 -4
- package/dist/analytics/telemetry.cjs +42 -0
- package/dist/analytics/telemetry.d.cts +20 -0
- package/dist/analytics/telemetry.d.ts +20 -0
- package/dist/analytics/telemetry.js +8 -0
- package/dist/{chunk-ZHKEARLJ.js → chunk-2LBDRHE4.js} +1 -1
- package/dist/{chunk-TQLRG35T.js → chunk-AWOO63ME.js} +71 -45
- package/dist/chunk-CPW6G62V.js +17 -0
- package/dist/{chunk-LR7AGRHN.js → chunk-CRKJBDN7.js} +56 -33
- package/dist/{chunk-CR6AK5LR.js → chunk-EM36OL6A.js} +1 -1
- package/dist/{chunk-TWN43V4T.js → chunk-IONYKKOY.js} +1 -1
- package/dist/{chunk-WAIADKQE.js → chunk-LJMXOEEC.js} +10 -3
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +205 -124
- package/dist/eventcatalog.config.d.cts +59 -1
- package/dist/eventcatalog.config.d.ts +59 -1
- package/dist/eventcatalog.js +30 -11
- package/dist/federation/federate.js +2 -2
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/runtime-dependencies/@hocuspocus__provider.mjs +3 -0
- package/dist/runtime-dependencies/@hocuspocus__server.mjs +3 -0
- package/dist/runtime-dependencies/@mcp-b__webmcp-local-relay.mjs +3 -0
- package/dist/runtime-dependencies/@mcp-b__webmcp-polyfill.mjs +3 -0
- package/dist/runtime-dependencies/manifest.json +8 -1
- package/dist/runtime-dependencies/react-hotkeys-hook.mjs +3 -0
- package/dist/runtime-dependencies/y-protocols.mjs +3 -0
- package/dist/runtime-dependencies/y-protocols__awareness.mjs +3 -0
- package/dist/runtime-dependencies/yjs.mjs +3 -0
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/dist/utils/license-status.cjs +71 -45
- package/dist/utils/license-status.d.cts +14 -5
- package/dist/utils/license-status.d.ts +14 -5
- package/dist/utils/license-status.js +1 -1
- package/eventcatalog/astro.config.mjs +5 -0
- package/eventcatalog/integrations/storage.mjs +20 -0
- package/eventcatalog/integrations/studio-server.mjs +32 -0
- package/eventcatalog/src/components/Grids/utils.tsx +0 -23
- package/eventcatalog/src/components/Header.astro +3 -3
- package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.tsx +6 -1
- package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +12 -4
- package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +16 -7
- package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +10 -0
- package/eventcatalog/src/components/Tables/Discover/DiscoverTable.tsx +4 -62
- package/eventcatalog/src/components/Tables/Discover/columns.tsx +17 -150
- package/eventcatalog/src/components/Tables/Resources/ResourcesTable.tsx +425 -0
- package/eventcatalog/src/components/Tables/RowActionsMenu.tsx +157 -0
- package/eventcatalog/src/components/Tables/TablePagination.tsx +82 -0
- package/eventcatalog/src/features/ai/chat-api.ts +3 -3
- package/eventcatalog/src/features/auth/middleware/middleware-auth.ts +10 -0
- package/eventcatalog/src/features/integrations/eventcatalog-features.ts +34 -1
- package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +47 -0
- package/eventcatalog/src/features/mcp/apps/canvas/styles.css +22 -0
- package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +276 -0
- package/eventcatalog/src/features/mcp/apps/generated/canvas.html +210 -0
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +75 -74
- package/eventcatalog/src/features/mcp/apps/shared/app-view.tsx +2 -1
- package/eventcatalog/src/features/mcp/mcp-apps.ts +6 -3
- package/eventcatalog/src/features/mcp/mcp-server.ts +36 -5
- package/eventcatalog/src/features/server/start.ts +25 -0
- package/eventcatalog/src/features/storage/database.ts +88 -0
- package/eventcatalog/src/features/storage/migrations.ts +85 -0
- package/eventcatalog/src/features/storage/sql/database.sql +5 -0
- package/eventcatalog/src/features/storage/sql/migrations/0001-studio-canvases.sql +12 -0
- package/eventcatalog/src/features/storage/sql/migrations.sql +15 -0
- package/eventcatalog/src/features/storage/sql.ts +28 -0
- package/eventcatalog/src/features/studio/agent-choreography.ts +381 -0
- package/eventcatalog/src/features/studio/api/canvases-api.ts +170 -0
- package/eventcatalog/src/features/studio/api/canvases.ts +25 -0
- package/eventcatalog/src/features/studio/api/client.ts +35 -0
- package/eventcatalog/src/features/studio/canvas-actions.ts +414 -0
- package/eventcatalog/src/features/studio/canvas-doc.ts +414 -0
- package/eventcatalog/src/features/studio/canvas-preview.ts +53 -0
- package/eventcatalog/src/features/studio/catalog-resources.ts +155 -0
- package/eventcatalog/src/features/studio/catalog.ts +171 -0
- package/eventcatalog/src/features/studio/clipboard.ts +97 -0
- package/eventcatalog/src/features/studio/components/CanvasControls.tsx +154 -0
- package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +370 -0
- package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +117 -0
- package/eventcatalog/src/features/studio/components/Comments.tsx +770 -0
- package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +281 -0
- package/eventcatalog/src/features/studio/components/ConnectionsChooser.tsx +144 -0
- package/eventcatalog/src/features/studio/components/ContainerChooser.tsx +145 -0
- package/eventcatalog/src/features/studio/components/DeleteCanvasDialog.tsx +81 -0
- package/eventcatalog/src/features/studio/components/DragGhost.tsx +115 -0
- package/eventcatalog/src/features/studio/components/JoinForm.tsx +34 -0
- package/eventcatalog/src/features/studio/components/LeftPanel.tsx +314 -0
- package/eventcatalog/src/features/studio/components/Picture.tsx +21 -0
- package/eventcatalog/src/features/studio/components/Presence.tsx +286 -0
- package/eventcatalog/src/features/studio/components/PropertiesPanel.tsx +212 -0
- package/eventcatalog/src/features/studio/components/ShareDialog.tsx +260 -0
- package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +1216 -0
- package/eventcatalog/src/features/studio/components/StudioNotice.astro +24 -0
- package/eventcatalog/src/features/studio/components/canvas-nodes.tsx +255 -0
- package/eventcatalog/src/features/studio/components/icons.ts +53 -0
- package/eventcatalog/src/features/studio/components/status.ts +53 -0
- package/eventcatalog/src/features/studio/edges.ts +56 -0
- package/eventcatalog/src/features/studio/flow-state.ts +92 -0
- package/eventcatalog/src/features/studio/grouping.ts +226 -0
- package/eventcatalog/src/features/studio/hooks/presence-sender.ts +46 -0
- package/eventcatalog/src/features/studio/hooks/presence-store.ts +119 -0
- package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +347 -0
- package/eventcatalog/src/features/studio/hooks/use-comments.ts +54 -0
- package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +104 -0
- package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +589 -0
- package/eventcatalog/src/features/studio/identity.ts +37 -0
- package/eventcatalog/src/features/studio/layout.ts +42 -0
- package/eventcatalog/src/features/studio/levels.ts +282 -0
- package/eventcatalog/src/features/studio/node-types.ts +205 -0
- package/eventcatalog/src/features/studio/pages/[id].astro +68 -0
- package/eventcatalog/src/features/studio/pages/index.astro +784 -0
- package/eventcatalog/src/features/studio/pages/webmcp-relay.ts +27 -0
- package/eventcatalog/src/features/studio/placement.ts +197 -0
- package/eventcatalog/src/features/studio/server/canvas-mcp.ts +510 -0
- package/eventcatalog/src/features/studio/server/runtime.ts +491 -0
- package/eventcatalog/src/features/studio/server/sign-in.ts +18 -0
- package/eventcatalog/src/features/studio/server/sql/storage.sql +18 -0
- package/eventcatalog/src/features/studio/server/storage.ts +90 -0
- package/eventcatalog/src/features/studio/server/tool-icons.ts +30 -0
- package/eventcatalog/src/features/studio/studio.css +273 -0
- package/eventcatalog/src/features/studio/tool-sync.ts +75 -0
- package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +10 -5
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.spec.ts +52 -63
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +34 -46
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +11 -29
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_ResourcesPage.astro +79 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.spec.ts +32 -14
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.ts +63 -58
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/index.astro +2 -62
- package/eventcatalog/src/pages/docs/services/[id]/[version]/resources/index.astro +13 -0
- package/eventcatalog/src/stores/sidebar-store/builders/domain.ts +24 -42
- package/eventcatalog/src/stores/sidebar-store/builders/flow.ts +2 -0
- package/eventcatalog/src/stores/sidebar-store/builders/message.ts +2 -0
- package/eventcatalog/src/stores/sidebar-store/builders/service.ts +23 -23
- package/eventcatalog/src/stores/sidebar-store/builders/shared.ts +13 -0
- package/eventcatalog/src/stores/sidebar-store/builders/system.ts +18 -27
- package/eventcatalog/src/stores/sidebar-store/state.ts +5 -0
- package/eventcatalog/src/styles/tailwind.css +2 -2
- package/eventcatalog/src/utils/collections/channels.ts +56 -1
- package/eventcatalog/src/utils/collections/flows.ts +50 -1
- package/eventcatalog/src/utils/collections/message-connections.ts +60 -0
- package/eventcatalog/src/utils/collections/message-triggers.ts +2 -1
- package/eventcatalog/src/utils/collections/resource-owners.ts +193 -0
- package/eventcatalog/src/utils/collections/resources.ts +310 -0
- package/eventcatalog/src/utils/collections/util.ts +31 -0
- package/eventcatalog/src/utils/feature.ts +8 -0
- package/eventcatalog/src/utils/git.ts +11 -5
- package/eventcatalog/src/utils/node-graphs/export-mermaid.ts +0 -2
- package/eventcatalog/src/utils/trial.ts +1 -1
- package/package.json +17 -12
- package/dist/runtime-dependencies/shelljs.mjs +0 -3
- package/eventcatalog/public/studio-bg.png +0 -0
- package/eventcatalog/src/components/Grids/DomainGrid.tsx +0 -838
- package/eventcatalog/src/components/Grids/MessageGrid.tsx +0 -344
- package/eventcatalog/src/components/Grids/SystemGrid.tsx +0 -185
- package/eventcatalog/src/components/Grids/components.tsx +0 -170
- package/eventcatalog/src/components/Grids/message-link.ts +0 -38
- package/eventcatalog/src/components/Studio/StudioPageModal.tsx +0 -150
- package/eventcatalog/src/components/Tables/SystemResources/SystemResourcesTable.tsx +0 -294
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.spec.ts +0 -65
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.ts +0 -46
- package/eventcatalog/src/pages/studio.astro +0 -316
- package/dist/{chunk-AYNZXUB5.js → chunk-KHACCTW2.js} +3 -3
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { Bot, Check, ChevronDown, Copy, X } from 'lucide-react';
|
|
3
|
+
import type { WebMcpStatus } from '../hooks/use-canvas-webmcp';
|
|
4
|
+
import { STATUS } from './status';
|
|
5
|
+
|
|
6
|
+
type CanvasLink = { canvasId: string; title?: string; canvasUrl: string; mcpUrl: string };
|
|
7
|
+
|
|
8
|
+
export const RELAY_COMMAND = 'npx -y @mcp-b/webmcp-local-relay@5.1.0';
|
|
9
|
+
const RELAY_KEY = 'eventcatalog-webmcp-relay';
|
|
10
|
+
const WEBMCP_KEY = 'eventcatalog-webmcp';
|
|
11
|
+
|
|
12
|
+
const isOn = (key: string) => {
|
|
13
|
+
try {
|
|
14
|
+
return localStorage.getItem(key) === 'true';
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const turnOn = (key: string) => {
|
|
20
|
+
try {
|
|
21
|
+
localStorage.setItem(key, 'true');
|
|
22
|
+
} catch {}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* WebMCP through the polyfill (for browsers without WebMCP of their own) is opt in, remembered in this browser:
|
|
27
|
+
* the polyfill watches every change to the page, which on a canvas is every drag and pan. The relay needs it.
|
|
28
|
+
*/
|
|
29
|
+
export const isWebMcpEnabled = () => isOn(WEBMCP_KEY) || isOn(RELAY_KEY);
|
|
30
|
+
export const enableWebMcp = () => turnOn(WEBMCP_KEY);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The WebMCP local relay hands this tab's tools to desktop agents. Opt in: its script keeps looking for a
|
|
34
|
+
* relay on localhost, so it only loads once someone turns it on (remembered in this browser).
|
|
35
|
+
*/
|
|
36
|
+
export const isRelayEnabled = () => isOn(RELAY_KEY);
|
|
37
|
+
|
|
38
|
+
export const loadRelay = (scriptPath: string) => {
|
|
39
|
+
if (document.querySelector('script[data-webmcp-relay-embed]')) return;
|
|
40
|
+
const script = document.createElement('script');
|
|
41
|
+
script.src = scriptPath;
|
|
42
|
+
script.dataset.webmcpRelayEmbed = '';
|
|
43
|
+
script.dataset.relayPort = '9333';
|
|
44
|
+
document.body.appendChild(script);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const enableRelay = (scriptPath: string) => {
|
|
48
|
+
turnOn(RELAY_KEY);
|
|
49
|
+
loadRelay(scriptPath);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The prompt people give their agent to join this canvas. It works out how to connect from what the agent
|
|
54
|
+
* can do: the EventCatalog MCP server (and openCanvas in chats that show MCP Apps), WebMCP in the open tab,
|
|
55
|
+
* or the WebMCP local relay for desktop agents.
|
|
56
|
+
*/
|
|
57
|
+
export const buildAgentPrompt = ({ canvasId, title, canvasUrl, mcpUrl }: CanvasLink) =>
|
|
58
|
+
`Join me on an EventCatalog canvas so we can design this together, live.
|
|
59
|
+
|
|
60
|
+
Canvas: ${title ? `"${title}"` : 'Untitled'}
|
|
61
|
+
Canvas id: ${canvasId}
|
|
62
|
+
Link: ${canvasUrl}
|
|
63
|
+
|
|
64
|
+
Connect with the first of these you can use:
|
|
65
|
+
|
|
66
|
+
1. EventCatalog MCP server (Streamable HTTP): ${mcpUrl}
|
|
67
|
+
- If you already have it (tools like getCanvas, addToCanvas), use them with canvasId "${canvasId}".
|
|
68
|
+
- If not and you can add MCP servers, add it. For example, in Claude Code: claude mcp add --transport http eventcatalog ${mcpUrl}
|
|
69
|
+
- If you can show MCP Apps in this chat (e.g. ChatGPT, Claude), call openCanvas with canvasId "${canvasId}" so I can work on the canvas with you right here.
|
|
70
|
+
2. WebMCP: if you can use tools from my browser tab, I have the canvas open there and it offers tools like get_canvas, add_to_canvas and add_comment.
|
|
71
|
+
3. WebMCP local relay: if you run on my computer (e.g. Claude Code, Cursor) and can't reach the MCP server, add the MCP server \`${RELAY_COMMAND}\`. It gives you the tools of my open canvas tab.
|
|
72
|
+
|
|
73
|
+
Once connected:
|
|
74
|
+
- Read the canvas first (getCanvas / get_canvas) and tell me here who else is on it.
|
|
75
|
+
- Ask me here, in our conversation, what we are designing, then work on the canvas as we talk: add catalog resources (find them with getResources / search_catalog), add new components for things that don't exist yet, and connect them. Leave out positions so new nodes go next to what they connect to; only re-lay the whole canvas (layoutCanvas / layout_canvas) when I ask.
|
|
76
|
+
- Talk to me here, not in canvas comments. Only comment on the canvas when I ask you to (e.g. to review it), and reply to comments people leave for you.
|
|
77
|
+
- Pass agentName with your name on MCP tools, so people on the canvas can see who you are.`;
|
|
78
|
+
|
|
79
|
+
function CopyButton({ text, label = 'Copy', large = false }: { text: string; label?: string; large?: boolean }) {
|
|
80
|
+
const [copied, setCopied] = useState(false);
|
|
81
|
+
return (
|
|
82
|
+
<button
|
|
83
|
+
onClick={async () => {
|
|
84
|
+
await navigator.clipboard.writeText(text);
|
|
85
|
+
setCopied(true);
|
|
86
|
+
setTimeout(() => setCopied(false), 1500);
|
|
87
|
+
}}
|
|
88
|
+
className={`flex shrink-0 items-center justify-center gap-1.5 rounded-lg font-medium bg-[rgb(var(--ec-button-bg))] text-[rgb(var(--ec-button-text))] hover:bg-[rgb(var(--ec-button-bg-hover))] ${
|
|
89
|
+
large ? 'w-full px-4 py-2.5 text-sm' : 'px-2.5 py-1.5 text-xs'
|
|
90
|
+
}`}
|
|
91
|
+
>
|
|
92
|
+
{copied ? <Check size={large ? 16 : 14} /> : <Copy size={large ? 16 : 14} />}
|
|
93
|
+
{copied ? 'Copied' : label}
|
|
94
|
+
</button>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Something to copy (an address or a command), with its copy button */
|
|
99
|
+
function CopyField({ value }: { value: string }) {
|
|
100
|
+
return (
|
|
101
|
+
<div className="flex items-center gap-2">
|
|
102
|
+
<code className="min-w-0 flex-1 truncate rounded-md border px-2 py-1.5 text-xs bg-[rgb(var(--ec-input-bg))] border-[rgb(var(--ec-input-border))]">
|
|
103
|
+
{value}
|
|
104
|
+
</code>
|
|
105
|
+
<CopyButton text={value} />
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Turns something on for this tab, or says it's on */
|
|
111
|
+
function TabSwitch({ on, onTurnOn, label }: { on: boolean; onTurnOn: () => void; label: string }) {
|
|
112
|
+
return on ? (
|
|
113
|
+
<p className={`flex shrink-0 items-center gap-1.5 whitespace-nowrap text-xs font-medium ${STATUS.success.text}`}>
|
|
114
|
+
<span className={`h-2 w-2 rounded-full ${STATUS.success.dot}`} />
|
|
115
|
+
On for this tab
|
|
116
|
+
</p>
|
|
117
|
+
) : (
|
|
118
|
+
<button
|
|
119
|
+
onClick={onTurnOn}
|
|
120
|
+
className="shrink-0 whitespace-nowrap rounded-lg border px-2.5 py-1.5 text-xs font-medium border-[rgb(var(--ec-page-border))] hover:bg-[rgb(var(--ec-page-border)/0.4)]"
|
|
121
|
+
>
|
|
122
|
+
{label}
|
|
123
|
+
</button>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
type Way = 'chat' | 'code' | 'browser';
|
|
128
|
+
const WAYS: { id: Way; label: string; hint: string }[] = [
|
|
129
|
+
{ id: 'chat', label: 'Chat app', hint: 'Claude, ChatGPT' },
|
|
130
|
+
{ id: 'code', label: 'Coding agent', hint: 'Claude Code, Cursor, Codex' },
|
|
131
|
+
{ id: 'browser', label: 'This browser', hint: 'WebMCP' },
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Bringing an AI agent onto the canvas: copy the prompt and give it to the agent (it works out how to connect).
|
|
136
|
+
* The prompt is there to read, and how each kind of agent connects is one step each, for setting it up by hand.
|
|
137
|
+
*/
|
|
138
|
+
export default function ConnectAgentDialog({
|
|
139
|
+
link,
|
|
140
|
+
webMcp,
|
|
141
|
+
relayScriptPath,
|
|
142
|
+
onEnableWebMcp,
|
|
143
|
+
onClose,
|
|
144
|
+
}: {
|
|
145
|
+
link: CanvasLink;
|
|
146
|
+
webMcp: WebMcpStatus;
|
|
147
|
+
/** Turn WebMCP on for this tab (through the polyfill, when the browser has none) */
|
|
148
|
+
onEnableWebMcp: () => void;
|
|
149
|
+
/** Where the relay's script is served (only on EventCatalog's own canvas page) */
|
|
150
|
+
relayScriptPath?: string;
|
|
151
|
+
onClose: () => void;
|
|
152
|
+
}) {
|
|
153
|
+
const prompt = buildAgentPrompt(link);
|
|
154
|
+
const [relayOn, setRelayOn] = useState(isRelayEnabled);
|
|
155
|
+
const [showPrompt, setShowPrompt] = useState(false);
|
|
156
|
+
const [way, setWay] = useState<Way>('chat');
|
|
157
|
+
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
const onKeyDown = (event: KeyboardEvent) => event.key === 'Escape' && onClose();
|
|
160
|
+
window.addEventListener('keydown', onKeyDown);
|
|
161
|
+
return () => window.removeEventListener('keydown', onKeyDown);
|
|
162
|
+
}, [onClose]);
|
|
163
|
+
|
|
164
|
+
const turnOnRelay = () => {
|
|
165
|
+
if (!relayScriptPath) return;
|
|
166
|
+
enableRelay(relayScriptPath);
|
|
167
|
+
setRelayOn(true);
|
|
168
|
+
// The relay hands this tab's WebMCP tools to desktop agents
|
|
169
|
+
onEnableWebMcp();
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4" onClick={onClose}>
|
|
174
|
+
<div
|
|
175
|
+
role="dialog"
|
|
176
|
+
aria-label="Connect your agent"
|
|
177
|
+
onClick={(event) => event.stopPropagation()}
|
|
178
|
+
className="flex max-h-full w-full max-w-md flex-col rounded-xl border shadow-2xl bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text))]"
|
|
179
|
+
>
|
|
180
|
+
<header className="flex items-start justify-between gap-3 px-5 pb-3 pt-5">
|
|
181
|
+
<div className="flex items-center gap-3">
|
|
182
|
+
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[rgb(var(--ec-accent-subtle))] text-[rgb(var(--ec-accent))]">
|
|
183
|
+
<Bot size={18} />
|
|
184
|
+
</span>
|
|
185
|
+
<div>
|
|
186
|
+
<h2 className="text-base font-semibold">Bring in an AI agent</h2>
|
|
187
|
+
<p className="text-xs text-[rgb(var(--ec-page-text-muted))]">It joins this canvas and designs with you, live.</p>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
<button
|
|
191
|
+
onClick={onClose}
|
|
192
|
+
aria-label="Close"
|
|
193
|
+
className="rounded-md p-1 text-[rgb(var(--ec-icon-color))] hover:bg-[rgb(var(--ec-page-border)/0.5)]"
|
|
194
|
+
>
|
|
195
|
+
<X size={16} />
|
|
196
|
+
</button>
|
|
197
|
+
</header>
|
|
198
|
+
|
|
199
|
+
<div className="min-h-0 space-y-5 overflow-y-auto px-5 pb-5">
|
|
200
|
+
{/* The one thing most people need: the prompt */}
|
|
201
|
+
<section className="space-y-2">
|
|
202
|
+
<CopyButton text={prompt} label="Copy prompt" large />
|
|
203
|
+
<p className="text-center text-xs text-[rgb(var(--ec-page-text-muted))]">
|
|
204
|
+
Paste it into your agent. It works out how to connect.{' '}
|
|
205
|
+
<button
|
|
206
|
+
onClick={() => setShowPrompt((shown) => !shown)}
|
|
207
|
+
aria-expanded={showPrompt}
|
|
208
|
+
className="inline-flex items-center gap-0.5 font-medium text-[rgb(var(--ec-accent))] hover:underline"
|
|
209
|
+
>
|
|
210
|
+
{showPrompt ? 'Hide prompt' : 'Show prompt'}
|
|
211
|
+
<ChevronDown size={12} className={`transition-transform ${showPrompt ? 'rotate-180' : ''}`} />
|
|
212
|
+
</button>
|
|
213
|
+
</p>
|
|
214
|
+
{showPrompt && (
|
|
215
|
+
<pre className="max-h-56 overflow-y-auto whitespace-pre-wrap rounded-lg border p-3 font-mono text-[11px] leading-relaxed bg-[rgb(var(--ec-page-bg))] border-[rgb(var(--ec-page-border))]">
|
|
216
|
+
{prompt}
|
|
217
|
+
</pre>
|
|
218
|
+
)}
|
|
219
|
+
</section>
|
|
220
|
+
|
|
221
|
+
{/* Or set it up by hand, for the kind of agent you use */}
|
|
222
|
+
<section className="space-y-3 border-t pt-4 border-[rgb(var(--ec-page-border))]">
|
|
223
|
+
<h3 className="text-xs font-semibold">Or connect it yourself</h3>
|
|
224
|
+
<div role="tablist" className="grid grid-cols-3 gap-1 rounded-lg p-1 bg-[rgb(var(--ec-page-border)/0.5)]">
|
|
225
|
+
{WAYS.map(({ id, label, hint }) => (
|
|
226
|
+
<button
|
|
227
|
+
key={id}
|
|
228
|
+
role="tab"
|
|
229
|
+
aria-selected={way === id}
|
|
230
|
+
onClick={() => setWay(id)}
|
|
231
|
+
className={`rounded-md px-2 py-1.5 text-center ${
|
|
232
|
+
way === id ? 'bg-[rgb(var(--ec-card-bg))] shadow-sm' : 'text-[rgb(var(--ec-page-text-muted))]'
|
|
233
|
+
}`}
|
|
234
|
+
>
|
|
235
|
+
<span className="block text-xs font-medium">{label}</span>
|
|
236
|
+
<span className="block truncate text-[10px] text-[rgb(var(--ec-page-text-muted))]">{hint}</span>
|
|
237
|
+
</button>
|
|
238
|
+
))}
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
{way === 'chat' && (
|
|
242
|
+
<div className="space-y-2">
|
|
243
|
+
<p className="text-xs text-[rgb(var(--ec-page-text-muted))]">
|
|
244
|
+
Add this MCP server to your chat app. Chats that show MCP Apps open the canvas right in the chat.
|
|
245
|
+
</p>
|
|
246
|
+
<CopyField value={link.mcpUrl} />
|
|
247
|
+
</div>
|
|
248
|
+
)}
|
|
249
|
+
{way === 'code' && (
|
|
250
|
+
<div className="space-y-2">
|
|
251
|
+
<p className="text-xs text-[rgb(var(--ec-page-text-muted))]">
|
|
252
|
+
Add EventCatalog's MCP server, e.g. in Claude Code:
|
|
253
|
+
</p>
|
|
254
|
+
<CopyField value={`claude mcp add --transport http eventcatalog ${link.mcpUrl}`} />
|
|
255
|
+
{relayScriptPath && (
|
|
256
|
+
<div className="flex items-center justify-between gap-3 pt-1">
|
|
257
|
+
<p className="text-[11px] text-[rgb(var(--ec-page-text-muted))]">
|
|
258
|
+
Can't reach this server? Use this tab through the local relay: add <code>{RELAY_COMMAND}</code> as an MCP
|
|
259
|
+
server.
|
|
260
|
+
</p>
|
|
261
|
+
<TabSwitch on={relayOn} onTurnOn={turnOnRelay} label="Turn on relay" />
|
|
262
|
+
</div>
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
)}
|
|
266
|
+
{way === 'browser' && (
|
|
267
|
+
<div className="flex items-center justify-between gap-3">
|
|
268
|
+
<p className="text-xs text-[rgb(var(--ec-page-text-muted))]">
|
|
269
|
+
{webMcp.state === 'on'
|
|
270
|
+
? `This tab offers ${webMcp.tools} canvas tools to agents in your browser (like the MCP-B extension).`
|
|
271
|
+
: 'Let agents in your browser (like the MCP-B extension) use this canvas through WebMCP.'}
|
|
272
|
+
</p>
|
|
273
|
+
<TabSwitch on={webMcp.state === 'on'} onTurnOn={onEnableWebMcp} label="Turn on" />
|
|
274
|
+
</div>
|
|
275
|
+
)}
|
|
276
|
+
</section>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { useEffect, useState, type ReactElement } from 'react';
|
|
2
|
+
import { X } from 'lucide-react';
|
|
3
|
+
import type { ConnectionGroup, ConnectionGroupId } from '../canvas-actions';
|
|
4
|
+
import type { CatalogResource } from '../catalog-resources';
|
|
5
|
+
import { getNodeLabel } from '../node-types';
|
|
6
|
+
import { CardPreview } from './ContainerChooser';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Asked when a catalog service or message is dropped on the canvas (and connects to things that aren't on it yet):
|
|
10
|
+
* just it, or it with its connections (a service's messages and data stores, the services that send and receive a
|
|
11
|
+
* message), picked by group. Styled like the domain and system chooser.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type ConnectionsChoice = { as: 'card' } | { as: 'connected'; include: CatalogResource[] };
|
|
15
|
+
|
|
16
|
+
// It in the middle, what comes in before it and what goes out after it
|
|
17
|
+
const ConnectedPreview = () => (
|
|
18
|
+
<svg viewBox="0 0 64 36" className="h-9 w-16" aria-hidden>
|
|
19
|
+
<rect x="1" y="7" width="10" height="8" rx="1.5" fill="currentColor" fillOpacity="0.4" />
|
|
20
|
+
<rect x="1" y="21" width="10" height="8" rx="1.5" fill="currentColor" fillOpacity="0.4" />
|
|
21
|
+
<path d="M11 11h8M11 25h8" stroke="currentColor" strokeOpacity="0.5" strokeWidth="1.5" />
|
|
22
|
+
<rect
|
|
23
|
+
x="19"
|
|
24
|
+
y="8"
|
|
25
|
+
width="26"
|
|
26
|
+
height="20"
|
|
27
|
+
rx="4"
|
|
28
|
+
fill="currentColor"
|
|
29
|
+
fillOpacity="0.1"
|
|
30
|
+
stroke="currentColor"
|
|
31
|
+
strokeOpacity="0.5"
|
|
32
|
+
strokeWidth="1.5"
|
|
33
|
+
/>
|
|
34
|
+
<rect x="23" y="13" width="13" height="3" rx="1.5" fill="currentColor" fillOpacity="0.6" />
|
|
35
|
+
<rect x="23" y="19" width="17" height="2" rx="1" fill="currentColor" fillOpacity="0.35" />
|
|
36
|
+
<path d="M45 18h8" stroke="currentColor" strokeOpacity="0.5" strokeWidth="1.5" />
|
|
37
|
+
<rect x="53" y="14" width="10" height="8" rx="1.5" fill="currentColor" fillOpacity="0.4" />
|
|
38
|
+
</svg>
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
/** What "with its connections" brings, by what's dropped */
|
|
42
|
+
const CONNECTED_HINTS: Record<string, string> = {
|
|
43
|
+
services: 'What it sends, receives and stores, around it',
|
|
44
|
+
};
|
|
45
|
+
const MESSAGE_HINT = 'The services that send and receive it, around it';
|
|
46
|
+
|
|
47
|
+
export default function ConnectionsChooser({
|
|
48
|
+
resource,
|
|
49
|
+
groups,
|
|
50
|
+
position,
|
|
51
|
+
onChoose,
|
|
52
|
+
onCancel,
|
|
53
|
+
}: {
|
|
54
|
+
resource: CatalogResource;
|
|
55
|
+
/** What it connects to that isn't on the canvas yet */
|
|
56
|
+
groups: ConnectionGroup[];
|
|
57
|
+
/** Where to show it (screen coordinates) */
|
|
58
|
+
position: { x: number; y: number };
|
|
59
|
+
onChoose: (choice: ConnectionsChoice) => void;
|
|
60
|
+
onCancel: () => void;
|
|
61
|
+
}) {
|
|
62
|
+
const noun = getNodeLabel(resource.node.type).toLowerCase();
|
|
63
|
+
const [included, setIncluded] = useState<Set<ConnectionGroupId>>(() => new Set(groups.map((group) => group.id)));
|
|
64
|
+
const toggle = (id: ConnectionGroupId) =>
|
|
65
|
+
setIncluded((current) => {
|
|
66
|
+
const next = new Set(current);
|
|
67
|
+
if (next.has(id)) next.delete(id);
|
|
68
|
+
else next.add(id);
|
|
69
|
+
return next;
|
|
70
|
+
});
|
|
71
|
+
const include = groups.flatMap((group) => (included.has(group.id) ? group.resources : []));
|
|
72
|
+
const options: { as: ConnectionsChoice['as']; label: string; hint: string; Preview: () => ReactElement }[] = [
|
|
73
|
+
{ as: 'card', label: `Just the ${noun}`, hint: 'Connected to what is already on the canvas', Preview: CardPreview },
|
|
74
|
+
{
|
|
75
|
+
as: 'connected',
|
|
76
|
+
label: 'With its connections',
|
|
77
|
+
hint: CONNECTED_HINTS[resource.collection] ?? MESSAGE_HINT,
|
|
78
|
+
Preview: ConnectedPreview,
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
const onKeyDown = (event: KeyboardEvent) => event.key === 'Escape' && onCancel();
|
|
84
|
+
window.addEventListener('keydown', onKeyDown);
|
|
85
|
+
return () => window.removeEventListener('keydown', onKeyDown);
|
|
86
|
+
}, [onCancel]);
|
|
87
|
+
|
|
88
|
+
// Kept on screen, next to where it was dropped
|
|
89
|
+
const left = Math.min(Math.max(position.x - 160, 12), window.innerWidth - 332);
|
|
90
|
+
const top = Math.min(Math.max(position.y - 20, 12), window.innerHeight - 300);
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div className="fixed inset-0 z-50" onPointerDown={onCancel}>
|
|
94
|
+
<div
|
|
95
|
+
role="dialog"
|
|
96
|
+
aria-label={`Add ${resource.name}`}
|
|
97
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
98
|
+
className="absolute w-80 rounded-lg border p-1.5 shadow-xl bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text))]"
|
|
99
|
+
style={{ left, top }}
|
|
100
|
+
>
|
|
101
|
+
<div className="flex items-start justify-between gap-2 px-2 pb-1.5 pt-1">
|
|
102
|
+
<div>
|
|
103
|
+
<p className="text-[10px] font-semibold uppercase tracking-wider text-[rgb(var(--ec-page-text-muted))]">
|
|
104
|
+
Add this {noun}
|
|
105
|
+
</p>
|
|
106
|
+
<p className="truncate text-xs font-semibold">{resource.name}</p>
|
|
107
|
+
</div>
|
|
108
|
+
<button
|
|
109
|
+
onClick={onCancel}
|
|
110
|
+
aria-label="Cancel"
|
|
111
|
+
className="rounded p-0.5 text-[rgb(var(--ec-icon-color))] hover:bg-[rgb(var(--ec-page-border)/0.5)]"
|
|
112
|
+
>
|
|
113
|
+
<X size={14} />
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div className="grid grid-cols-2 gap-1.5 px-0.5 pb-0.5">
|
|
118
|
+
{options.map(({ as, label, hint, Preview }) => (
|
|
119
|
+
<button
|
|
120
|
+
key={as}
|
|
121
|
+
autoFocus={as === 'connected'}
|
|
122
|
+
disabled={as === 'connected' && include.length === 0}
|
|
123
|
+
onClick={() => onChoose(as === 'card' ? { as } : { as, include })}
|
|
124
|
+
className="flex flex-col items-center gap-1 rounded-md border px-2 pb-1.5 pt-2 text-xs outline-none transition-colors border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text-muted))] hover:border-[rgb(var(--ec-accent))] hover:bg-[rgb(var(--ec-accent)/0.08)] hover:text-[rgb(var(--ec-accent))] focus-visible:border-[rgb(var(--ec-accent))] focus-visible:text-[rgb(var(--ec-accent))] disabled:cursor-not-allowed disabled:opacity-50"
|
|
125
|
+
>
|
|
126
|
+
<Preview />
|
|
127
|
+
<span className="font-semibold">{label}</span>
|
|
128
|
+
<span className="text-center text-[10px] leading-tight text-[rgb(var(--ec-page-text-muted))]">{hint}</span>
|
|
129
|
+
</button>
|
|
130
|
+
))}
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<div className="mt-1 space-y-0.5 px-2 pb-1">
|
|
134
|
+
{groups.map((group) => (
|
|
135
|
+
<label key={group.id} className="flex items-center gap-2 py-0.5 text-[11px] text-[rgb(var(--ec-page-text-muted))]">
|
|
136
|
+
<input type="checkbox" checked={included.has(group.id)} onChange={() => toggle(group.id)} />
|
|
137
|
+
{group.label} ({group.resources.length})
|
|
138
|
+
</label>
|
|
139
|
+
))}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { useEffect, useState, type ReactElement } from 'react';
|
|
2
|
+
import { X } from 'lucide-react';
|
|
3
|
+
import type { CatalogResource } from '../catalog-resources';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Asked when a domain or system is dropped on the canvas: is it a card (connected to what it contains), or a
|
|
7
|
+
* container that what it contains sits inside? Styled like the visualiser's group-by picker (boxes or lanes).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type ContainerChoice = { as: 'card' } | { as: 'container'; withContents: boolean };
|
|
11
|
+
|
|
12
|
+
// Small pictures of the two ways a domain or system can be on the canvas
|
|
13
|
+
export const CardPreview = () => (
|
|
14
|
+
<svg viewBox="0 0 64 36" className="h-9 w-16" aria-hidden>
|
|
15
|
+
<rect
|
|
16
|
+
x="18"
|
|
17
|
+
y="8"
|
|
18
|
+
width="28"
|
|
19
|
+
height="20"
|
|
20
|
+
rx="4"
|
|
21
|
+
fill="currentColor"
|
|
22
|
+
fillOpacity="0.1"
|
|
23
|
+
stroke="currentColor"
|
|
24
|
+
strokeOpacity="0.5"
|
|
25
|
+
strokeWidth="1.5"
|
|
26
|
+
/>
|
|
27
|
+
<rect x="23" y="13" width="14" height="3" rx="1.5" fill="currentColor" fillOpacity="0.6" />
|
|
28
|
+
<rect x="23" y="19" width="18" height="2" rx="1" fill="currentColor" fillOpacity="0.35" />
|
|
29
|
+
<path d="M46 18h12" stroke="currentColor" strokeOpacity="0.5" strokeWidth="1.5" strokeDasharray="2 2" />
|
|
30
|
+
<rect x="57" y="14" width="6" height="8" rx="1.5" fill="currentColor" fillOpacity="0.4" />
|
|
31
|
+
</svg>
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const ContainerPreview = () => (
|
|
35
|
+
<svg viewBox="0 0 64 36" className="h-9 w-16" aria-hidden>
|
|
36
|
+
<rect
|
|
37
|
+
x="4"
|
|
38
|
+
y="2"
|
|
39
|
+
width="56"
|
|
40
|
+
height="32"
|
|
41
|
+
rx="4"
|
|
42
|
+
fill="currentColor"
|
|
43
|
+
fillOpacity="0.08"
|
|
44
|
+
stroke="currentColor"
|
|
45
|
+
strokeOpacity="0.5"
|
|
46
|
+
strokeWidth="1.5"
|
|
47
|
+
/>
|
|
48
|
+
<path d="M4 10h56" stroke="currentColor" strokeOpacity="0.35" strokeWidth="1" />
|
|
49
|
+
<rect x="8" y="4.5" width="14" height="3" rx="1.5" fill="currentColor" fillOpacity="0.6" />
|
|
50
|
+
<rect x="10" y="15" width="18" height="12" rx="2" fill="currentColor" fillOpacity="0.5" />
|
|
51
|
+
<rect x="36" y="15" width="18" height="12" rx="2" fill="currentColor" fillOpacity="0.5" />
|
|
52
|
+
</svg>
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const OPTIONS: { as: ContainerChoice['as']; label: string; hint: string; Preview: () => ReactElement }[] = [
|
|
56
|
+
{ as: 'card', label: 'Card', hint: 'Connected to what it contains', Preview: CardPreview },
|
|
57
|
+
{ as: 'container', label: 'Container', hint: 'What it contains sits inside it', Preview: ContainerPreview },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const plural = (count: number, noun: string) => `${count} ${noun}${count === 1 ? '' : 's'}`;
|
|
61
|
+
|
|
62
|
+
export default function ContainerChooser({
|
|
63
|
+
resource,
|
|
64
|
+
contents,
|
|
65
|
+
position,
|
|
66
|
+
onChoose,
|
|
67
|
+
onCancel,
|
|
68
|
+
}: {
|
|
69
|
+
resource: CatalogResource;
|
|
70
|
+
/** What it contains in the catalog that isn't on the canvas yet */
|
|
71
|
+
contents: { systems: number; services: number };
|
|
72
|
+
/** Where to show it (screen coordinates) */
|
|
73
|
+
position: { x: number; y: number };
|
|
74
|
+
onChoose: (choice: ContainerChoice) => void;
|
|
75
|
+
onCancel: () => void;
|
|
76
|
+
}) {
|
|
77
|
+
const [withContents, setWithContents] = useState(true);
|
|
78
|
+
const noun = resource.collection === 'domains' ? 'domain' : 'system';
|
|
79
|
+
const contentsLabel = [
|
|
80
|
+
contents.systems > 0 && plural(contents.systems, 'system'),
|
|
81
|
+
contents.services > 0 && plural(contents.services, 'service'),
|
|
82
|
+
]
|
|
83
|
+
.filter(Boolean)
|
|
84
|
+
.join(' and ');
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
const onKeyDown = (event: KeyboardEvent) => event.key === 'Escape' && onCancel();
|
|
88
|
+
window.addEventListener('keydown', onKeyDown);
|
|
89
|
+
return () => window.removeEventListener('keydown', onKeyDown);
|
|
90
|
+
}, [onCancel]);
|
|
91
|
+
|
|
92
|
+
// Kept on screen, next to where it was dropped
|
|
93
|
+
const left = Math.min(Math.max(position.x - 160, 12), window.innerWidth - 332);
|
|
94
|
+
const top = Math.min(Math.max(position.y - 20, 12), window.innerHeight - 260);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div className="fixed inset-0 z-50" onPointerDown={onCancel}>
|
|
98
|
+
<div
|
|
99
|
+
role="dialog"
|
|
100
|
+
aria-label={`Add ${resource.name} as`}
|
|
101
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
102
|
+
className="absolute w-80 rounded-lg border p-1.5 shadow-xl bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text))]"
|
|
103
|
+
style={{ left, top }}
|
|
104
|
+
>
|
|
105
|
+
<div className="flex items-start justify-between gap-2 px-2 pb-1.5 pt-1">
|
|
106
|
+
<div>
|
|
107
|
+
<p className="text-[10px] font-semibold uppercase tracking-wider text-[rgb(var(--ec-page-text-muted))]">
|
|
108
|
+
Add this {noun} as
|
|
109
|
+
</p>
|
|
110
|
+
<p className="truncate text-xs font-semibold">{resource.name}</p>
|
|
111
|
+
</div>
|
|
112
|
+
<button
|
|
113
|
+
onClick={onCancel}
|
|
114
|
+
aria-label="Cancel"
|
|
115
|
+
className="rounded p-0.5 text-[rgb(var(--ec-icon-color))] hover:bg-[rgb(var(--ec-page-border)/0.5)]"
|
|
116
|
+
>
|
|
117
|
+
<X size={14} />
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div className="grid grid-cols-2 gap-1.5 px-0.5 pb-0.5">
|
|
122
|
+
{OPTIONS.map(({ as, label, hint, Preview }) => (
|
|
123
|
+
<button
|
|
124
|
+
key={as}
|
|
125
|
+
autoFocus={as === 'container'}
|
|
126
|
+
onClick={() => onChoose(as === 'card' ? { as } : { as, withContents: withContents && !!contentsLabel })}
|
|
127
|
+
className="flex flex-col items-center gap-1 rounded-md border px-2 pb-1.5 pt-2 text-xs outline-none transition-colors border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text-muted))] hover:border-[rgb(var(--ec-accent))] hover:bg-[rgb(var(--ec-accent)/0.08)] hover:text-[rgb(var(--ec-accent))] focus-visible:border-[rgb(var(--ec-accent))] focus-visible:text-[rgb(var(--ec-accent))]"
|
|
128
|
+
>
|
|
129
|
+
<Preview />
|
|
130
|
+
<span className="font-semibold">{label}</span>
|
|
131
|
+
<span className="text-center text-[10px] leading-tight text-[rgb(var(--ec-page-text-muted))]">{hint}</span>
|
|
132
|
+
</button>
|
|
133
|
+
))}
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
{contentsLabel && (
|
|
137
|
+
<label className="mt-1 flex items-center gap-2 rounded-md px-2 py-1.5 text-[11px] text-[rgb(var(--ec-page-text-muted))]">
|
|
138
|
+
<input type="checkbox" checked={withContents} onChange={(event) => setWithContents(event.target.checked)} />
|
|
139
|
+
As a container, bring its {contentsLabel} inside, with their messages and data stores
|
|
140
|
+
</label>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { usePeople, type PresenceStore } from '../hooks/presence-store';
|
|
3
|
+
import { STATUS } from './status';
|
|
4
|
+
|
|
5
|
+
/** Asks before deleting a canvas: it's deleted for everyone, for good */
|
|
6
|
+
export default function DeleteCanvasDialog({
|
|
7
|
+
title,
|
|
8
|
+
presence,
|
|
9
|
+
clientId,
|
|
10
|
+
onDelete,
|
|
11
|
+
onClose,
|
|
12
|
+
}: {
|
|
13
|
+
title?: string;
|
|
14
|
+
/** Who has it open (to say it closes for them) */
|
|
15
|
+
presence: PresenceStore | null;
|
|
16
|
+
clientId: number | null;
|
|
17
|
+
/** Deletes it (rejects with what went wrong) */
|
|
18
|
+
onDelete: () => Promise<void>;
|
|
19
|
+
onClose: () => void;
|
|
20
|
+
}) {
|
|
21
|
+
const othersHere = usePeople(presence).filter((peer) => peer.clientId !== clientId).length;
|
|
22
|
+
const [deleting, setDeleting] = useState(false);
|
|
23
|
+
const [error, setError] = useState<string>();
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const onKeyDown = (event: KeyboardEvent) => event.key === 'Escape' && !deleting && onClose();
|
|
27
|
+
window.addEventListener('keydown', onKeyDown);
|
|
28
|
+
return () => window.removeEventListener('keydown', onKeyDown);
|
|
29
|
+
}, [onClose, deleting]);
|
|
30
|
+
|
|
31
|
+
const confirm = async () => {
|
|
32
|
+
setDeleting(true);
|
|
33
|
+
setError(undefined);
|
|
34
|
+
try {
|
|
35
|
+
await onDelete();
|
|
36
|
+
} catch (failure) {
|
|
37
|
+
setError((failure as Error).message);
|
|
38
|
+
setDeleting(false);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4" onClick={() => !deleting && onClose()}>
|
|
44
|
+
<div
|
|
45
|
+
role="alertdialog"
|
|
46
|
+
aria-labelledby="delete-canvas-title"
|
|
47
|
+
onClick={(event) => event.stopPropagation()}
|
|
48
|
+
className="w-full max-w-md rounded-xl border shadow-2xl bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text))]"
|
|
49
|
+
>
|
|
50
|
+
<div className="space-y-2 px-5 py-4">
|
|
51
|
+
<h2 id="delete-canvas-title" className="text-base font-semibold">
|
|
52
|
+
Delete {title ? `"${title}"` : 'this canvas'}?
|
|
53
|
+
</h2>
|
|
54
|
+
<p className="text-sm text-[rgb(var(--ec-page-text-muted))]">
|
|
55
|
+
It's deleted for everyone, with its comments, and can't be brought back.
|
|
56
|
+
{othersHere > 0 &&
|
|
57
|
+
` ${othersHere === 1 ? 'Someone else has' : `${othersHere} others have`} it open: it closes for them too.`}
|
|
58
|
+
</p>
|
|
59
|
+
{error && <p className={`text-sm ${STATUS.danger.text}`}>{error}</p>}
|
|
60
|
+
</div>
|
|
61
|
+
<div className="flex justify-end gap-2 border-t px-5 py-4 border-[rgb(var(--ec-page-border))]">
|
|
62
|
+
<button
|
|
63
|
+
onClick={onClose}
|
|
64
|
+
disabled={deleting}
|
|
65
|
+
className="rounded-lg border px-4 py-2 text-sm font-medium border-[rgb(var(--ec-page-border))] hover:bg-[rgb(var(--ec-page-border)/0.4)] disabled:opacity-50"
|
|
66
|
+
>
|
|
67
|
+
Cancel
|
|
68
|
+
</button>
|
|
69
|
+
<button
|
|
70
|
+
autoFocus
|
|
71
|
+
onClick={confirm}
|
|
72
|
+
disabled={deleting}
|
|
73
|
+
className={`rounded-lg px-4 py-2 text-sm font-medium disabled:opacity-60 ${STATUS.danger.button}`}
|
|
74
|
+
>
|
|
75
|
+
{deleting ? 'Deleting…' : 'Delete canvas'}
|
|
76
|
+
</button>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|