@eventcatalog/core 5.0.0-beta.1 → 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.
Files changed (171) hide show
  1. package/LICENSE +227 -128
  2. package/dist/analytics/analytics.cjs +18 -3
  3. package/dist/analytics/analytics.d.cts +1 -1
  4. package/dist/analytics/analytics.d.ts +1 -1
  5. package/dist/analytics/analytics.js +3 -2
  6. package/dist/analytics/log-build.cjs +71 -35
  7. package/dist/analytics/log-build.js +5 -4
  8. package/dist/analytics/telemetry.cjs +42 -0
  9. package/dist/analytics/telemetry.d.cts +20 -0
  10. package/dist/analytics/telemetry.d.ts +20 -0
  11. package/dist/analytics/telemetry.js +8 -0
  12. package/dist/{chunk-K2ACDO7Y.js → chunk-2LBDRHE4.js} +1 -1
  13. package/dist/{chunk-TQLRG35T.js → chunk-AWOO63ME.js} +71 -45
  14. package/dist/chunk-CPW6G62V.js +17 -0
  15. package/dist/{chunk-UDS3SYLZ.js → chunk-CRKJBDN7.js} +56 -33
  16. package/dist/{chunk-Q6SD5SNQ.js → chunk-EM36OL6A.js} +1 -1
  17. package/dist/{chunk-S2QGT7ZF.js → chunk-IONYKKOY.js} +1 -1
  18. package/dist/{chunk-KYJZJLBD.js → chunk-LJMXOEEC.js} +9 -3
  19. package/dist/constants.cjs +1 -1
  20. package/dist/constants.js +1 -1
  21. package/dist/eventcatalog.cjs +204 -124
  22. package/dist/eventcatalog.config.d.cts +59 -1
  23. package/dist/eventcatalog.config.d.ts +59 -1
  24. package/dist/eventcatalog.js +30 -11
  25. package/dist/federation/federate.js +2 -2
  26. package/dist/generate.cjs +1 -1
  27. package/dist/generate.js +3 -3
  28. package/dist/runtime-dependencies/@hocuspocus__provider.mjs +3 -0
  29. package/dist/runtime-dependencies/@hocuspocus__server.mjs +3 -0
  30. package/dist/runtime-dependencies/@mcp-b__webmcp-local-relay.mjs +3 -0
  31. package/dist/runtime-dependencies/@mcp-b__webmcp-polyfill.mjs +3 -0
  32. package/dist/runtime-dependencies/manifest.json +8 -1
  33. package/dist/runtime-dependencies/react-hotkeys-hook.mjs +3 -0
  34. package/dist/runtime-dependencies/y-protocols.mjs +3 -0
  35. package/dist/runtime-dependencies/y-protocols__awareness.mjs +3 -0
  36. package/dist/runtime-dependencies/yjs.mjs +3 -0
  37. package/dist/utils/cli-logger.cjs +1 -1
  38. package/dist/utils/cli-logger.js +2 -2
  39. package/dist/utils/license-status.cjs +71 -45
  40. package/dist/utils/license-status.d.cts +14 -5
  41. package/dist/utils/license-status.d.ts +14 -5
  42. package/dist/utils/license-status.js +1 -1
  43. package/eventcatalog/astro.config.mjs +5 -0
  44. package/eventcatalog/integrations/storage.mjs +20 -0
  45. package/eventcatalog/integrations/studio-server.mjs +32 -0
  46. package/eventcatalog/src/components/Grids/utils.tsx +0 -23
  47. package/eventcatalog/src/components/Header.astro +3 -3
  48. package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.tsx +6 -1
  49. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +12 -4
  50. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +16 -7
  51. package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +10 -0
  52. package/eventcatalog/src/components/Tables/Discover/DiscoverTable.tsx +4 -62
  53. package/eventcatalog/src/components/Tables/Discover/columns.tsx +17 -150
  54. package/eventcatalog/src/components/Tables/Resources/ResourcesTable.tsx +425 -0
  55. package/eventcatalog/src/components/Tables/RowActionsMenu.tsx +157 -0
  56. package/eventcatalog/src/components/Tables/TablePagination.tsx +82 -0
  57. package/eventcatalog/src/features/ai/chat-api.ts +3 -3
  58. package/eventcatalog/src/features/auth/middleware/middleware-auth.ts +10 -0
  59. package/eventcatalog/src/features/integrations/eventcatalog-features.ts +34 -1
  60. package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +47 -0
  61. package/eventcatalog/src/features/mcp/apps/canvas/styles.css +22 -0
  62. package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +276 -0
  63. package/eventcatalog/src/features/mcp/apps/generated/canvas.html +210 -0
  64. package/eventcatalog/src/features/mcp/apps/generated/viewer.html +75 -74
  65. package/eventcatalog/src/features/mcp/apps/shared/app-view.tsx +2 -1
  66. package/eventcatalog/src/features/mcp/mcp-apps.ts +6 -3
  67. package/eventcatalog/src/features/mcp/mcp-server.ts +36 -5
  68. package/eventcatalog/src/features/server/start.ts +25 -0
  69. package/eventcatalog/src/features/storage/database.ts +88 -0
  70. package/eventcatalog/src/features/storage/migrations.ts +85 -0
  71. package/eventcatalog/src/features/storage/sql/database.sql +5 -0
  72. package/eventcatalog/src/features/storage/sql/migrations/0001-studio-canvases.sql +12 -0
  73. package/eventcatalog/src/features/storage/sql/migrations.sql +15 -0
  74. package/eventcatalog/src/features/storage/sql.ts +28 -0
  75. package/eventcatalog/src/features/studio/agent-choreography.ts +381 -0
  76. package/eventcatalog/src/features/studio/api/canvases-api.ts +170 -0
  77. package/eventcatalog/src/features/studio/api/canvases.ts +25 -0
  78. package/eventcatalog/src/features/studio/api/client.ts +35 -0
  79. package/eventcatalog/src/features/studio/canvas-actions.ts +414 -0
  80. package/eventcatalog/src/features/studio/canvas-doc.ts +414 -0
  81. package/eventcatalog/src/features/studio/canvas-preview.ts +53 -0
  82. package/eventcatalog/src/features/studio/catalog-resources.ts +155 -0
  83. package/eventcatalog/src/features/studio/catalog.ts +171 -0
  84. package/eventcatalog/src/features/studio/clipboard.ts +97 -0
  85. package/eventcatalog/src/features/studio/components/CanvasControls.tsx +154 -0
  86. package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +370 -0
  87. package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +117 -0
  88. package/eventcatalog/src/features/studio/components/Comments.tsx +770 -0
  89. package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +281 -0
  90. package/eventcatalog/src/features/studio/components/ConnectionsChooser.tsx +144 -0
  91. package/eventcatalog/src/features/studio/components/ContainerChooser.tsx +145 -0
  92. package/eventcatalog/src/features/studio/components/DeleteCanvasDialog.tsx +81 -0
  93. package/eventcatalog/src/features/studio/components/DragGhost.tsx +115 -0
  94. package/eventcatalog/src/features/studio/components/JoinForm.tsx +34 -0
  95. package/eventcatalog/src/features/studio/components/LeftPanel.tsx +314 -0
  96. package/eventcatalog/src/features/studio/components/Picture.tsx +21 -0
  97. package/eventcatalog/src/features/studio/components/Presence.tsx +286 -0
  98. package/eventcatalog/src/features/studio/components/PropertiesPanel.tsx +212 -0
  99. package/eventcatalog/src/features/studio/components/ShareDialog.tsx +260 -0
  100. package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +1216 -0
  101. package/eventcatalog/src/features/studio/components/StudioNotice.astro +24 -0
  102. package/eventcatalog/src/features/studio/components/canvas-nodes.tsx +255 -0
  103. package/eventcatalog/src/features/studio/components/icons.ts +53 -0
  104. package/eventcatalog/src/features/studio/components/status.ts +53 -0
  105. package/eventcatalog/src/features/studio/edges.ts +56 -0
  106. package/eventcatalog/src/features/studio/flow-state.ts +92 -0
  107. package/eventcatalog/src/features/studio/grouping.ts +226 -0
  108. package/eventcatalog/src/features/studio/hooks/presence-sender.ts +46 -0
  109. package/eventcatalog/src/features/studio/hooks/presence-store.ts +119 -0
  110. package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +347 -0
  111. package/eventcatalog/src/features/studio/hooks/use-comments.ts +54 -0
  112. package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +104 -0
  113. package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +589 -0
  114. package/eventcatalog/src/features/studio/identity.ts +37 -0
  115. package/eventcatalog/src/features/studio/layout.ts +42 -0
  116. package/eventcatalog/src/features/studio/levels.ts +282 -0
  117. package/eventcatalog/src/features/studio/node-types.ts +205 -0
  118. package/eventcatalog/src/features/studio/pages/[id].astro +68 -0
  119. package/eventcatalog/src/features/studio/pages/index.astro +784 -0
  120. package/eventcatalog/src/features/studio/pages/webmcp-relay.ts +27 -0
  121. package/eventcatalog/src/features/studio/placement.ts +197 -0
  122. package/eventcatalog/src/features/studio/server/canvas-mcp.ts +510 -0
  123. package/eventcatalog/src/features/studio/server/runtime.ts +491 -0
  124. package/eventcatalog/src/features/studio/server/sign-in.ts +18 -0
  125. package/eventcatalog/src/features/studio/server/sql/storage.sql +18 -0
  126. package/eventcatalog/src/features/studio/server/storage.ts +90 -0
  127. package/eventcatalog/src/features/studio/server/tool-icons.ts +30 -0
  128. package/eventcatalog/src/features/studio/studio.css +273 -0
  129. package/eventcatalog/src/features/studio/tool-sync.ts +75 -0
  130. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +10 -5
  131. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.spec.ts +52 -63
  132. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +34 -46
  133. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +11 -29
  134. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_ResourcesPage.astro +79 -0
  135. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.spec.ts +32 -14
  136. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.ts +63 -58
  137. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/index.astro +2 -62
  138. package/eventcatalog/src/pages/docs/services/[id]/[version]/resources/index.astro +13 -0
  139. package/eventcatalog/src/stores/sidebar-store/builders/domain.ts +24 -42
  140. package/eventcatalog/src/stores/sidebar-store/builders/flow.ts +2 -0
  141. package/eventcatalog/src/stores/sidebar-store/builders/message.ts +2 -0
  142. package/eventcatalog/src/stores/sidebar-store/builders/service.ts +23 -23
  143. package/eventcatalog/src/stores/sidebar-store/builders/shared.ts +13 -0
  144. package/eventcatalog/src/stores/sidebar-store/builders/system.ts +18 -27
  145. package/eventcatalog/src/stores/sidebar-store/state.ts +5 -0
  146. package/eventcatalog/src/styles/tailwind.css +2 -2
  147. package/eventcatalog/src/utils/collections/channels.ts +56 -1
  148. package/eventcatalog/src/utils/collections/flows.ts +50 -1
  149. package/eventcatalog/src/utils/collections/message-connections.ts +60 -0
  150. package/eventcatalog/src/utils/collections/message-triggers.ts +2 -1
  151. package/eventcatalog/src/utils/collections/resource-owners.ts +193 -0
  152. package/eventcatalog/src/utils/collections/resources.ts +310 -0
  153. package/eventcatalog/src/utils/collections/util.ts +31 -0
  154. package/eventcatalog/src/utils/feature.ts +8 -0
  155. package/eventcatalog/src/utils/git.ts +11 -5
  156. package/eventcatalog/src/utils/node-graphs/export-mermaid.ts +0 -2
  157. package/eventcatalog/src/utils/trial.ts +1 -1
  158. package/package.json +17 -12
  159. package/dist/runtime-dependencies/shelljs.mjs +0 -3
  160. package/eventcatalog/public/studio-bg.png +0 -0
  161. package/eventcatalog/src/components/Grids/DomainGrid.tsx +0 -838
  162. package/eventcatalog/src/components/Grids/MessageGrid.tsx +0 -344
  163. package/eventcatalog/src/components/Grids/SystemGrid.tsx +0 -185
  164. package/eventcatalog/src/components/Grids/components.tsx +0 -170
  165. package/eventcatalog/src/components/Grids/message-link.ts +0 -38
  166. package/eventcatalog/src/components/Studio/StudioPageModal.tsx +0 -150
  167. package/eventcatalog/src/components/Tables/SystemResources/SystemResourcesTable.tsx +0 -294
  168. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.spec.ts +0 -65
  169. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.ts +0 -46
  170. package/eventcatalog/src/pages/studio.astro +0 -316
  171. package/dist/{chunk-AYNZXUB5.js → chunk-KHACCTW2.js} +3 -3
