@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
|
@@ -2,7 +2,8 @@ import { memo, useCallback, useMemo, useState, type ReactNode } from 'react';
|
|
|
2
2
|
import { Check, ChevronRight, Plus, Search } from 'lucide-react';
|
|
3
3
|
import type { CatalogResource } from '../catalog-resources';
|
|
4
4
|
import { catalogNodeData } from '../catalog';
|
|
5
|
-
import { NODE_CATEGORIES, nodeDefinitions } from '../node-types';
|
|
5
|
+
import { isShownOnLevel, NODE_CATEGORIES, nodeDefinitions } from '../node-types';
|
|
6
|
+
import type { Level } from '../levels';
|
|
6
7
|
import type { Thread } from '../hooks/use-comments';
|
|
7
8
|
import { CommentList } from './Comments';
|
|
8
9
|
import { useDragGhost, type StartDrag } from './DragGhost';
|
|
@@ -70,6 +71,8 @@ type Tab = 'components' | 'catalog' | 'comments';
|
|
|
70
71
|
|
|
71
72
|
/** Components and catalog resources to drag onto the canvas, and the comments on it */
|
|
72
73
|
export default memo(function LeftPanel({
|
|
74
|
+
level,
|
|
75
|
+
onShowAll,
|
|
73
76
|
resources,
|
|
74
77
|
keysOnCanvas,
|
|
75
78
|
onAddComponent,
|
|
@@ -79,6 +82,10 @@ export default memo(function LeftPanel({
|
|
|
79
82
|
onToggleResolved,
|
|
80
83
|
onOpenThread,
|
|
81
84
|
}: {
|
|
85
|
+
/** The level shown: only what it shows is listed, so what's added stays on it */
|
|
86
|
+
level: Level;
|
|
87
|
+
/** Goes to L3, which shows everything */
|
|
88
|
+
onShowAll: () => void;
|
|
82
89
|
resources: CatalogResource[];
|
|
83
90
|
/** The catalog resources on the canvas */
|
|
84
91
|
keysOnCanvas: Set<string>;
|
|
@@ -94,6 +101,10 @@ export default memo(function LeftPanel({
|
|
|
94
101
|
const { startDrag, ghost } = useDragGhost();
|
|
95
102
|
const sections = useCollapsed();
|
|
96
103
|
const openThreads = threads.filter((thread) => !thread.resolved).length;
|
|
104
|
+
const shownResources = useMemo(
|
|
105
|
+
() => (level === 3 ? resources : resources.filter((resource) => isShownOnLevel(resource.node.type, level))),
|
|
106
|
+
[resources, level]
|
|
107
|
+
);
|
|
97
108
|
const tabClass = (active: boolean) =>
|
|
98
109
|
`flex flex-1 items-center justify-center gap-1 whitespace-nowrap rounded-md px-1.5 py-1 text-xs font-medium ${
|
|
99
110
|
active ? 'bg-[rgb(var(--ec-page-bg))] shadow-sm' : 'text-[rgb(var(--ec-page-text-muted))]'
|
|
@@ -117,10 +128,11 @@ export default memo(function LeftPanel({
|
|
|
117
128
|
)}
|
|
118
129
|
</button>
|
|
119
130
|
</div>
|
|
120
|
-
{tab
|
|
131
|
+
{tab !== 'comments' && level !== 3 && <LevelHint level={level} onShowAll={onShowAll} />}
|
|
132
|
+
{tab === 'components' && <ComponentList level={level} onAdd={onAddComponent} startDrag={startDrag} sections={sections} />}
|
|
121
133
|
{tab === 'catalog' && (
|
|
122
134
|
<CatalogList
|
|
123
|
-
resources={
|
|
135
|
+
resources={shownResources}
|
|
124
136
|
keysOnCanvas={keysOnCanvas}
|
|
125
137
|
onAdd={onAddResource}
|
|
126
138
|
startDrag={startDrag}
|
|
@@ -140,16 +152,30 @@ const listedDefinitions = NODE_CATEGORIES.map((category) => ({
|
|
|
140
152
|
definitions: nodeDefinitions.filter((definition) => definition.category === category.id && !definition.unlisted),
|
|
141
153
|
}));
|
|
142
154
|
|
|
155
|
+
/** On L1 and L2: only what the level shows is listed (added there, it stays there), the rest is on L3 */
|
|
156
|
+
function LevelHint({ level, onShowAll }: { level: Level; onShowAll: () => void }) {
|
|
157
|
+
return (
|
|
158
|
+
<p className="mx-3 mb-2 text-[11px] leading-snug text-[rgb(var(--ec-page-text-muted))]">
|
|
159
|
+
Showing what L{level} shows.{' '}
|
|
160
|
+
<button onClick={onShowAll} className="font-medium text-[rgb(var(--ec-accent))] hover:underline">
|
|
161
|
+
Go to L3 for everything
|
|
162
|
+
</button>
|
|
163
|
+
</p>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
143
167
|
// Each component's data, made once, for the node shown while it's dragged
|
|
144
168
|
const componentPreviews = new Map(
|
|
145
169
|
nodeDefinitions.map((definition) => [definition.type, { type: definition.type, data: definition.createData() }])
|
|
146
170
|
);
|
|
147
171
|
|
|
148
172
|
const ComponentList = memo(function ComponentList({
|
|
173
|
+
level,
|
|
149
174
|
onAdd,
|
|
150
175
|
startDrag,
|
|
151
176
|
sections: { collapsed, toggle },
|
|
152
177
|
}: {
|
|
178
|
+
level: Level;
|
|
153
179
|
onAdd: (type: string) => void;
|
|
154
180
|
startDrag: StartDrag;
|
|
155
181
|
sections: Collapsed;
|
|
@@ -157,6 +183,8 @@ const ComponentList = memo(function ComponentList({
|
|
|
157
183
|
return (
|
|
158
184
|
<div className="min-h-0 flex-1 space-y-3 overflow-y-auto px-3 pb-3">
|
|
159
185
|
{listedDefinitions.map((category) => {
|
|
186
|
+
const definitions = category.definitions.filter((definition) => isShownOnLevel(definition.type, level));
|
|
187
|
+
if (definitions.length === 0) return null;
|
|
160
188
|
const id = `components:${category.id}`;
|
|
161
189
|
const open = !collapsed.has(id);
|
|
162
190
|
return (
|
|
@@ -165,7 +193,7 @@ const ComponentList = memo(function ComponentList({
|
|
|
165
193
|
{category.label}
|
|
166
194
|
</SectionHeading>
|
|
167
195
|
{open &&
|
|
168
|
-
|
|
196
|
+
definitions.map(({ type, label }) => {
|
|
169
197
|
const { icon: Icon, className } = NODE_ICONS[type];
|
|
170
198
|
return (
|
|
171
199
|
<div
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useState, type FormEvent } from 'react';
|
|
2
|
+
import { MAX_TITLE_LENGTH } from '../limits';
|
|
3
|
+
import Dialog, { PRIMARY_BUTTON, SECONDARY_BUTTON } from './Dialog';
|
|
4
|
+
import { STATUS } from './status';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Naming the canvas a diagram is opened in (the visualiser's "Open in Studio"), named after the diagram to start
|
|
8
|
+
* with, like a new canvas on the Studio page. Loaded with the visualiser, so it imports nothing that loads a canvas.
|
|
9
|
+
*/
|
|
10
|
+
export default function OpenInStudioDialog({
|
|
11
|
+
title,
|
|
12
|
+
onOpen,
|
|
13
|
+
onClose,
|
|
14
|
+
}: {
|
|
15
|
+
/** The diagram's title */
|
|
16
|
+
title?: string;
|
|
17
|
+
/** Makes the canvas with this name and opens it (rejects with what went wrong) */
|
|
18
|
+
onOpen: (title: string | undefined) => Promise<void>;
|
|
19
|
+
onClose: () => void;
|
|
20
|
+
}) {
|
|
21
|
+
const [name, setName] = useState((title ?? '').slice(0, MAX_TITLE_LENGTH));
|
|
22
|
+
const [opening, setOpening] = useState(false);
|
|
23
|
+
const [error, setError] = useState<string>();
|
|
24
|
+
|
|
25
|
+
const submit = async (event: FormEvent) => {
|
|
26
|
+
event.preventDefault();
|
|
27
|
+
setOpening(true);
|
|
28
|
+
setError(undefined);
|
|
29
|
+
try {
|
|
30
|
+
// Stays open (and busy) while the canvas page loads
|
|
31
|
+
await onOpen(name.trim() || undefined);
|
|
32
|
+
} catch (failure) {
|
|
33
|
+
setError((failure as Error).message);
|
|
34
|
+
setOpening(false);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Dialog labelledBy="open-in-studio-title" busy={opening} onClose={onClose}>
|
|
40
|
+
<form onSubmit={submit}>
|
|
41
|
+
<div className="border-b px-5 py-4 border-[rgb(var(--ec-page-border))]">
|
|
42
|
+
<h2 id="open-in-studio-title" className="text-base font-semibold">
|
|
43
|
+
Open in Studio
|
|
44
|
+
</h2>
|
|
45
|
+
<p className="mt-0.5 text-sm text-[rgb(var(--ec-page-text-muted))]">
|
|
46
|
+
A new canvas starts with this diagram. Name it for what you're designing, so your team can find it.
|
|
47
|
+
</p>
|
|
48
|
+
</div>
|
|
49
|
+
<div className="px-5 py-4">
|
|
50
|
+
<label htmlFor="open-in-studio-name" className="block text-sm font-semibold">
|
|
51
|
+
Name
|
|
52
|
+
</label>
|
|
53
|
+
<input
|
|
54
|
+
id="open-in-studio-name"
|
|
55
|
+
// Ready to type a new name, or keep the diagram's
|
|
56
|
+
autoFocus
|
|
57
|
+
onFocus={(event) => event.target.select()}
|
|
58
|
+
value={name}
|
|
59
|
+
onChange={(event) => setName(event.target.value)}
|
|
60
|
+
maxLength={MAX_TITLE_LENGTH}
|
|
61
|
+
autoComplete="off"
|
|
62
|
+
placeholder="e.g. Payments redesign"
|
|
63
|
+
disabled={opening}
|
|
64
|
+
className="mt-2 w-full rounded-lg border px-3 py-2 text-sm shadow-sm bg-[rgb(var(--ec-input-bg))] border-[rgb(var(--ec-input-border))] text-[rgb(var(--ec-input-text))] placeholder:text-[rgb(var(--ec-page-text-muted))] focus:outline-none focus:border-[rgb(var(--ec-accent))] focus:ring-2 focus:ring-[rgb(var(--ec-accent)/0.25)]"
|
|
65
|
+
/>
|
|
66
|
+
{error && <p className={`mt-2 text-sm ${STATUS.danger.text}`}>{error}</p>}
|
|
67
|
+
</div>
|
|
68
|
+
<div className="flex justify-end gap-2 border-t px-5 py-4 border-[rgb(var(--ec-page-border))]">
|
|
69
|
+
<button type="button" onClick={onClose} disabled={opening} className={SECONDARY_BUTTON}>
|
|
70
|
+
Cancel
|
|
71
|
+
</button>
|
|
72
|
+
<button type="submit" disabled={opening} className={PRIMARY_BUTTON}>
|
|
73
|
+
{opening ? 'Opening…' : 'Open in Studio'}
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
76
|
+
</form>
|
|
77
|
+
</Dialog>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -3,14 +3,15 @@ import { useInternalNode, useStore, useStoreApi, ViewportPortal, type XYPosition
|
|
|
3
3
|
import { Bot, Eye, EyeOff } from 'lucide-react';
|
|
4
4
|
import { sizeOf } from '../grouping';
|
|
5
5
|
import { usePeople, type Peer, type PresenceStore } from '../hooks/presence-store';
|
|
6
|
+
import { COUNTER_SCALE, useCounterScale } from '../hooks/use-counter-scale';
|
|
6
7
|
import { useFollowCamera } from '../hooks/use-follow-camera';
|
|
7
8
|
import Picture from './Picture';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Other people on the canvas: their pointers, selections and the connections they're dragging. Drawn in the
|
|
11
12
|
* canvas's own coordinates (React Flow's viewport portal), so panning and zooming move them without
|
|
12
|
-
* re-rendering, and counter-scaled so they stay the same size on screen. Each
|
|
13
|
-
* them on their own, so one pointer moving doesn't re-render anyone else's.
|
|
13
|
+
* re-rendering, and counter-scaled (COUNTER_SCALE, set outside React) so they stay the same size on screen. Each
|
|
14
|
+
* person's pointer and marks follow them on their own, so one pointer moving doesn't re-render anyone else's.
|
|
14
15
|
*/
|
|
15
16
|
export const RemotePresence = memo(function RemotePresence({
|
|
16
17
|
store,
|
|
@@ -20,18 +21,18 @@ export const RemotePresence = memo(function RemotePresence({
|
|
|
20
21
|
clientId: number | null;
|
|
21
22
|
}) {
|
|
22
23
|
const people = usePeople(store);
|
|
23
|
-
const
|
|
24
|
-
|
|
24
|
+
const layer = useRef<HTMLDivElement>(null);
|
|
25
|
+
useCounterScale(layer);
|
|
25
26
|
const others = store ? people.filter((peer) => peer.clientId !== clientId) : [];
|
|
26
27
|
|
|
27
28
|
return (
|
|
28
29
|
<ViewportPortal>
|
|
29
30
|
{/* Above nodes (selected ones are raised to 1000) */}
|
|
30
|
-
<div className="pointer-events-none absolute left-0 top-0" style={{ zIndex: 2000 }}>
|
|
31
|
+
<div ref={layer} className="pointer-events-none absolute left-0 top-0" style={{ zIndex: 2000 }}>
|
|
31
32
|
{others.map((peer) => (
|
|
32
33
|
<Fragment key={peer.clientId}>
|
|
33
|
-
<PeerMarks store={store!} clientId={peer.clientId}
|
|
34
|
-
{peer.pointer && <PeerPointer store={store!} peer={peer}
|
|
34
|
+
<PeerMarks store={store!} clientId={peer.clientId} />
|
|
35
|
+
{peer.pointer && <PeerPointer store={store!} peer={peer} />}
|
|
35
36
|
</Fragment>
|
|
36
37
|
))}
|
|
37
38
|
</div>
|
|
@@ -51,33 +52,39 @@ const marksKey = (peer: Peer | undefined) =>
|
|
|
51
52
|
: '';
|
|
52
53
|
|
|
53
54
|
/** Someone's selection, and the connection they're dragging (a dashed line from where it starts to their pointer) */
|
|
54
|
-
const PeerMarks = memo(function PeerMarks({ store, clientId
|
|
55
|
+
const PeerMarks = memo(function PeerMarks({ store, clientId }: { store: PresenceStore; clientId: number }) {
|
|
55
56
|
useSyncExternalStore(store.subscribe, () => marksKey(findPeer(store, clientId)));
|
|
56
57
|
const peer = findPeer(store, clientId);
|
|
57
58
|
if (!peer) return null;
|
|
58
59
|
return (
|
|
59
60
|
<>
|
|
60
61
|
{peer.selection.map((id) => (
|
|
61
|
-
<SelectionOutline key={id} nodeId={id} name={peer.name} color={peer.color}
|
|
62
|
+
<SelectionOutline key={id} nodeId={id} name={peer.name} color={peer.color} />
|
|
62
63
|
))}
|
|
63
|
-
{peer.pointer && peer.connecting &&
|
|
64
|
-
<svg className="absolute left-0 top-0 overflow-visible" width={1} height={1}>
|
|
65
|
-
<line
|
|
66
|
-
x1={peer.connecting.x}
|
|
67
|
-
y1={peer.connecting.y}
|
|
68
|
-
x2={peer.pointer.x}
|
|
69
|
-
y2={peer.pointer.y}
|
|
70
|
-
stroke={peer.color}
|
|
71
|
-
strokeWidth={2 * scale}
|
|
72
|
-
strokeDasharray={`${6 * scale} ${4 * scale}`}
|
|
73
|
-
/>
|
|
74
|
-
<circle cx={peer.connecting.x} cy={peer.connecting.y} r={4 * scale} fill={peer.color} />
|
|
75
|
-
</svg>
|
|
76
|
-
)}
|
|
64
|
+
{peer.pointer && peer.connecting && <ConnectingLine from={peer.connecting} to={peer.pointer} color={peer.color} />}
|
|
77
65
|
</>
|
|
78
66
|
);
|
|
79
67
|
});
|
|
80
68
|
|
|
69
|
+
/** A connection someone's dragging: only shown while they drag one, so it can follow the zoom itself */
|
|
70
|
+
function ConnectingLine({ from, to, color }: { from: XYPosition; to: XYPosition; color: string }) {
|
|
71
|
+
const scale = 1 / useStore((state) => state.transform[2]);
|
|
72
|
+
return (
|
|
73
|
+
<svg className="absolute left-0 top-0 overflow-visible" width={1} height={1}>
|
|
74
|
+
<line
|
|
75
|
+
x1={from.x}
|
|
76
|
+
y1={from.y}
|
|
77
|
+
x2={to.x}
|
|
78
|
+
y2={to.y}
|
|
79
|
+
stroke={color}
|
|
80
|
+
strokeWidth={2 * scale}
|
|
81
|
+
strokeDasharray={`${6 * scale} ${4 * scale}`}
|
|
82
|
+
/>
|
|
83
|
+
<circle cx={from.x} cy={from.y} r={4 * scale} fill={color} />
|
|
84
|
+
</svg>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
81
88
|
/** Longest a pointer takes to glide to where it was last seen (updates come about every 33 ms) */
|
|
82
89
|
const MAX_GLIDE_MS = 120;
|
|
83
90
|
|
|
@@ -85,19 +92,15 @@ const MAX_GLIDE_MS = 120;
|
|
|
85
92
|
* Someone's pointer, gliding from where it was to each new position over the time since the last one (so a
|
|
86
93
|
* steady stream of updates looks like steady movement). Moved with a transform outside React: no re-renders.
|
|
87
94
|
*/
|
|
88
|
-
const PeerPointer = memo(function PeerPointer({ store, peer
|
|
95
|
+
const PeerPointer = memo(function PeerPointer({ store, peer }: { store: PresenceStore; peer: Peer }) {
|
|
89
96
|
const ref = useRef<HTMLDivElement>(null);
|
|
90
97
|
const shown = useRef<XYPosition | null>(null);
|
|
91
|
-
const scaleRef = useRef(scale);
|
|
92
|
-
scaleRef.current = scale;
|
|
93
98
|
|
|
94
99
|
const draw = () => {
|
|
95
100
|
if (ref.current && shown.current)
|
|
96
|
-
ref.current.style.transform = `translate(${shown.current.x}px, ${shown.current.y}px) scale(${
|
|
101
|
+
ref.current.style.transform = `translate(${shown.current.x}px, ${shown.current.y}px) scale(var(${COUNTER_SCALE}, 1))`;
|
|
97
102
|
};
|
|
98
103
|
|
|
99
|
-
useLayoutEffect(draw, [scale]);
|
|
100
|
-
|
|
101
104
|
useLayoutEffect(() => {
|
|
102
105
|
let target: XYPosition | null = null;
|
|
103
106
|
let from: XYPosition = { x: 0, y: 0 };
|
|
@@ -157,17 +160,18 @@ const PeerPointer = memo(function PeerPointer({ store, peer, scale }: { store: P
|
|
|
157
160
|
);
|
|
158
161
|
});
|
|
159
162
|
|
|
163
|
+
// Screen pixels, in the canvas's coordinates (counter-scaled by the layer's COUNTER_SCALE)
|
|
164
|
+
const onScreen = (px: number) => `calc(${px}px * var(${COUNTER_SCALE}, 1))`;
|
|
165
|
+
|
|
160
166
|
/** Someone's selection around a node: follows that node only, so other nodes moving don't re-render it */
|
|
161
167
|
const SelectionOutline = memo(function SelectionOutline({
|
|
162
168
|
nodeId,
|
|
163
169
|
name,
|
|
164
170
|
color,
|
|
165
|
-
scale,
|
|
166
171
|
}: {
|
|
167
172
|
nodeId: string;
|
|
168
173
|
name: string;
|
|
169
174
|
color: string;
|
|
170
|
-
scale: number;
|
|
171
175
|
}) {
|
|
172
176
|
const node = useInternalNode(nodeId);
|
|
173
177
|
if (!node) return null;
|
|
@@ -177,17 +181,21 @@ const SelectionOutline = memo(function SelectionOutline({
|
|
|
177
181
|
<div
|
|
178
182
|
className="absolute"
|
|
179
183
|
style={{
|
|
180
|
-
left: x - 4
|
|
181
|
-
top: y - 4
|
|
182
|
-
width: width + 8
|
|
183
|
-
height: height + 8
|
|
184
|
-
border: `${2
|
|
185
|
-
borderRadius: 8
|
|
184
|
+
left: `calc(${x}px - ${onScreen(4)})`,
|
|
185
|
+
top: `calc(${y}px - ${onScreen(4)})`,
|
|
186
|
+
width: `calc(${width}px + ${onScreen(8)})`,
|
|
187
|
+
height: `calc(${height}px + ${onScreen(8)})`,
|
|
188
|
+
border: `${onScreen(2)} solid ${color}`,
|
|
189
|
+
borderRadius: onScreen(8),
|
|
186
190
|
}}
|
|
187
191
|
>
|
|
188
192
|
<span
|
|
189
193
|
className="absolute right-0 top-0 whitespace-nowrap rounded px-1.5 text-[10px] leading-4 text-white"
|
|
190
|
-
style={{
|
|
194
|
+
style={{
|
|
195
|
+
backgroundColor: color,
|
|
196
|
+
transform: `translateY(-100%) scale(var(${COUNTER_SCALE}, 1))`,
|
|
197
|
+
transformOrigin: 'bottom right',
|
|
198
|
+
}}
|
|
191
199
|
>
|
|
192
200
|
{name}
|
|
193
201
|
</span>
|
|
@@ -253,6 +261,7 @@ export const AgentActivity = memo(function AgentActivity({
|
|
|
253
261
|
const working = people.filter((peer) => peer.clientId !== clientId && peer.agent && peer.pointer);
|
|
254
262
|
const camera = useFollowCamera({
|
|
255
263
|
enabled: following && enabled,
|
|
264
|
+
fitsContent: enabled,
|
|
256
265
|
agentsWorking: working.length > 0,
|
|
257
266
|
getAgents: () => (store?.getPeers() ?? []).filter((peer: Peer) => peer.clientId !== clientId && peer.agent && peer.pointer),
|
|
258
267
|
});
|