@eventcatalog/core 5.0.0-beta.11 → 5.0.0-beta.12
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/README.md +1 -1
- package/dist/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/log-build.cjs +1 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/{chunk-76DAAJLV.js → chunk-BWC4YKW3.js} +1 -1
- package/dist/{chunk-4YFM63DS.js → chunk-CX2VB7AI.js} +1 -1
- package/dist/{chunk-MFEMKQZ6.js → chunk-FY6MSP6J.js} +1 -1
- package/dist/{chunk-LQZ7IG7B.js → chunk-QB7X34OD.js} +1 -1
- package/dist/{chunk-WM4QYMIA.js → chunk-TSY5CC27.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +1 -1
- package/dist/eventcatalog.js +5 -5
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +2 -0
- package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +34 -3
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +5 -1
- package/eventcatalog/src/components/NavigationProgress.astro +7 -0
- package/eventcatalog/src/features/mcp/apps/architecture-diagram/node-chat.tsx +6 -45
- package/eventcatalog/src/features/mcp/apps/generated/canvas.html +67 -67
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +69 -69
- package/eventcatalog/src/features/studio/agent-choreography.ts +8 -6
- package/eventcatalog/src/features/studio/api/canvases-api.ts +64 -15
- package/eventcatalog/src/features/studio/api/client.ts +6 -1
- package/eventcatalog/src/features/studio/canvas-actions.ts +140 -22
- package/eventcatalog/src/features/studio/canvas-doc.ts +165 -10
- package/eventcatalog/src/features/studio/catalog.ts +15 -10
- package/eventcatalog/src/features/studio/components/CanvasControls.tsx +54 -89
- package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +43 -26
- package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +8 -11
- package/eventcatalog/src/features/studio/components/Comments.tsx +15 -8
- package/eventcatalog/src/features/studio/components/DeleteCanvasDialog.tsx +27 -43
- package/eventcatalog/src/features/studio/components/Dialog.tsx +60 -0
- package/eventcatalog/src/features/studio/components/LeftPanel.tsx +32 -4
- package/eventcatalog/src/features/studio/components/OpenInStudioDialog.tsx +79 -0
- package/eventcatalog/src/features/studio/components/Presence.tsx +47 -38
- package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +323 -99
- package/eventcatalog/src/features/studio/components/canvas-nodes.tsx +33 -17
- package/eventcatalog/src/features/studio/flow-state.ts +29 -0
- package/eventcatalog/src/features/studio/from-visualiser.ts +138 -0
- package/eventcatalog/src/features/studio/grouping.ts +4 -0
- package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +1 -1
- package/eventcatalog/src/features/studio/hooks/use-counter-scale.ts +20 -0
- package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +8 -3
- package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +67 -13
- package/eventcatalog/src/features/studio/layout.ts +29 -15
- package/eventcatalog/src/features/studio/level-layout.ts +145 -0
- package/eventcatalog/src/features/studio/levels.ts +82 -27
- package/eventcatalog/src/features/studio/limits.ts +2 -0
- package/eventcatalog/src/features/studio/node-types.ts +44 -4
- package/eventcatalog/src/features/studio/pages/index.astro +2 -1
- package/eventcatalog/src/features/studio/server/canvas-mcp.ts +1 -1
- package/eventcatalog/src/features/studio/server/runtime.ts +12 -4
- package/eventcatalog/src/utils/node-graphs/domain-levels-node-graph.ts +3 -3
- package/eventcatalog/src/utils/node-graphs/node-resource.ts +67 -0
- package/eventcatalog/src/utils/node-graphs/systems-node-graph.ts +1 -1
- package/package.json +2 -2
|
@@ -66,16 +66,21 @@ export const canBeContainer = (resource: CatalogResource) =>
|
|
|
66
66
|
export const containerTypeFor = (resource: CatalogResource) =>
|
|
67
67
|
resource.collection === 'domains' ? GROUP_TYPES.domain : GROUP_TYPES.system;
|
|
68
68
|
|
|
69
|
-
/** A container's data: the domain or system it stands for, and where it is in the catalog */
|
|
70
|
-
export const catalogGroupData = (resource: CatalogResource) =>
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
69
|
+
/** A container's data: the domain or system it stands for (an external system marked as one), and where it is in the catalog */
|
|
70
|
+
export const catalogGroupData = (resource: CatalogResource) => {
|
|
71
|
+
const key = resource.collection === 'domains' ? 'domain' : 'system';
|
|
72
|
+
const { scope } = (resource.node.data[key] ?? {}) as { scope?: string };
|
|
73
|
+
return {
|
|
74
|
+
[key]: {
|
|
75
|
+
id: resource.id,
|
|
76
|
+
name: resource.name,
|
|
77
|
+
version: resource.version,
|
|
78
|
+
summary: resource.summary,
|
|
79
|
+
...(scope && { scope }),
|
|
80
|
+
},
|
|
81
|
+
catalog: { key: resource.key, url: resource.url, version: resource.version } satisfies CatalogLink,
|
|
82
|
+
};
|
|
83
|
+
};
|
|
79
84
|
|
|
80
85
|
/** What a domain or system contains in the catalog: a domain's systems and services, a system's services */
|
|
81
86
|
export const getContents = (
|
|
@@ -1,51 +1,17 @@
|
|
|
1
|
-
import { memo
|
|
2
|
-
import { useReactFlow
|
|
1
|
+
import { memo } from 'react';
|
|
2
|
+
import { useReactFlow } from '@xyflow/react';
|
|
3
3
|
import { Maximize, MessageCircle, Redo, SendToBack, StickyNote, Undo, ZoomIn, ZoomOut } from 'lucide-react';
|
|
4
|
+
import { DIAGRAM_FIT_VIEW_OPTIONS, Toolbar, ToolbarButton, ToolbarDivider, ZoomLevel } from '@eventcatalog/visualiser';
|
|
4
5
|
import { LEVELS, type Level } from '../levels';
|
|
5
6
|
|
|
6
|
-
// The canvas's bottom bar: layout, view, levels of detail, undo, comments and notes
|
|
7
|
+
// The canvas's bottom bar: layout, view, levels of detail, undo, comments and notes. Built from the visualiser's
|
|
8
|
+
// toolbar, so it looks and works like a diagram's bar (in the same places: the view on the left, then the levels).
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
// Fitted like the visualiser fits a diagram
|
|
11
|
+
export const FIT_VIEW_OPTIONS = DIAGRAM_FIT_VIEW_OPTIONS;
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
onClick,
|
|
13
|
-
disabled,
|
|
14
|
-
active,
|
|
15
|
-
children,
|
|
16
|
-
}: {
|
|
17
|
-
label: string;
|
|
18
|
-
onClick: () => void;
|
|
19
|
-
disabled?: boolean;
|
|
20
|
-
active?: boolean;
|
|
21
|
-
children: ReactNode;
|
|
22
|
-
}) {
|
|
23
|
-
return (
|
|
24
|
-
<button
|
|
25
|
-
title={label}
|
|
26
|
-
aria-label={label}
|
|
27
|
-
onClick={onClick}
|
|
28
|
-
disabled={disabled}
|
|
29
|
-
className={`rounded-full p-2 transition-colors ${
|
|
30
|
-
disabled
|
|
31
|
-
? 'cursor-not-allowed opacity-40'
|
|
32
|
-
: active
|
|
33
|
-
? 'bg-[rgb(var(--ec-accent-subtle))] text-[rgb(var(--ec-accent))]'
|
|
34
|
-
: 'text-[rgb(var(--ec-icon-color))] hover:bg-[rgb(var(--ec-page-border)/0.5)] hover:text-[rgb(var(--ec-icon-hover))]'
|
|
35
|
-
}`}
|
|
36
|
-
>
|
|
37
|
-
{children}
|
|
38
|
-
</button>
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Isolated, so the bar only re-renders when the zoom changes (not on every pan) */
|
|
43
|
-
function ZoomLevel() {
|
|
44
|
-
const zoom = useStore((state) => Math.round(state.transform[2] * 100));
|
|
45
|
-
return <div className="w-12 text-center text-xs font-medium tabular-nums text-[rgb(var(--ec-page-text-muted))]">{zoom}%</div>;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const Divider = () => <div className="mx-1 h-6 w-px bg-[rgb(var(--ec-page-border))]" />;
|
|
13
|
+
// Why comments can't be added on L1 or L2 (they're pinned to the canvas, L3)
|
|
14
|
+
const EDIT_IN_L3 = 'Only on L3, where comments are pinned';
|
|
49
15
|
|
|
50
16
|
function LevelSwitch({
|
|
51
17
|
level,
|
|
@@ -58,29 +24,18 @@ function LevelSwitch({
|
|
|
58
24
|
}) {
|
|
59
25
|
return (
|
|
60
26
|
<div role="group" aria-label="Level of detail" className="flex items-center gap-0.5">
|
|
61
|
-
{LEVELS.map(({ level: value, description }) =>
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
reason
|
|
74
|
-
? 'cursor-not-allowed opacity-40'
|
|
75
|
-
: active
|
|
76
|
-
? 'bg-[rgb(var(--ec-accent-subtle))] text-[rgb(var(--ec-accent))]'
|
|
77
|
-
: 'text-[rgb(var(--ec-page-text-muted))] hover:bg-[rgb(var(--ec-page-border)/0.5)]'
|
|
78
|
-
}`}
|
|
79
|
-
>
|
|
80
|
-
L{value}
|
|
81
|
-
</button>
|
|
82
|
-
);
|
|
83
|
-
})}
|
|
27
|
+
{LEVELS.map(({ level: value, description }) => (
|
|
28
|
+
<ToolbarButton
|
|
29
|
+
key={value}
|
|
30
|
+
label={`Level ${value}: ${description}`}
|
|
31
|
+
hint={unavailable[value]}
|
|
32
|
+
active={value === level}
|
|
33
|
+
disabled={!!unavailable[value]}
|
|
34
|
+
onClick={() => onChange(value)}
|
|
35
|
+
>
|
|
36
|
+
<span className="text-xs font-semibold">L{value}</span>
|
|
37
|
+
</ToolbarButton>
|
|
38
|
+
))}
|
|
84
39
|
</div>
|
|
85
40
|
);
|
|
86
41
|
}
|
|
@@ -102,7 +57,7 @@ export default memo(function CanvasControls({
|
|
|
102
57
|
level: Level;
|
|
103
58
|
onLevelChange: (level: Level) => void;
|
|
104
59
|
unavailableLevels: Partial<Record<Level, string | undefined>>;
|
|
105
|
-
/** L1 and L2 are
|
|
60
|
+
/** L3, the canvas itself (comments are there; L1 and L2 are arranged on their own) */
|
|
106
61
|
editable: boolean;
|
|
107
62
|
onReorder: () => void;
|
|
108
63
|
canUndo: boolean;
|
|
@@ -116,39 +71,49 @@ export default memo(function CanvasControls({
|
|
|
116
71
|
const { fitView, zoomIn, zoomOut } = useReactFlow();
|
|
117
72
|
|
|
118
73
|
return (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
74
|
+
// Where the visualiser's bar is (React Flow's bottom-center panel)
|
|
75
|
+
<div className="absolute bottom-[15px] left-1/2 z-10 -translate-x-1/2">
|
|
76
|
+
<Toolbar>
|
|
77
|
+
<ToolbarButton
|
|
78
|
+
label={editable ? "Reorder diagram (the visualiser's layout)" : `Reorder L${level} (the visualiser's layout)`}
|
|
79
|
+
onClick={onReorder}
|
|
80
|
+
>
|
|
122
81
|
<SendToBack className="h-4 w-4" />
|
|
123
|
-
</
|
|
124
|
-
<
|
|
82
|
+
</ToolbarButton>
|
|
83
|
+
<ToolbarButton label="Fit view" onClick={() => void fitView({ ...FIT_VIEW_OPTIONS, duration: 500 })}>
|
|
125
84
|
<Maximize className="h-4 w-4" />
|
|
126
|
-
</
|
|
127
|
-
<
|
|
85
|
+
</ToolbarButton>
|
|
86
|
+
<ToolbarButton label="Zoom out" onClick={() => void zoomOut({ duration: 200 })}>
|
|
128
87
|
<ZoomOut className="h-4 w-4" />
|
|
129
|
-
</
|
|
88
|
+
</ToolbarButton>
|
|
130
89
|
<ZoomLevel />
|
|
131
|
-
<
|
|
90
|
+
<ToolbarButton label="Zoom in" onClick={() => void zoomIn({ duration: 200 })}>
|
|
132
91
|
<ZoomIn className="h-4 w-4" />
|
|
133
|
-
</
|
|
134
|
-
<
|
|
92
|
+
</ToolbarButton>
|
|
93
|
+
<ToolbarDivider />
|
|
135
94
|
<LevelSwitch level={level} onChange={onLevelChange} unavailable={unavailableLevels} />
|
|
136
|
-
<
|
|
137
|
-
<
|
|
95
|
+
<ToolbarDivider />
|
|
96
|
+
<ToolbarButton label="Undo (⌘Z)" hint={canUndo ? undefined : 'Nothing to undo'} disabled={!canUndo} onClick={onUndo}>
|
|
138
97
|
<Undo className="h-4 w-4" />
|
|
139
|
-
</
|
|
140
|
-
<
|
|
98
|
+
</ToolbarButton>
|
|
99
|
+
<ToolbarButton label="Redo (⇧⌘Z)" hint={canRedo ? undefined : 'Nothing to redo'} disabled={!canRedo} onClick={onRedo}>
|
|
141
100
|
<Redo className="h-4 w-4" />
|
|
142
|
-
</
|
|
143
|
-
<
|
|
144
|
-
<
|
|
101
|
+
</ToolbarButton>
|
|
102
|
+
<ToolbarDivider />
|
|
103
|
+
<ToolbarButton
|
|
104
|
+
label="Comment (C)"
|
|
105
|
+
hint={editable ? undefined : EDIT_IN_L3}
|
|
106
|
+
active={commentMode}
|
|
107
|
+
disabled={!editable}
|
|
108
|
+
onClick={onToggleCommentMode}
|
|
109
|
+
>
|
|
145
110
|
<MessageCircle className="h-4 w-4" />
|
|
146
|
-
</
|
|
111
|
+
</ToolbarButton>
|
|
147
112
|
{/* Notes can be added on every level, and are shown on the level they're added on */}
|
|
148
|
-
<
|
|
113
|
+
<ToolbarButton label={editable ? 'Add sticky note' : `Add sticky note to L${level}`} onClick={onAddNote}>
|
|
149
114
|
<StickyNote className="h-4 w-4 text-yellow-500" />
|
|
150
|
-
</
|
|
151
|
-
</
|
|
115
|
+
</ToolbarButton>
|
|
116
|
+
</Toolbar>
|
|
152
117
|
</div>
|
|
153
118
|
);
|
|
154
119
|
});
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { memo, useEffect, useRef, useState, type ReactNode } from 'react';
|
|
2
|
-
import { Bot,
|
|
2
|
+
import { Bot, Link2, MoreVertical, Pencil, Plus, Trash2, UserPlus } from 'lucide-react';
|
|
3
|
+
import {
|
|
4
|
+
DIAGRAM_MENU,
|
|
5
|
+
DIAGRAM_MENU_BUTTON_ICON,
|
|
6
|
+
DIAGRAM_MENU_ITEM,
|
|
7
|
+
DIAGRAM_MENU_ITEM_ICON,
|
|
8
|
+
DIAGRAM_MENU_SEPARATOR,
|
|
9
|
+
DIAGRAM_MENU_TITLE,
|
|
10
|
+
} from '@eventcatalog/visualiser';
|
|
3
11
|
import { usePeople, type Peer, type PresenceStore } from '../hooks/presence-store';
|
|
4
12
|
import type { WebMcpStatus } from '../hooks/use-canvas-webmcp';
|
|
5
13
|
import type { Status, Transport } from '../hooks/use-studio-flow';
|
|
@@ -9,17 +17,17 @@ import { CONNECTION_DOT, STATUS } from './status';
|
|
|
9
17
|
import Picture from './Picture';
|
|
10
18
|
|
|
11
19
|
/**
|
|
12
|
-
* The canvas's header
|
|
13
|
-
*
|
|
20
|
+
* The canvas's header: two small bars over the canvas. On the left the canvas (its title, and a menu), on the right
|
|
21
|
+
* who's here, connecting an agent and sharing. They look like the visualiser's menu at the top left of a diagram
|
|
22
|
+
* (its styles, `DIAGRAM_MENU_*`), so a diagram opened in Studio feels the same.
|
|
14
23
|
*/
|
|
15
24
|
|
|
16
25
|
const barClass =
|
|
17
|
-
'pointer-events-auto flex h-
|
|
26
|
+
'pointer-events-auto flex h-[42px] items-center rounded-md border bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))]';
|
|
18
27
|
// The header lets the pointer through to the canvas around its bars, so its menus take it back
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
const menuItemClass = `${menuItemLayout} hover:bg-[rgb(var(--ec-page-border)/0.5)]`;
|
|
28
|
+
const popoverClass = 'pointer-events-auto absolute top-full mt-2';
|
|
29
|
+
const menuClass = `${popoverClass} ${DIAGRAM_MENU}`;
|
|
30
|
+
const menuItemClass = `${DIAGRAM_MENU_ITEM} w-full text-left`;
|
|
23
31
|
const MAX_AVATARS = 4;
|
|
24
32
|
|
|
25
33
|
/** Closes a popover on outside clicks and Escape */
|
|
@@ -87,7 +95,7 @@ function CanvasMenu({
|
|
|
87
95
|
|
|
88
96
|
return (
|
|
89
97
|
<div ref={menu.ref} className="relative">
|
|
90
|
-
<div className={`${barClass} gap-1 pl-
|
|
98
|
+
<div className={`${barClass} gap-1.5 pl-4 pr-1.5`}>
|
|
91
99
|
<span title={statusLabel} className={`h-2 w-2 shrink-0 rounded-full ${statusColor}`} />
|
|
92
100
|
<input
|
|
93
101
|
ref={titleInput}
|
|
@@ -101,15 +109,15 @@ function CanvasMenu({
|
|
|
101
109
|
}}
|
|
102
110
|
onKeyDown={(e) => e.key === 'Enter' && e.currentTarget.blur()}
|
|
103
111
|
size={Math.max(12, (draft ?? title ?? 'Untitled canvas').length)}
|
|
104
|
-
className=
|
|
112
|
+
className={`${DIAGRAM_MENU_TITLE} max-w-64 truncate rounded-md bg-transparent px-1.5 py-1 placeholder:text-[rgb(var(--ec-page-text-muted))] hover:bg-[rgb(var(--ec-page-border)/0.4)] focus:bg-[rgb(var(--ec-input-bg))] focus:outline-none`}
|
|
105
113
|
/>
|
|
106
114
|
{children}
|
|
107
115
|
<button
|
|
108
116
|
aria-label="Canvas menu"
|
|
109
117
|
onClick={() => menu.setOpen((open) => !open)}
|
|
110
|
-
className="rounded-md p-1
|
|
118
|
+
className="group rounded-md p-1 hover:bg-[rgb(var(--ec-accent-subtle))]"
|
|
111
119
|
>
|
|
112
|
-
<
|
|
120
|
+
<MoreVertical className={DIAGRAM_MENU_BUTTON_ICON} />
|
|
113
121
|
</button>
|
|
114
122
|
</div>
|
|
115
123
|
{menu.open && (
|
|
@@ -122,7 +130,7 @@ function CanvasMenu({
|
|
|
122
130
|
titleInput.current?.select();
|
|
123
131
|
}}
|
|
124
132
|
>
|
|
125
|
-
<Pencil
|
|
133
|
+
<Pencil className={DIAGRAM_MENU_ITEM_ICON} /> Rename canvas
|
|
126
134
|
</button>
|
|
127
135
|
<button
|
|
128
136
|
className={menuItemClass}
|
|
@@ -131,29 +139,30 @@ function CanvasMenu({
|
|
|
131
139
|
menu.setOpen(false);
|
|
132
140
|
}}
|
|
133
141
|
>
|
|
134
|
-
<Link2
|
|
142
|
+
<Link2 className={DIAGRAM_MENU_ITEM_ICON} /> Copy link
|
|
135
143
|
</button>
|
|
136
144
|
{newCanvasUrl && (
|
|
137
145
|
<a className={menuItemClass} href={newCanvasUrl}>
|
|
138
|
-
<Plus
|
|
146
|
+
<Plus className={DIAGRAM_MENU_ITEM_ICON} /> New canvas
|
|
139
147
|
</a>
|
|
140
148
|
)}
|
|
141
149
|
{onDelete && (
|
|
142
150
|
<>
|
|
143
|
-
<div className=
|
|
151
|
+
<div className={DIAGRAM_MENU_SEPARATOR} />
|
|
144
152
|
<button
|
|
145
|
-
|
|
153
|
+
// Laid out like the other items, in the theme's danger colour
|
|
154
|
+
className={`flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-left text-xs transition-colors ${STATUS.danger.text} ${STATUS.danger.hover}`}
|
|
146
155
|
onClick={() => {
|
|
147
156
|
menu.setOpen(false);
|
|
148
157
|
onDelete();
|
|
149
158
|
}}
|
|
150
159
|
>
|
|
151
|
-
<Trash2
|
|
160
|
+
<Trash2 className="h-3.5 w-3.5 shrink-0" /> Delete canvas
|
|
152
161
|
</button>
|
|
153
162
|
</>
|
|
154
163
|
)}
|
|
155
|
-
<div className=
|
|
156
|
-
<p className="flex items-center gap-2 px-
|
|
164
|
+
<div className={DIAGRAM_MENU_SEPARATOR} />
|
|
165
|
+
<p className="flex items-center gap-2 px-3 py-1.5 text-[11px] text-[rgb(var(--ec-page-text-muted))]">
|
|
157
166
|
<span className={`h-1.5 w-1.5 rounded-full ${statusColor}`} />
|
|
158
167
|
{statusLabel}
|
|
159
168
|
</p>
|
|
@@ -188,7 +197,9 @@ function YouAvatar({ peer, onRename }: { peer: Peer; onRename?: (name: string) =
|
|
|
188
197
|
<Avatar peer={peer} />
|
|
189
198
|
</button>
|
|
190
199
|
{popover.open && (
|
|
191
|
-
<div
|
|
200
|
+
<div
|
|
201
|
+
className={`${popoverClass} right-0 z-50 w-60 space-y-2 rounded-lg border p-3 shadow-xl bg-[rgb(var(--ec-page-bg))] border-[rgb(var(--ec-page-border))]`}
|
|
202
|
+
>
|
|
192
203
|
<label className="block space-y-1.5 text-[11px] font-medium text-[rgb(var(--ec-page-text-muted))]">
|
|
193
204
|
Your name on this canvas
|
|
194
205
|
<input
|
|
@@ -319,9 +330,11 @@ export default memo(function CanvasHeader({
|
|
|
319
330
|
}) {
|
|
320
331
|
const peers = usePeople(presence);
|
|
321
332
|
const agentsHere = peers.some((peer) => peer.agent);
|
|
333
|
+
// Nobody else here, and signed in (so not shown as "you" to rename): no list of people
|
|
334
|
+
const showPeople = !!onRename || peers.some((peer) => peer.clientId !== clientId);
|
|
322
335
|
|
|
323
336
|
return (
|
|
324
|
-
<div className="pointer-events-none absolute left-
|
|
337
|
+
<div className="pointer-events-none absolute left-4 right-4 top-4 z-10 flex items-start justify-between gap-3">
|
|
325
338
|
<CanvasMenu
|
|
326
339
|
title={title}
|
|
327
340
|
status={status}
|
|
@@ -335,10 +348,14 @@ export default memo(function CanvasHeader({
|
|
|
335
348
|
</CanvasMenu>
|
|
336
349
|
|
|
337
350
|
<div className={`${barClass} gap-2 px-1.5`}>
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
351
|
+
{showPeople && (
|
|
352
|
+
<>
|
|
353
|
+
<div className="pl-1">
|
|
354
|
+
<People peers={peers} clientId={clientId} onJumpTo={onJumpTo} onRename={onRename} />
|
|
355
|
+
</div>
|
|
356
|
+
<div className="h-5 w-px bg-[rgb(var(--ec-page-border))]" />
|
|
357
|
+
</>
|
|
358
|
+
)}
|
|
342
359
|
<button
|
|
343
360
|
onClick={onConnectAgent}
|
|
344
361
|
title={
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { memo, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { Check, ChevronDown } from 'lucide-react';
|
|
3
|
+
import { DIAGRAM_MENU, DIAGRAM_MENU_ITEM, DIAGRAM_MENU_SEPARATOR } from '@eventcatalog/visualiser';
|
|
3
4
|
import { CANVAS_STATUSES, type CanvasStatus, type StatusChange } from '../canvas-doc';
|
|
4
5
|
import { timeAgo } from './Comments';
|
|
5
6
|
import { CANVAS_STATUS_LOOK } from './status';
|
|
@@ -58,18 +59,14 @@ export default memo(function CanvasStatusMenu({
|
|
|
58
59
|
<ChevronDown size={12} />
|
|
59
60
|
</button>
|
|
60
61
|
{open && (
|
|
61
|
-
<div className=
|
|
62
|
-
<p className="px-
|
|
62
|
+
<div className={`absolute left-0 top-full mt-2 w-72 ${DIAGRAM_MENU}`}>
|
|
63
|
+
<p className="px-3 pb-1 pt-1.5 text-[11px] font-semibold uppercase tracking-wide text-[rgb(var(--ec-page-text-muted))]">
|
|
63
64
|
Status
|
|
64
65
|
</p>
|
|
65
66
|
{CANVAS_STATUSES.map((value) => {
|
|
66
67
|
const option = CANVAS_STATUS_LOOK[value];
|
|
67
68
|
return (
|
|
68
|
-
<button
|
|
69
|
-
key={value}
|
|
70
|
-
onClick={() => choose(value)}
|
|
71
|
-
className="flex w-full items-center gap-2.5 rounded-lg px-2.5 py-1.5 text-left hover:bg-[rgb(var(--ec-page-border)/0.5)]"
|
|
72
|
-
>
|
|
69
|
+
<button key={value} onClick={() => choose(value)} className={`${DIAGRAM_MENU_ITEM} w-full text-left`}>
|
|
73
70
|
<span className={`h-2 w-2 shrink-0 rounded-full ${option.dot}`} />
|
|
74
71
|
<span className="min-w-0 flex-1">
|
|
75
72
|
<span className="block text-xs font-medium">{option.label}</span>
|
|
@@ -79,7 +76,7 @@ export default memo(function CanvasStatusMenu({
|
|
|
79
76
|
</button>
|
|
80
77
|
);
|
|
81
78
|
})}
|
|
82
|
-
<div className="px-
|
|
79
|
+
<div className="px-3 py-2">
|
|
83
80
|
<input
|
|
84
81
|
value={note}
|
|
85
82
|
onChange={(event) => setNote(event.target.value)}
|
|
@@ -90,11 +87,11 @@ export default memo(function CanvasStatusMenu({
|
|
|
90
87
|
</div>
|
|
91
88
|
{recent.length > 0 && (
|
|
92
89
|
<>
|
|
93
|
-
<div className=
|
|
94
|
-
<p className="px-
|
|
90
|
+
<div className={DIAGRAM_MENU_SEPARATOR} />
|
|
91
|
+
<p className="px-3 pb-1 pt-1.5 text-[11px] font-semibold uppercase tracking-wide text-[rgb(var(--ec-page-text-muted))]">
|
|
95
92
|
History
|
|
96
93
|
</p>
|
|
97
|
-
<ul className="max-h-48 space-y-1.5 overflow-y-auto px-
|
|
94
|
+
<ul className="max-h-48 space-y-1.5 overflow-y-auto px-3 pb-2">
|
|
98
95
|
{recent.map((change) => (
|
|
99
96
|
<li key={`${change.at}-${change.status}`} className="text-[11px]">
|
|
100
97
|
<span className="flex items-center gap-1.5">
|
|
@@ -10,10 +10,11 @@ import {
|
|
|
10
10
|
type ReactNode,
|
|
11
11
|
} from 'react';
|
|
12
12
|
import { createPortal } from 'react-dom';
|
|
13
|
-
import { useInternalNode, useReactFlow,
|
|
13
|
+
import { useInternalNode, useReactFlow, ViewportPortal, type Node, type XYPosition } from '@xyflow/react';
|
|
14
14
|
import { Bot, Check, Maximize2, ClipboardPaste, Copy, CopyPlus, MessageCircle, Pencil, RotateCcw, Trash2, X } from 'lucide-react';
|
|
15
15
|
import { getAbsolutePosition } from '../grouping';
|
|
16
16
|
import type { Author, CommentAnchor, Thread } from '../hooks/use-comments';
|
|
17
|
+
import { COUNTER_SCALE, useCounterScale } from '../hooks/use-counter-scale';
|
|
17
18
|
import { isWrittenOnCanvas } from '../node-types';
|
|
18
19
|
import { STATUS } from './status';
|
|
19
20
|
import Picture from './Picture';
|
|
@@ -458,13 +459,19 @@ function ThreadDialog({
|
|
|
458
459
|
);
|
|
459
460
|
}
|
|
460
461
|
|
|
461
|
-
/**
|
|
462
|
-
|
|
462
|
+
/**
|
|
463
|
+
* Where a pin goes on the canvas, following only the node it's on (if it's on one), counter-scaled (by the layer's
|
|
464
|
+
* COUNTER_SCALE, so zooming doesn't re-render it)
|
|
465
|
+
*/
|
|
466
|
+
function PinAt({ anchor, zIndex, children }: { anchor: CommentAnchor; zIndex: number; children: ReactNode }) {
|
|
463
467
|
const node = useInternalNode(anchor.nodeId ?? '');
|
|
464
468
|
const origin = anchor.nodeId && anchor.offset ? node?.internals.positionAbsolute : undefined;
|
|
465
469
|
const { x, y } = origin && anchor.offset ? { x: origin.x + anchor.offset.x, y: origin.y + anchor.offset.y } : anchor.position;
|
|
466
470
|
return (
|
|
467
|
-
<div
|
|
471
|
+
<div
|
|
472
|
+
className="absolute"
|
|
473
|
+
style={{ left: x, top: y, transform: `scale(var(${COUNTER_SCALE}, 1))`, transformOrigin: '0 0', zIndex }}
|
|
474
|
+
>
|
|
468
475
|
{/* The pin's tip (bottom left corner) sits on the anchor point, and it grows up and right as it opens */}
|
|
469
476
|
<div className="nopan nodrag nowheel pointer-events-auto absolute bottom-0 left-0">{children}</div>
|
|
470
477
|
</div>
|
|
@@ -511,7 +518,8 @@ export const CommentLayer = memo(function CommentLayer({
|
|
|
511
518
|
onMove: (threadId: string, anchor: CommentAnchor) => void;
|
|
512
519
|
onMoveDraft: (anchor: CommentAnchor) => void;
|
|
513
520
|
}) {
|
|
514
|
-
const
|
|
521
|
+
const layer = useRef<HTMLDivElement>(null);
|
|
522
|
+
useCounterScale(layer);
|
|
515
523
|
const { screenToFlowPosition, getInternalNode } = useReactFlow();
|
|
516
524
|
// A pin being dragged stays here until it's dropped, then moves for everyone
|
|
517
525
|
const [moving, setMoving] = useState<{ threadId: string; anchor: CommentAnchor } | null>(null);
|
|
@@ -539,7 +547,7 @@ export const CommentLayer = memo(function CommentLayer({
|
|
|
539
547
|
return (
|
|
540
548
|
<ViewportPortal>
|
|
541
549
|
{/* Above nodes (selected ones are raised to 1000) and other people's pointers */}
|
|
542
|
-
<div className="absolute left-0 top-0" style={{ zIndex: 2001 }}>
|
|
550
|
+
<div ref={layer} className="absolute left-0 top-0" style={{ zIndex: 2001 }}>
|
|
543
551
|
{visible.map((thread) => {
|
|
544
552
|
const isOpen = thread.id === openThreadId;
|
|
545
553
|
return (
|
|
@@ -552,7 +560,6 @@ export const CommentLayer = memo(function CommentLayer({
|
|
|
552
560
|
? { position: { x: thread.position.x + selectionOffset.x, y: thread.position.y + selectionOffset.y } }
|
|
553
561
|
: thread
|
|
554
562
|
}
|
|
555
|
-
zoom={zoom}
|
|
556
563
|
zIndex={isOpen ? 2 : 1}
|
|
557
564
|
>
|
|
558
565
|
<Pin
|
|
@@ -605,7 +612,7 @@ export const CommentLayer = memo(function CommentLayer({
|
|
|
605
612
|
)}
|
|
606
613
|
|
|
607
614
|
{draft && (
|
|
608
|
-
<PinAt anchor={draft}
|
|
615
|
+
<PinAt anchor={draft} zIndex={3}>
|
|
609
616
|
<Pin author={author} active onDrag={(tip, phase) => onMoveDraft(anchorFor(tip, phase))} />
|
|
610
617
|
<FloatingPanel>
|
|
611
618
|
<Composer placeholder="Add a comment" onSubmit={onCreate} onCancel={onCancelDraft} />
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useState } from 'react';
|
|
2
2
|
import { usePeople, type PresenceStore } from '../hooks/presence-store';
|
|
3
|
+
import Dialog, { SECONDARY_BUTTON } from './Dialog';
|
|
3
4
|
import { STATUS } from './status';
|
|
4
5
|
|
|
5
6
|
/** Asks before deleting a canvas: it's deleted for everyone, for good */
|
|
@@ -22,12 +23,6 @@ export default function DeleteCanvasDialog({
|
|
|
22
23
|
const [deleting, setDeleting] = useState(false);
|
|
23
24
|
const [error, setError] = useState<string>();
|
|
24
25
|
|
|
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
26
|
const confirm = async () => {
|
|
32
27
|
setDeleting(true);
|
|
33
28
|
setError(undefined);
|
|
@@ -40,42 +35,31 @@ export default function DeleteCanvasDialog({
|
|
|
40
35
|
};
|
|
41
36
|
|
|
42
37
|
return (
|
|
43
|
-
<
|
|
44
|
-
<div
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
className="
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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>
|
|
38
|
+
<Dialog role="alertdialog" labelledBy="delete-canvas-title" busy={deleting} onClose={onClose}>
|
|
39
|
+
<div className="space-y-2 px-5 py-4">
|
|
40
|
+
<h2 id="delete-canvas-title" className="text-base font-semibold">
|
|
41
|
+
Delete {title ? `"${title}"` : 'this canvas'}?
|
|
42
|
+
</h2>
|
|
43
|
+
<p className="text-sm text-[rgb(var(--ec-page-text-muted))]">
|
|
44
|
+
It's deleted for everyone, with its comments, and can't be brought back.
|
|
45
|
+
{othersHere > 0 &&
|
|
46
|
+
` ${othersHere === 1 ? 'Someone else has' : `${othersHere} others have`} it open: it closes for them too.`}
|
|
47
|
+
</p>
|
|
48
|
+
{error && <p className={`text-sm ${STATUS.danger.text}`}>{error}</p>}
|
|
49
|
+
</div>
|
|
50
|
+
<div className="flex justify-end gap-2 border-t px-5 py-4 border-[rgb(var(--ec-page-border))]">
|
|
51
|
+
<button onClick={onClose} disabled={deleting} className={SECONDARY_BUTTON}>
|
|
52
|
+
Cancel
|
|
53
|
+
</button>
|
|
54
|
+
<button
|
|
55
|
+
autoFocus
|
|
56
|
+
onClick={confirm}
|
|
57
|
+
disabled={deleting}
|
|
58
|
+
className={`rounded-lg px-4 py-2 text-sm font-medium disabled:opacity-60 ${STATUS.danger.button}`}
|
|
59
|
+
>
|
|
60
|
+
{deleting ? 'Deleting…' : 'Delete canvas'}
|
|
61
|
+
</button>
|
|
78
62
|
</div>
|
|
79
|
-
</
|
|
63
|
+
</Dialog>
|
|
80
64
|
);
|
|
81
65
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useEffect, useRef, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Studio's dialogs: a card over the page, closed by Escape or clicking outside it (not while it's busy). Focus stays
|
|
5
|
+
* in it: anything that moves focus outside (e.g. the menu it was opened from, closing) sends it back to its first
|
|
6
|
+
* field or button.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const SECONDARY_BUTTON =
|
|
10
|
+
'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';
|
|
11
|
+
export const PRIMARY_BUTTON =
|
|
12
|
+
'rounded-lg px-4 py-2 text-sm font-medium bg-[rgb(var(--ec-button-bg))] text-[rgb(var(--ec-button-text))] hover:bg-[rgb(var(--ec-button-bg-hover))] disabled:opacity-60';
|
|
13
|
+
|
|
14
|
+
export default function Dialog({
|
|
15
|
+
labelledBy,
|
|
16
|
+
role = 'dialog',
|
|
17
|
+
busy = false,
|
|
18
|
+
onClose,
|
|
19
|
+
children,
|
|
20
|
+
}: {
|
|
21
|
+
/** The id of its heading */
|
|
22
|
+
labelledBy: string;
|
|
23
|
+
/** `alertdialog` when it asks to confirm something that can't be undone */
|
|
24
|
+
role?: 'dialog' | 'alertdialog';
|
|
25
|
+
/** Doing what was asked: it can't be closed until it's done */
|
|
26
|
+
busy?: boolean;
|
|
27
|
+
onClose: () => void;
|
|
28
|
+
children: ReactNode;
|
|
29
|
+
}) {
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const onKeyDown = (event: KeyboardEvent) => event.key === 'Escape' && !busy && onClose();
|
|
32
|
+
window.addEventListener('keydown', onKeyDown);
|
|
33
|
+
return () => window.removeEventListener('keydown', onKeyDown);
|
|
34
|
+
}, [onClose, busy]);
|
|
35
|
+
|
|
36
|
+
const card = useRef<HTMLDivElement>(null);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
const keepFocus = (event: FocusEvent) => {
|
|
39
|
+
if (!card.current || card.current.contains(event.target as globalThis.Node)) return;
|
|
40
|
+
card.current.querySelector<HTMLElement>('input, button')?.focus();
|
|
41
|
+
};
|
|
42
|
+
document.addEventListener('focusin', keepFocus);
|
|
43
|
+
return () => document.removeEventListener('focusin', keepFocus);
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4" onClick={() => !busy && onClose()}>
|
|
48
|
+
<div
|
|
49
|
+
ref={card}
|
|
50
|
+
role={role}
|
|
51
|
+
aria-modal="true"
|
|
52
|
+
aria-labelledby={labelledBy}
|
|
53
|
+
onClick={(event) => event.stopPropagation()}
|
|
54
|
+
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))]"
|
|
55
|
+
>
|
|
56
|
+
{children}
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|