@@ -0,0 +1,115 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState, type DragEvent } from 'react';
2
+ import { ReactFlow, ReactFlowProvider, useStoreApi, type Node, type XYPosition } from '@xyflow/react';
3
+ import { getNodeSize, isGroupType } from '../node-types';
4
+ import { nodeTypes } from './canvas-nodes';
5
+
6
+ /**
7
+ * Dragging something from the left panel onto the canvas shows the node it becomes, under the pointer, at the
8
+ * canvas's zoom and where it will land (centred on the pointer, as it's dropped). Drawn here rather than as the
9
+ * browser's drag image, which browsers shrink, fade and put a backing behind.
10
+ */
11
+
12
+ /** What's dragged: the node it becomes */
13
+ export type DragPreview = { type: string; data: Record<string, unknown> };
14
+ /** Starts dragging `value` as `dragType`, showing `preview` as what's dragged */
15
+ export type StartDrag = (dragType: string, value: string, preview: DragPreview) => (event: DragEvent) => void;
16
+
17
+ // Hides the browser's own drag image (a transparent pixel)
18
+ const NO_DRAG_IMAGE =
19
+ typeof Image === 'undefined'
20
+ ? undefined
21
+ : Object.assign(new Image(), { src: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' });
22
+
23
+ // Room around the node for the badge on its top edge
24
+ const MARGIN = 16;
25
+ const PRO_OPTIONS = { hideAttribution: true };
26
+
27
+ type Ghost = { preview: DragPreview; zoom: number; at: XYPosition };
28
+
29
+ /** The ghost of what's being dragged, and how to start dragging something with one */
30
+ export function useDragGhost() {
31
+ const store = useStoreApi();
32
+ const [ghost, setGhost] = useState<Ghost | null>(null);
33
+
34
+ const startDrag = useCallback<StartDrag>(
35
+ (dragType, value, preview) => (event) => {
36
+ event.dataTransfer.setData(dragType, value);
37
+ event.dataTransfer.effectAllowed = 'move';
38
+ if (NO_DRAG_IMAGE) event.dataTransfer.setDragImage(NO_DRAG_IMAGE, 0, 0);
39
+ setGhost({ preview, zoom: store.getState().transform[2], at: { x: event.clientX, y: event.clientY } });
40
+ },
41
+ [store]
42
+ );
43
+
44
+ const element = ghost && <DragGhost ghost={ghost} onEnd={() => setGhost(null)} />;
45
+ return { startDrag, ghost: element };
46
+ }
47
+
48
+ function DragGhost({ ghost, onEnd }: { ghost: Ghost; onEnd: () => void }) {
49
+ const ref = useRef<HTMLDivElement>(null);
50
+ const { preview, zoom } = ghost;
51
+ const container = isGroupType(preview.type);
52
+ // The size it's added at (containers are sized; cards are drawn at their own size inside this)
53
+ const size = getNodeSize(preview.type);
54
+ // The node is placed with its middle where it's dropped, at the size it's added at
55
+ const offset = { x: (size.width / 2) * zoom + MARGIN, y: (size.height / 2) * zoom + MARGIN };
56
+
57
+ // Follows the pointer (as the browser reports it while dragging) without re-rendering, and goes when it's dropped
58
+ useEffect(() => {
59
+ const move = (x: number, y: number) => {
60
+ if (ref.current) ref.current.style.transform = `translate(${x - offset.x}px, ${y - offset.y}px)`;
61
+ };
62
+ move(ghost.at.x, ghost.at.y);
63
+ const onDragOver = (event: globalThis.DragEvent) => {
64
+ if (event.clientX || event.clientY) move(event.clientX, event.clientY);
65
+ };
66
+ document.addEventListener('dragover', onDragOver);
67
+ document.addEventListener('drop', onEnd);
68
+ document.addEventListener('dragend', onEnd);
69
+ return () => {
70
+ document.removeEventListener('dragover', onDragOver);
71
+ document.removeEventListener('drop', onEnd);
72
+ document.removeEventListener('dragend', onEnd);
73
+ };
74
+ }, [ghost]);
75
+
76
+ const nodes = useMemo<Node[]>(
77
+ () => [
78
+ {
79
+ id: 'drag-ghost',
80
+ type: preview.type,
81
+ position: { x: 0, y: 0 },
82
+ data: preview.data,
83
+ ...(container && size),
84
+ },
85
+ ],
86
+ [preview, container, size]
87
+ );
88
+ const viewport = useMemo(() => ({ x: MARGIN, y: MARGIN, zoom }), [zoom]);
89
+
90
+ return (
91
+ <div
92
+ ref={ref}
93
+ aria-hidden
94
+ // Sized here: the visualiser's styles make its class fill the window
95
+ className="studio-drag-preview eventcatalog-visualizer pointer-events-none fixed left-0 top-0 z-[100]"
96
+ style={{ width: size.width * zoom + MARGIN * 2, height: size.height * zoom + MARGIN * 2 }}
97
+ >
98
+ <ReactFlowProvider>
99
+ <ReactFlow
100
+ nodes={nodes}
101
+ nodeTypes={nodeTypes}
102
+ defaultViewport={viewport}
103
+ proOptions={PRO_OPTIONS}
104
+ nodesDraggable={false}
105
+ nodesConnectable={false}
106
+ elementsSelectable={false}
107
+ panOnDrag={false}
108
+ zoomOnScroll={false}
109
+ zoomOnPinch={false}
110
+ preventScrolling={false}
111
+ />
112
+ </ReactFlowProvider>
113
+ </div>
114
+ );
115
+ }
@@ -0,0 +1,34 @@
1
+ import { useState } from 'react';
2
+
3
+ export default function JoinForm({ onJoin }: { onJoin: (name: string) => void }) {
4
+ const [draft, setDraft] = useState('');
5
+
6
+ return (
7
+ <form
8
+ onSubmit={(e) => {
9
+ e.preventDefault();
10
+ if (draft.trim()) onJoin(draft.trim());
11
+ }}
12
+ 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))]"
13
+ >
14
+ <div>
15
+ <h1 className="text-lg font-semibold">Join this canvas</h1>
16
+ <p className="text-sm text-[rgb(var(--ec-page-text-muted))]">What should others call you?</p>
17
+ </div>
18
+ <input
19
+ autoFocus
20
+ value={draft}
21
+ onChange={(e) => setDraft(e.target.value)}
22
+ placeholder="Your name"
23
+ 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))]"
24
+ />
25
+ <button
26
+ type="submit"
27
+ disabled={!draft.trim()}
28
+ 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))]"
29
+ >
30
+ Join
31
+ </button>
32
+ </form>
33
+ );
34
+ }
@@ -0,0 +1,314 @@
1
+ import { memo, useCallback, useMemo, useState, type ReactNode } from 'react';
2
+ import { Check, ChevronRight, Plus, Search } from 'lucide-react';
3
+ import type { CatalogResource } from '../catalog-resources';
4
+ import { catalogNodeData } from '../catalog';
5
+ import { NODE_CATEGORIES, nodeDefinitions } from '../node-types';
6
+ import type { Thread } from '../hooks/use-comments';
7
+ import { CommentList } from './Comments';
8
+ import { useDragGhost, type StartDrag } from './DragGhost';
9
+ import { CATALOG_GROUPS, NODE_ICONS } from './icons';
10
+ import { STATUS } from './status';
11
+
12
+ /** What's dragged from the left panel onto the canvas: a component's type, or a catalog resource's key */
13
+ export const COMPONENT_DRAG_TYPE = 'application/eventcatalog-studio';
14
+ export const CATALOG_DRAG_TYPE = 'application/eventcatalog-studio-resource';
15
+
16
+ /** A catalog group shows this many resources until it's expanded, so big catalogs stay quick */
17
+ const GROUP_PREVIEW_SIZE = 50;
18
+
19
+ const itemClass =
20
+ '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))]';
21
+ const addButtonClass =
22
+ 'shrink-0 rounded p-0.5 opacity-0 group-hover:opacity-100 text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-accent))]';
23
+ // The sections that are collapsed, by id (e.g. "components:messages"), remembered in this browser
24
+ const COLLAPSED_KEY = 'eventcatalog-studio-collapsed';
25
+ type Collapsed = { collapsed: ReadonlySet<string>; toggle: (id: string) => void };
26
+
27
+ const readCollapsed = (): Set<string> => {
28
+ try {
29
+ return new Set(JSON.parse(localStorage.getItem(COLLAPSED_KEY) ?? '[]') as string[]);
30
+ } catch {
31
+ return new Set();
32
+ }
33
+ };
34
+
35
+ function useCollapsed(): Collapsed {
36
+ const [collapsed, setCollapsed] = useState(readCollapsed);
37
+ const toggle = useCallback((id: string) => {
38
+ setCollapsed((current) => {
39
+ const next = new Set(current);
40
+ if (next.has(id)) next.delete(id);
41
+ else next.add(id);
42
+ try {
43
+ localStorage.setItem(COLLAPSED_KEY, JSON.stringify([...next]));
44
+ } catch {
45
+ // Not remembered (e.g. storage is blocked): it still works for now
46
+ }
47
+ return next;
48
+ });
49
+ }, []);
50
+ return useMemo(() => ({ collapsed, toggle }), [collapsed, toggle]);
51
+ }
52
+
53
+ /** A section's heading, which opens and closes it */
54
+ function SectionHeading({ open, onToggle, children }: { open: boolean; onToggle: () => void; children: ReactNode }) {
55
+ return (
56
+ <h3>
57
+ <button
58
+ onClick={onToggle}
59
+ aria-expanded={open}
60
+ className="flex w-full cursor-pointer items-center gap-1 rounded-md px-1.5 py-1 text-left text-[11px] font-semibold uppercase tracking-wide text-[rgb(var(--ec-page-text-muted))] transition-colors hover:bg-[rgb(var(--ec-page-border)/0.6)] hover:text-[rgb(var(--ec-page-text))]"
61
+ >
62
+ <ChevronRight size={12} className={`shrink-0 transition-transform ${open ? 'rotate-90' : ''}`} />
63
+ {children}
64
+ </button>
65
+ </h3>
66
+ );
67
+ }
68
+
69
+ type Tab = 'components' | 'catalog' | 'comments';
70
+
71
+ /** Components and catalog resources to drag onto the canvas, and the comments on it */
72
+ export default memo(function LeftPanel({
73
+ resources,
74
+ keysOnCanvas,
75
+ onAddComponent,
76
+ onAddResource,
77
+ threads,
78
+ showResolved,
79
+ onToggleResolved,
80
+ onOpenThread,
81
+ }: {
82
+ resources: CatalogResource[];
83
+ /** The catalog resources on the canvas */
84
+ keysOnCanvas: Set<string>;
85
+ onAddComponent: (type: string) => void;
86
+ onAddResource: (key: string) => void;
87
+ threads: Thread[];
88
+ showResolved: boolean;
89
+ onToggleResolved: () => void;
90
+ onOpenThread: (thread: Thread) => void;
91
+ }) {
92
+ const [tab, setTab] = useState<Tab>('components');
93
+ // What's being dragged onto the canvas, drawn as the node it becomes (see DragGhost)
94
+ const { startDrag, ghost } = useDragGhost();
95
+ const sections = useCollapsed();
96
+ const openThreads = threads.filter((thread) => !thread.resolved).length;
97
+ const tabClass = (active: boolean) =>
98
+ `flex flex-1 items-center justify-center gap-1 whitespace-nowrap rounded-md px-1.5 py-1 text-xs font-medium ${
99
+ active ? 'bg-[rgb(var(--ec-page-bg))] shadow-sm' : 'text-[rgb(var(--ec-page-text-muted))]'
100
+ }`;
101
+
102
+ return (
103
+ <aside className="flex w-72 shrink-0 flex-col border-r bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))]">
104
+ <div className="m-3 flex gap-1 rounded-lg p-1 bg-[rgb(var(--ec-page-border)/0.5)]">
105
+ <button className={tabClass(tab === 'components')} onClick={() => setTab('components')}>
106
+ Components
107
+ </button>
108
+ <button className={tabClass(tab === 'catalog')} onClick={() => setTab('catalog')}>
109
+ Catalog
110
+ </button>
111
+ <button className={tabClass(tab === 'comments')} onClick={() => setTab('comments')}>
112
+ Comments
113
+ {openThreads > 0 && (
114
+ <span className="rounded-full px-1.5 text-[10px] leading-4 bg-[rgb(var(--ec-button-bg))] text-[rgb(var(--ec-button-text))]">
115
+ {openThreads}
116
+ </span>
117
+ )}
118
+ </button>
119
+ </div>
120
+ {tab === 'components' && <ComponentList onAdd={onAddComponent} startDrag={startDrag} sections={sections} />}
121
+ {tab === 'catalog' && (
122
+ <CatalogList
123
+ resources={resources}
124
+ keysOnCanvas={keysOnCanvas}
125
+ onAdd={onAddResource}
126
+ startDrag={startDrag}
127
+ sections={sections}
128
+ />
129
+ )}
130
+ {tab === 'comments' && (
131
+ <CommentList threads={threads} showResolved={showResolved} onToggleResolved={onToggleResolved} onOpen={onOpenThread} />
132
+ )}
133
+ {ghost}
134
+ </aside>
135
+ );
136
+ });
137
+
138
+ const listedDefinitions = NODE_CATEGORIES.map((category) => ({
139
+ ...category,
140
+ definitions: nodeDefinitions.filter((definition) => definition.category === category.id && !definition.unlisted),
141
+ }));
142
+
143
+ // Each component's data, made once, for the node shown while it's dragged
144
+ const componentPreviews = new Map(
145
+ nodeDefinitions.map((definition) => [definition.type, { type: definition.type, data: definition.createData() }])
146
+ );
147
+
148
+ const ComponentList = memo(function ComponentList({
149
+ onAdd,
150
+ startDrag,
151
+ sections: { collapsed, toggle },
152
+ }: {
153
+ onAdd: (type: string) => void;
154
+ startDrag: StartDrag;
155
+ sections: Collapsed;
156
+ }) {
157
+ return (
158
+ <div className="min-h-0 flex-1 space-y-3 overflow-y-auto px-3 pb-3">
159
+ {listedDefinitions.map((category) => {
160
+ const id = `components:${category.id}`;
161
+ const open = !collapsed.has(id);
162
+ return (
163
+ <section key={category.id} className="space-y-1.5">
164
+ <SectionHeading open={open} onToggle={() => toggle(id)}>
165
+ {category.label}
166
+ </SectionHeading>
167
+ {open &&
168
+ category.definitions.map(({ type, label }) => {
169
+ const { icon: Icon, className } = NODE_ICONS[type];
170
+ return (
171
+ <div
172
+ key={type}
173
+ draggable
174
+ onDragStart={startDrag(COMPONENT_DRAG_TYPE, type, componentPreviews.get(type)!)}
175
+ className={itemClass}
176
+ >
177
+ <span className="flex items-center gap-2 text-xs font-medium">
178
+ <Icon size={16} className={className} />
179
+ {label}
180
+ </span>
181
+ <button onClick={() => onAdd(type)} title={`Add ${label}`} className={addButtonClass}>
182
+ <Plus size={14} />
183
+ </button>
184
+ </div>
185
+ );
186
+ })}
187
+ </section>
188
+ );
189
+ })}
190
+ </div>
191
+ );
192
+ });
193
+
194
+ const CatalogList = memo(function CatalogList({
195
+ resources,
196
+ keysOnCanvas,
197
+ onAdd,
198
+ startDrag,
199
+ sections: { collapsed, toggle },
200
+ }: {
201
+ resources: CatalogResource[];
202
+ keysOnCanvas: Set<string>;
203
+ onAdd: (key: string) => void;
204
+ startDrag: StartDrag;
205
+ sections: Collapsed;
206
+ }) {
207
+ const [search, setSearch] = useState('');
208
+ const [expanded, setExpanded] = useState<Set<string>>(() => new Set());
209
+ const searching = search.trim() !== '';
210
+ const groups = useMemo(() => {
211
+ const term = search.trim().toLowerCase();
212
+ const matches = term
213
+ ? resources.filter((resource) => resource.name.toLowerCase().includes(term) || resource.id.toLowerCase().includes(term))
214
+ : resources;
215
+ return CATALOG_GROUPS.map((group) => ({
216
+ ...group,
217
+ items: matches.filter((resource) => resource.collection === group.collection),
218
+ })).filter((group) => group.items.length > 0);
219
+ }, [resources, search]);
220
+
221
+ return (
222
+ <div className="flex min-h-0 flex-1 flex-col">
223
+ <div className="relative mx-3 mb-2">
224
+ <Search size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-[rgb(var(--ec-icon-color))]" />
225
+ <input
226
+ value={search}
227
+ onChange={(e) => setSearch(e.target.value)}
228
+ placeholder="Search the catalog"
229
+ 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))]"
230
+ />
231
+ </div>
232
+ <div className="min-h-0 flex-1 space-y-3 overflow-y-auto px-3 pb-3">
233
+ {groups.length === 0 && (
234
+ <p className="py-6 text-center text-xs text-[rgb(var(--ec-page-text-muted))]">No resources found.</p>
235
+ )}
236
+ {groups.map(({ collection, label, icon, className, items }) => {
237
+ const id = `catalog:${collection}`;
238
+ // While searching, every group with matches shows them
239
+ const open = searching || !collapsed.has(id);
240
+ const shown = !open ? [] : expanded.has(collection) ? items : items.slice(0, GROUP_PREVIEW_SIZE);
241
+ return (
242
+ <section key={collection} className="space-y-1.5">
243
+ <SectionHeading open={open} onToggle={() => toggle(id)}>
244
+ {label} <span className="font-normal">({items.length})</span>
245
+ </SectionHeading>
246
+ {shown.map((resource) => (
247
+ <CatalogItem
248
+ key={resource.key}
249
+ resource={resource}
250
+ icon={icon}
251
+ iconClassName={className}
252
+ onCanvas={keysOnCanvas.has(resource.key)}
253
+ onAdd={onAdd}
254
+ startDrag={startDrag}
255
+ />
256
+ ))}
257
+ {open && shown.length < items.length && (
258
+ <button
259
+ onClick={() => setExpanded((current) => new Set(current).add(collection))}
260
+ 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))]"
261
+ >
262
+ Show all {items.length} {label.toLowerCase()}
263
+ </button>
264
+ )}
265
+ </section>
266
+ );
267
+ })}
268
+ </div>
269
+ </div>
270
+ );
271
+ });
272
+
273
+ const CatalogItem = memo(function CatalogItem({
274
+ resource,
275
+ icon: Icon,
276
+ iconClassName,
277
+ onCanvas,
278
+ onAdd,
279
+ startDrag,
280
+ }: {
281
+ resource: CatalogResource;
282
+ icon: (typeof CATALOG_GROUPS)[number]['icon'];
283
+ iconClassName: string;
284
+ onCanvas: boolean;
285
+ onAdd: (key: string) => void;
286
+ startDrag: StartDrag;
287
+ }) {
288
+ // Drawn like the card it becomes (domains and systems can become containers too, but start as cards)
289
+ const preview = useMemo(() => ({ type: resource.node.type, data: catalogNodeData(resource) }), [resource]);
290
+ return (
291
+ <div
292
+ draggable
293
+ onDragStart={startDrag(CATALOG_DRAG_TYPE, resource.key, preview)}
294
+ title={resource.summary}
295
+ className={itemClass}
296
+ >
297
+ <span className="flex min-w-0 items-center gap-2 text-xs font-medium">
298
+ <Icon size={16} className={`shrink-0 ${iconClassName}`} />
299
+ <span className="truncate">{resource.name}</span>
300
+ <span className="shrink-0 text-[10px] text-[rgb(var(--ec-page-text-muted))]">v{resource.version}</span>
301
+ </span>
302
+ <span className="flex shrink-0 items-center gap-1">
303
+ {onCanvas && (
304
+ <span title="On the canvas">
305
+ <Check size={14} className={STATUS.success.text} />
306
+ </span>
307
+ )}
308
+ <button onClick={() => onAdd(resource.key)} title={`Add ${resource.name}`} className={addButtonClass}>
309
+ <Plus size={14} />
310
+ </button>
311
+ </span>
312
+ </div>
313
+ );
314
+ });
@@ -0,0 +1,21 @@
1
+ import { useState } from 'react';
2
+
3
+ /**
4
+ * Someone's picture from their sign-in provider (SSO), loaded from there. By default it covers their avatar's
5
+ * initials, which show again if it doesn't load (blocked, expired, or no longer there).
6
+ */
7
+ export default function Picture({ src, className = 'absolute inset-0 h-full w-full' }: { src?: string; className?: string }) {
8
+ const [failed, setFailed] = useState<string>();
9
+ if (!src || failed === src) return null;
10
+ return (
11
+ <img
12
+ src={src}
13
+ alt=""
14
+ draggable={false}
15
+ // Some providers (e.g. Google) refuse pictures loaded with a referrer from another site
16
+ referrerPolicy="no-referrer"
17
+ onError={() => setFailed(src)}
18
+ className={`rounded-full object-cover ${className}`}
19
+ />
20
+ );
21
+ }