@eventcatalog/core 5.0.0-beta.5 → 5.0.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +4 -4
- package/dist/{chunk-43AEF3JE.js → chunk-56CD2NH6.js} +1 -1
- package/dist/{chunk-YYMBYOQR.js → chunk-DE3QHVJP.js} +1 -1
- package/dist/{chunk-XQXMSQA5.js → chunk-L2IRFOE7.js} +1 -1
- package/dist/{chunk-SHHO3JJI.js → chunk-QBP3BBSV.js} +1 -1
- package/dist/{chunk-EBDTVZDW.js → chunk-XMPFHLK4.js} +4 -4
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +1 -1
- package/dist/eventcatalog.js +11 -11
- package/dist/federation/federate.js +4 -4
- 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 -0
- 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/eventcatalog/astro.config.mjs +2 -0
- package/eventcatalog/integrations/studio-server.mjs +17 -0
- package/eventcatalog/src/features/integrations/eventcatalog-features.ts +20 -1
- package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +39 -0
- package/eventcatalog/src/features/mcp/apps/canvas/styles.css +22 -0
- package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +269 -0
- package/eventcatalog/src/features/mcp/apps/generated/canvas.html +209 -0
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +2 -2
- 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 +17 -4
- package/eventcatalog/src/features/studio/agent-choreography.ts +378 -0
- package/eventcatalog/src/features/studio/canvas-actions.ts +287 -0
- package/eventcatalog/src/features/studio/canvas-doc.ts +280 -0
- package/eventcatalog/src/features/studio/catalog-resources.ts +152 -0
- package/eventcatalog/src/features/studio/catalog.ts +171 -0
- package/eventcatalog/src/features/studio/components/CanvasControls.tsx +153 -0
- package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +332 -0
- package/eventcatalog/src/features/studio/components/Comments.tsx +455 -0
- package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +217 -0
- package/eventcatalog/src/features/studio/components/ContainerChooser.tsx +145 -0
- package/eventcatalog/src/features/studio/components/JoinForm.tsx +58 -0
- package/eventcatalog/src/features/studio/components/LeftPanel.tsx +218 -0
- package/eventcatalog/src/features/studio/components/Presence.tsx +284 -0
- package/eventcatalog/src/features/studio/components/PropertiesPanel.tsx +75 -0
- package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +760 -0
- package/eventcatalog/src/features/studio/components/canvas-nodes.tsx +79 -0
- package/eventcatalog/src/features/studio/components/icons.ts +48 -0
- package/eventcatalog/src/features/studio/components/status.ts +22 -0
- package/eventcatalog/src/features/studio/edges.ts +54 -0
- package/eventcatalog/src/features/studio/flow-state.ts +92 -0
- package/eventcatalog/src/features/studio/grouping.ts +133 -0
- package/eventcatalog/src/features/studio/hooks/presence-sender.ts +46 -0
- package/eventcatalog/src/features/studio/hooks/presence-store.ts +115 -0
- package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +337 -0
- package/eventcatalog/src/features/studio/hooks/use-comments.ts +47 -0
- package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +99 -0
- package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +423 -0
- package/eventcatalog/src/features/studio/layout.ts +39 -0
- package/eventcatalog/src/features/studio/levels.ts +216 -0
- package/eventcatalog/src/features/studio/node-types.ts +173 -0
- package/eventcatalog/src/features/studio/pages/[id].astro +38 -0
- package/eventcatalog/src/features/studio/pages/index.astro +8 -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 +475 -0
- package/eventcatalog/src/features/studio/server/runtime.ts +215 -0
- package/eventcatalog/src/features/studio/server/tool-icons.ts +30 -0
- package/eventcatalog/src/features/studio/studio.css +89 -0
- package/eventcatalog/src/features/studio/tool-sync.ts +75 -0
- package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +9 -2
- package/eventcatalog/src/utils/feature.ts +4 -0
- package/package.json +9 -2
- package/eventcatalog/public/studio-bg.png +0 -0
- package/eventcatalog/src/components/Studio/StudioPageModal.tsx +0 -150
- package/eventcatalog/src/pages/studio.astro +0 -316
- package/dist/{chunk-ZEY2XUJN.js → chunk-AYNZXUB5.js} +7 -7
|
@@ -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
|
+
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,58 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
const COLORS = ['#ef4444', '#f97316', '#eab308', '#22c55e', '#06b6d4', '#3b82f6', '#8b5cf6', '#ec4899'];
|
|
4
|
+
const NAME_KEY = 'eventcatalog-studio-name';
|
|
5
|
+
|
|
6
|
+
export const getStoredName = () => {
|
|
7
|
+
try {
|
|
8
|
+
return localStorage.getItem(NAME_KEY);
|
|
9
|
+
} catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const storeName = (name: string) => {
|
|
15
|
+
try {
|
|
16
|
+
localStorage.setItem(NAME_KEY, name);
|
|
17
|
+
} catch {}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Same name, same colour across reloads
|
|
21
|
+
export const colorForName = (name: string) => {
|
|
22
|
+
let hash = 0;
|
|
23
|
+
for (const char of name) hash = (hash * 31 + char.charCodeAt(0)) | 0;
|
|
24
|
+
return COLORS[Math.abs(hash) % COLORS.length];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default function JoinForm({ onJoin }: { onJoin: (name: string) => void }) {
|
|
28
|
+
const [draft, setDraft] = useState('');
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<form
|
|
32
|
+
onSubmit={(e) => {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
if (draft.trim()) onJoin(draft.trim());
|
|
35
|
+
}}
|
|
36
|
+
className="mx-auto mt-20 max-w-sm space-y-4 rounded-lg border p-6 bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text))]"
|
|
37
|
+
>
|
|
38
|
+
<div>
|
|
39
|
+
<h1 className="text-lg font-semibold">Join this canvas</h1>
|
|
40
|
+
<p className="text-sm text-[rgb(var(--ec-page-text-muted))]">What should others call you?</p>
|
|
41
|
+
</div>
|
|
42
|
+
<input
|
|
43
|
+
autoFocus
|
|
44
|
+
value={draft}
|
|
45
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
46
|
+
placeholder="Your name"
|
|
47
|
+
className="w-full rounded-md px-3 py-2 border bg-[rgb(var(--ec-input-bg))] border-[rgb(var(--ec-input-border))] text-[rgb(var(--ec-input-text))]"
|
|
48
|
+
/>
|
|
49
|
+
<button
|
|
50
|
+
type="submit"
|
|
51
|
+
disabled={!draft.trim()}
|
|
52
|
+
className="w-full rounded-md px-3 py-2 text-sm disabled:opacity-50 bg-[rgb(var(--ec-button-bg))] text-[rgb(var(--ec-button-text))]"
|
|
53
|
+
>
|
|
54
|
+
Join
|
|
55
|
+
</button>
|
|
56
|
+
</form>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { memo, useMemo, useState, type DragEvent } from 'react';
|
|
2
|
+
import { Check, Plus, Search } from 'lucide-react';
|
|
3
|
+
import type { CatalogResource } from '../catalog-resources';
|
|
4
|
+
import { NODE_CATEGORIES, nodeDefinitions } from '../node-types';
|
|
5
|
+
import type { Thread } from '../hooks/use-comments';
|
|
6
|
+
import { CommentList } from './Comments';
|
|
7
|
+
import { CATALOG_GROUPS, NODE_ICONS } from './icons';
|
|
8
|
+
import { STATUS } from './status';
|
|
9
|
+
|
|
10
|
+
/** What's dragged from the left panel onto the canvas: a component's type, or a catalog resource's key */
|
|
11
|
+
export const COMPONENT_DRAG_TYPE = 'application/eventcatalog-studio';
|
|
12
|
+
export const CATALOG_DRAG_TYPE = 'application/eventcatalog-studio-resource';
|
|
13
|
+
|
|
14
|
+
/** A catalog group shows this many resources until it's expanded, so big catalogs stay quick */
|
|
15
|
+
const GROUP_PREVIEW_SIZE = 50;
|
|
16
|
+
|
|
17
|
+
const itemClass =
|
|
18
|
+
'group flex cursor-grab items-center justify-between gap-2 rounded-md border px-2.5 py-2 bg-[rgb(var(--ec-page-bg))] border-[rgb(var(--ec-page-border))] hover:border-[rgb(var(--ec-accent))]';
|
|
19
|
+
const addButtonClass =
|
|
20
|
+
'shrink-0 rounded p-0.5 opacity-0 group-hover:opacity-100 text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-accent))]';
|
|
21
|
+
const headingClass = 'px-1 text-[11px] font-semibold uppercase tracking-wide text-[rgb(var(--ec-page-text-muted))]';
|
|
22
|
+
|
|
23
|
+
const startDrag = (type: string, value: string) => (event: DragEvent) => {
|
|
24
|
+
event.dataTransfer.setData(type, value);
|
|
25
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type Tab = 'components' | 'catalog' | 'comments';
|
|
29
|
+
|
|
30
|
+
/** Components and catalog resources to drag onto the canvas, and the comments on it */
|
|
31
|
+
export default memo(function LeftPanel({
|
|
32
|
+
resources,
|
|
33
|
+
keysOnCanvas,
|
|
34
|
+
onAddComponent,
|
|
35
|
+
onAddResource,
|
|
36
|
+
threads,
|
|
37
|
+
showResolved,
|
|
38
|
+
onToggleResolved,
|
|
39
|
+
onOpenThread,
|
|
40
|
+
}: {
|
|
41
|
+
resources: CatalogResource[];
|
|
42
|
+
/** The catalog resources on the canvas */
|
|
43
|
+
keysOnCanvas: Set<string>;
|
|
44
|
+
onAddComponent: (type: string) => void;
|
|
45
|
+
onAddResource: (key: string) => void;
|
|
46
|
+
threads: Thread[];
|
|
47
|
+
showResolved: boolean;
|
|
48
|
+
onToggleResolved: () => void;
|
|
49
|
+
onOpenThread: (thread: Thread) => void;
|
|
50
|
+
}) {
|
|
51
|
+
const [tab, setTab] = useState<Tab>('components');
|
|
52
|
+
const openThreads = threads.filter((thread) => !thread.resolved).length;
|
|
53
|
+
const tabClass = (active: boolean) =>
|
|
54
|
+
`flex flex-1 items-center justify-center gap-1 whitespace-nowrap rounded-md px-1.5 py-1 text-xs font-medium ${
|
|
55
|
+
active ? 'bg-[rgb(var(--ec-page-bg))] shadow-sm' : 'text-[rgb(var(--ec-page-text-muted))]'
|
|
56
|
+
}`;
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<aside className="flex w-72 shrink-0 flex-col border-r bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))]">
|
|
60
|
+
<div className="m-3 flex gap-1 rounded-lg p-1 bg-[rgb(var(--ec-page-border)/0.5)]">
|
|
61
|
+
<button className={tabClass(tab === 'components')} onClick={() => setTab('components')}>
|
|
62
|
+
Components
|
|
63
|
+
</button>
|
|
64
|
+
<button className={tabClass(tab === 'catalog')} onClick={() => setTab('catalog')}>
|
|
65
|
+
Catalog
|
|
66
|
+
</button>
|
|
67
|
+
<button className={tabClass(tab === 'comments')} onClick={() => setTab('comments')}>
|
|
68
|
+
Comments
|
|
69
|
+
{openThreads > 0 && (
|
|
70
|
+
<span className="rounded-full px-1.5 text-[10px] leading-4 bg-[rgb(var(--ec-button-bg))] text-[rgb(var(--ec-button-text))]">
|
|
71
|
+
{openThreads}
|
|
72
|
+
</span>
|
|
73
|
+
)}
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
76
|
+
{tab === 'components' && <ComponentList onAdd={onAddComponent} />}
|
|
77
|
+
{tab === 'catalog' && <CatalogList resources={resources} keysOnCanvas={keysOnCanvas} onAdd={onAddResource} />}
|
|
78
|
+
{tab === 'comments' && (
|
|
79
|
+
<CommentList threads={threads} showResolved={showResolved} onToggleResolved={onToggleResolved} onOpen={onOpenThread} />
|
|
80
|
+
)}
|
|
81
|
+
</aside>
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const listedDefinitions = NODE_CATEGORIES.map((category) => ({
|
|
86
|
+
...category,
|
|
87
|
+
definitions: nodeDefinitions.filter((definition) => definition.category === category.id && !definition.unlisted),
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
const ComponentList = memo(function ComponentList({ onAdd }: { onAdd: (type: string) => void }) {
|
|
91
|
+
return (
|
|
92
|
+
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto px-3 pb-3">
|
|
93
|
+
{listedDefinitions.map((category) => (
|
|
94
|
+
<section key={category.id} className="space-y-1.5">
|
|
95
|
+
<h3 className={headingClass}>{category.label}</h3>
|
|
96
|
+
{category.definitions.map(({ type, label }) => {
|
|
97
|
+
const { icon: Icon, className } = NODE_ICONS[type];
|
|
98
|
+
return (
|
|
99
|
+
<div key={type} draggable onDragStart={startDrag(COMPONENT_DRAG_TYPE, type)} className={itemClass}>
|
|
100
|
+
<span className="flex items-center gap-2 text-xs font-medium">
|
|
101
|
+
<Icon size={16} className={className} />
|
|
102
|
+
{label}
|
|
103
|
+
</span>
|
|
104
|
+
<button onClick={() => onAdd(type)} title={`Add ${label}`} className={addButtonClass}>
|
|
105
|
+
<Plus size={14} />
|
|
106
|
+
</button>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
})}
|
|
110
|
+
</section>
|
|
111
|
+
))}
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const CatalogList = memo(function CatalogList({
|
|
117
|
+
resources,
|
|
118
|
+
keysOnCanvas,
|
|
119
|
+
onAdd,
|
|
120
|
+
}: {
|
|
121
|
+
resources: CatalogResource[];
|
|
122
|
+
keysOnCanvas: Set<string>;
|
|
123
|
+
onAdd: (key: string) => void;
|
|
124
|
+
}) {
|
|
125
|
+
const [search, setSearch] = useState('');
|
|
126
|
+
const [expanded, setExpanded] = useState<Set<string>>(() => new Set());
|
|
127
|
+
const groups = useMemo(() => {
|
|
128
|
+
const term = search.trim().toLowerCase();
|
|
129
|
+
const matches = term
|
|
130
|
+
? resources.filter((resource) => resource.name.toLowerCase().includes(term) || resource.id.toLowerCase().includes(term))
|
|
131
|
+
: resources;
|
|
132
|
+
return CATALOG_GROUPS.map((group) => ({
|
|
133
|
+
...group,
|
|
134
|
+
items: matches.filter((resource) => resource.collection === group.collection),
|
|
135
|
+
})).filter((group) => group.items.length > 0);
|
|
136
|
+
}, [resources, search]);
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
140
|
+
<div className="relative mx-3 mb-2">
|
|
141
|
+
<Search size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-[rgb(var(--ec-icon-color))]" />
|
|
142
|
+
<input
|
|
143
|
+
value={search}
|
|
144
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
145
|
+
placeholder="Search the catalog"
|
|
146
|
+
className="w-full rounded-md border py-1.5 pl-8 pr-2 text-xs bg-[rgb(var(--ec-input-bg))] border-[rgb(var(--ec-input-border))] text-[rgb(var(--ec-input-text))]"
|
|
147
|
+
/>
|
|
148
|
+
</div>
|
|
149
|
+
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto px-3 pb-3">
|
|
150
|
+
{groups.length === 0 && (
|
|
151
|
+
<p className="py-6 text-center text-xs text-[rgb(var(--ec-page-text-muted))]">No resources found.</p>
|
|
152
|
+
)}
|
|
153
|
+
{groups.map(({ collection, label, icon, className, items }) => {
|
|
154
|
+
const shown = expanded.has(collection) ? items : items.slice(0, GROUP_PREVIEW_SIZE);
|
|
155
|
+
return (
|
|
156
|
+
<section key={collection} className="space-y-1.5">
|
|
157
|
+
<h3 className={headingClass}>
|
|
158
|
+
{label} <span className="font-normal">({items.length})</span>
|
|
159
|
+
</h3>
|
|
160
|
+
{shown.map((resource) => (
|
|
161
|
+
<CatalogItem
|
|
162
|
+
key={resource.key}
|
|
163
|
+
resource={resource}
|
|
164
|
+
icon={icon}
|
|
165
|
+
iconClassName={className}
|
|
166
|
+
onCanvas={keysOnCanvas.has(resource.key)}
|
|
167
|
+
onAdd={onAdd}
|
|
168
|
+
/>
|
|
169
|
+
))}
|
|
170
|
+
{shown.length < items.length && (
|
|
171
|
+
<button
|
|
172
|
+
onClick={() => setExpanded((current) => new Set(current).add(collection))}
|
|
173
|
+
className="w-full rounded-md px-2 py-1.5 text-xs font-medium text-[rgb(var(--ec-accent))] hover:bg-[rgb(var(--ec-accent-subtle))]"
|
|
174
|
+
>
|
|
175
|
+
Show all {items.length} {label.toLowerCase()}
|
|
176
|
+
</button>
|
|
177
|
+
)}
|
|
178
|
+
</section>
|
|
179
|
+
);
|
|
180
|
+
})}
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const CatalogItem = memo(function CatalogItem({
|
|
187
|
+
resource,
|
|
188
|
+
icon: Icon,
|
|
189
|
+
iconClassName,
|
|
190
|
+
onCanvas,
|
|
191
|
+
onAdd,
|
|
192
|
+
}: {
|
|
193
|
+
resource: CatalogResource;
|
|
194
|
+
icon: (typeof CATALOG_GROUPS)[number]['icon'];
|
|
195
|
+
iconClassName: string;
|
|
196
|
+
onCanvas: boolean;
|
|
197
|
+
onAdd: (key: string) => void;
|
|
198
|
+
}) {
|
|
199
|
+
return (
|
|
200
|
+
<div draggable onDragStart={startDrag(CATALOG_DRAG_TYPE, resource.key)} title={resource.summary} className={itemClass}>
|
|
201
|
+
<span className="flex min-w-0 items-center gap-2 text-xs font-medium">
|
|
202
|
+
<Icon size={16} className={`shrink-0 ${iconClassName}`} />
|
|
203
|
+
<span className="truncate">{resource.name}</span>
|
|
204
|
+
<span className="shrink-0 text-[10px] text-[rgb(var(--ec-page-text-muted))]">v{resource.version}</span>
|
|
205
|
+
</span>
|
|
206
|
+
<span className="flex shrink-0 items-center gap-1">
|
|
207
|
+
{onCanvas && (
|
|
208
|
+
<span title="On the canvas">
|
|
209
|
+
<Check size={14} className={STATUS.success.text} />
|
|
210
|
+
</span>
|
|
211
|
+
)}
|
|
212
|
+
<button onClick={() => onAdd(resource.key)} title={`Add ${resource.name}`} className={addButtonClass}>
|
|
213
|
+
<Plus size={14} />
|
|
214
|
+
</button>
|
|
215
|
+
</span>
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
});
|