@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,370 @@
1
+ import { memo, useEffect, useRef, useState, type ReactNode } from 'react';
2
+ import { Bot, ChevronDown, Link2, Pencil, Plus, Trash2, UserPlus } from 'lucide-react';
3
+ import { usePeople, type Peer, type PresenceStore } from '../hooks/presence-store';
4
+ import type { WebMcpStatus } from '../hooks/use-canvas-webmcp';
5
+ import type { Status, Transport } from '../hooks/use-studio-flow';
6
+ import type { CanvasStatus, StatusChange } from '../canvas-doc';
7
+ import CanvasStatusMenu from './CanvasStatusMenu';
8
+ import { CONNECTION_DOT, STATUS } from './status';
9
+ import Picture from './Picture';
10
+
11
+ /**
12
+ * The canvas's header, like Figma or Miro: two small floating bars. On the left the canvas (its title, and a
13
+ * menu), on the right who's here, connecting an agent and sharing.
14
+ */
15
+
16
+ const barClass =
17
+ 'pointer-events-auto flex h-10 items-center rounded-xl border shadow-md bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))]';
18
+ // The header lets the pointer through to the canvas around its bars, so its menus take it back
19
+ const menuClass =
20
+ 'pointer-events-auto absolute top-full z-50 mt-2 min-w-52 rounded-xl border p-1 shadow-xl bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text))]';
21
+ const menuItemLayout = 'flex w-full items-center gap-2 rounded-lg px-2.5 py-1.5 text-left text-xs';
22
+ const menuItemClass = `${menuItemLayout} hover:bg-[rgb(var(--ec-page-border)/0.5)]`;
23
+ const MAX_AVATARS = 4;
24
+
25
+ /** Closes a popover on outside clicks and Escape */
26
+ function usePopover() {
27
+ const [open, setOpen] = useState(false);
28
+ const ref = useRef<HTMLDivElement>(null);
29
+ useEffect(() => {
30
+ if (!open) return;
31
+ const onPointerDown = (event: PointerEvent) => {
32
+ if (!ref.current?.contains(event.target as globalThis.Node)) setOpen(false);
33
+ };
34
+ const onKeyDown = (event: KeyboardEvent) => event.key === 'Escape' && setOpen(false);
35
+ window.addEventListener('pointerdown', onPointerDown);
36
+ window.addEventListener('keydown', onKeyDown);
37
+ return () => {
38
+ window.removeEventListener('pointerdown', onPointerDown);
39
+ window.removeEventListener('keydown', onKeyDown);
40
+ };
41
+ }, [open]);
42
+ return { open, setOpen, ref };
43
+ }
44
+
45
+ function Avatar({ peer, size = 28, children }: { peer: Peer; size?: number; children?: ReactNode }) {
46
+ return (
47
+ <span
48
+ className="relative flex items-center justify-center rounded-full text-[11px] font-semibold text-white ring-2 ring-[rgb(var(--ec-card-bg))]"
49
+ style={{ backgroundColor: peer.color, width: size, height: size }}
50
+ >
51
+ {children ?? (peer.agent ? <Bot size={14} /> : peer.name.slice(0, 2).toUpperCase())}
52
+ {!children && <Picture src={peer.picture} />}
53
+ </span>
54
+ );
55
+ }
56
+
57
+ function CanvasMenu({
58
+ title,
59
+ status,
60
+ transport,
61
+ onRetitle,
62
+ shareUrl,
63
+ newCanvasUrl,
64
+ onDelete,
65
+ children,
66
+ }: {
67
+ title?: string;
68
+ status: Status;
69
+ transport: Transport;
70
+ onRetitle: (title: string) => void;
71
+ shareUrl?: string;
72
+ newCanvasUrl?: string;
73
+ /** Asks to delete the canvas (not offered when not given, e.g. inside a chat) */
74
+ onDelete?: () => void;
75
+ /** Shown after the title (the canvas's status) */
76
+ children?: ReactNode;
77
+ }) {
78
+ const menu = usePopover();
79
+ const titleInput = useRef<HTMLInputElement>(null);
80
+ const [draft, setDraft] = useState<string | null>(null);
81
+ const statusLabel = {
82
+ connected: transport === 'tools' ? 'Connected through the chat' : 'Connected',
83
+ connecting: 'Connecting…',
84
+ disconnected: 'Offline, reconnecting…',
85
+ }[status];
86
+ const statusColor = CONNECTION_DOT[status];
87
+
88
+ return (
89
+ <div ref={menu.ref} className="relative">
90
+ <div className={`${barClass} gap-1 pl-3 pr-1`}>
91
+ <span title={statusLabel} className={`h-2 w-2 shrink-0 rounded-full ${statusColor}`} />
92
+ <input
93
+ ref={titleInput}
94
+ aria-label="Canvas title"
95
+ value={draft ?? title ?? ''}
96
+ placeholder="Untitled canvas"
97
+ onChange={(e) => setDraft(e.target.value)}
98
+ onBlur={() => {
99
+ if (draft !== null && draft.trim() !== (title ?? '')) onRetitle(draft.trim());
100
+ setDraft(null);
101
+ }}
102
+ onKeyDown={(e) => e.key === 'Enter' && e.currentTarget.blur()}
103
+ size={Math.max(12, (draft ?? title ?? 'Untitled canvas').length)}
104
+ className="max-w-64 truncate rounded-md bg-transparent px-1.5 py-1 text-sm font-semibold placeholder:font-semibold 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
+ />
106
+ {children}
107
+ <button
108
+ aria-label="Canvas menu"
109
+ onClick={() => menu.setOpen((open) => !open)}
110
+ className="rounded-md p-1.5 text-[rgb(var(--ec-icon-color))] hover:bg-[rgb(var(--ec-page-border)/0.5)]"
111
+ >
112
+ <ChevronDown size={14} />
113
+ </button>
114
+ </div>
115
+ {menu.open && (
116
+ <div className={`${menuClass} left-0`}>
117
+ <button
118
+ className={menuItemClass}
119
+ onClick={() => {
120
+ menu.setOpen(false);
121
+ titleInput.current?.focus();
122
+ titleInput.current?.select();
123
+ }}
124
+ >
125
+ <Pencil size={14} /> Rename canvas
126
+ </button>
127
+ <button
128
+ className={menuItemClass}
129
+ onClick={() => {
130
+ void navigator.clipboard.writeText(shareUrl ?? window.location.href);
131
+ menu.setOpen(false);
132
+ }}
133
+ >
134
+ <Link2 size={14} /> Copy link
135
+ </button>
136
+ {newCanvasUrl && (
137
+ <a className={menuItemClass} href={newCanvasUrl}>
138
+ <Plus size={14} /> New canvas
139
+ </a>
140
+ )}
141
+ {onDelete && (
142
+ <>
143
+ <div className="my-1 h-px bg-[rgb(var(--ec-page-border))]" />
144
+ <button
145
+ className={`${menuItemLayout} ${STATUS.danger.text} ${STATUS.danger.hover}`}
146
+ onClick={() => {
147
+ menu.setOpen(false);
148
+ onDelete();
149
+ }}
150
+ >
151
+ <Trash2 size={14} /> Delete canvas
152
+ </button>
153
+ </>
154
+ )}
155
+ <div className="my-1 h-px bg-[rgb(var(--ec-page-border))]" />
156
+ <p className="flex items-center gap-2 px-2.5 py-1.5 text-[11px] text-[rgb(var(--ec-page-text-muted))]">
157
+ <span className={`h-1.5 w-1.5 rounded-full ${statusColor}`} />
158
+ {statusLabel}
159
+ </p>
160
+ </div>
161
+ )}
162
+ </div>
163
+ );
164
+ }
165
+
166
+ /** You: change the name others see you by (unless you're signed in, when it's the name you signed in with) */
167
+ function YouAvatar({ peer, onRename }: { peer: Peer; onRename?: (name: string) => void }) {
168
+ const popover = usePopover();
169
+ const [draft, setDraft] = useState(peer.name);
170
+ const commit = () => {
171
+ const trimmed = draft.trim();
172
+ if (trimmed && trimmed !== peer.name) onRename?.(trimmed);
173
+ popover.setOpen(false);
174
+ };
175
+
176
+ // Signed in: you're shown in the app's header already
177
+ if (!onRename) return null;
178
+ return (
179
+ <div ref={popover.ref} className="relative">
180
+ <button
181
+ title={`${peer.name} (you)`}
182
+ onClick={() => {
183
+ setDraft(peer.name);
184
+ popover.setOpen((open) => !open);
185
+ }}
186
+ className="block rounded-full transition-transform hover:scale-110"
187
+ >
188
+ <Avatar peer={peer} />
189
+ </button>
190
+ {popover.open && (
191
+ <div className={`${menuClass} right-0 w-60 space-y-2 p-3`}>
192
+ <label className="block space-y-1.5 text-[11px] font-medium text-[rgb(var(--ec-page-text-muted))]">
193
+ Your name on this canvas
194
+ <input
195
+ autoFocus
196
+ value={draft}
197
+ onChange={(e) => setDraft(e.target.value)}
198
+ onKeyDown={(e) => e.key === 'Enter' && commit()}
199
+ className="w-full rounded-md border px-2 py-1.5 text-xs bg-[rgb(var(--ec-input-bg))] border-[rgb(var(--ec-input-border))] text-[rgb(var(--ec-input-text))]"
200
+ />
201
+ </label>
202
+ <button
203
+ onClick={commit}
204
+ className="w-full rounded-md px-2 py-1.5 text-xs font-medium bg-[rgb(var(--ec-button-bg))] text-[rgb(var(--ec-button-text))]"
205
+ >
206
+ Save
207
+ </button>
208
+ </div>
209
+ )}
210
+ </div>
211
+ );
212
+ }
213
+
214
+ /** Who's here: click someone to jump to them; everyone past the first few is in a list */
215
+ function People({
216
+ peers,
217
+ clientId,
218
+ onJumpTo,
219
+ onRename,
220
+ }: {
221
+ peers: Peer[];
222
+ clientId: number | null;
223
+ onJumpTo: (peer: Peer) => void;
224
+ onRename?: (name: string) => void;
225
+ }) {
226
+ const overflow = usePopover();
227
+ const you = peers.find((peer) => peer.clientId === clientId);
228
+ const others = peers.filter((peer) => peer.clientId !== clientId);
229
+ const shown = others.slice(0, MAX_AVATARS);
230
+ const hidden = others.slice(MAX_AVATARS);
231
+ const describe = (peer: Peer) => `${peer.name}${peer.agent ? ` (AI agent)${peer.activity ? `: ${peer.activity}` : ''}` : ''}`;
232
+
233
+ return (
234
+ <div className="flex items-center gap-1.5">
235
+ <div className="flex -space-x-1.5">
236
+ {shown.map((peer) => (
237
+ <button
238
+ key={peer.clientId}
239
+ title={`${describe(peer)}. Click to jump to them`}
240
+ onClick={() => onJumpTo(peer)}
241
+ className="block rounded-full transition-transform hover:z-10 hover:scale-110"
242
+ >
243
+ <Avatar peer={peer} />
244
+ </button>
245
+ ))}
246
+ {hidden.length > 0 && (
247
+ <div ref={overflow.ref} className="relative">
248
+ <button
249
+ title={`${hidden.length} more`}
250
+ onClick={() => overflow.setOpen((open) => !open)}
251
+ className="flex h-7 w-7 items-center justify-center rounded-full text-[11px] font-semibold ring-2 ring-[rgb(var(--ec-card-bg))] bg-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text))]"
252
+ >
253
+ +{hidden.length}
254
+ </button>
255
+ {overflow.open && (
256
+ <div className={`${menuClass} right-0`}>
257
+ {hidden.map((peer) => (
258
+ <button
259
+ key={peer.clientId}
260
+ className={menuItemClass}
261
+ onClick={() => {
262
+ onJumpTo(peer);
263
+ overflow.setOpen(false);
264
+ }}
265
+ >
266
+ <Avatar peer={peer} size={20} />
267
+ {describe(peer)}
268
+ </button>
269
+ ))}
270
+ </div>
271
+ )}
272
+ </div>
273
+ )}
274
+ </div>
275
+ {you && <YouAvatar peer={you} onRename={onRename} />}
276
+ </div>
277
+ );
278
+ }
279
+
280
+ export default memo(function CanvasHeader({
281
+ title,
282
+ onRetitle,
283
+ status,
284
+ transport,
285
+ presence,
286
+ clientId,
287
+ onJumpTo,
288
+ onRename,
289
+ onConnectAgent,
290
+ webMcp,
291
+ shareUrl,
292
+ newCanvasUrl,
293
+ onShare,
294
+ canvasStatus,
295
+ statusHistory,
296
+ onStatusChange,
297
+ onDelete,
298
+ }: {
299
+ title?: string;
300
+ onRetitle: (title: string) => void;
301
+ canvasStatus: CanvasStatus;
302
+ statusHistory: StatusChange[];
303
+ onStatusChange: (status: CanvasStatus, note?: string) => void;
304
+ status: Status;
305
+ transport: Transport;
306
+ presence: PresenceStore | null;
307
+ clientId: number | null;
308
+ onJumpTo: (peer: Peer) => void;
309
+ /** Not given when you're signed in */
310
+ onRename?: (name: string) => void;
311
+ onConnectAgent: () => void;
312
+ webMcp: WebMcpStatus;
313
+ shareUrl?: string;
314
+ newCanvasUrl?: string;
315
+ /** Open the share dialog */
316
+ onShare: () => void;
317
+ /** Ask to delete the canvas (not given where it can't be, e.g. inside a chat) */
318
+ onDelete?: () => void;
319
+ }) {
320
+ const peers = usePeople(presence);
321
+ const agentsHere = peers.some((peer) => peer.agent);
322
+
323
+ return (
324
+ <div className="pointer-events-none absolute left-3 right-3 top-3 z-10 flex items-start justify-between gap-3">
325
+ <CanvasMenu
326
+ title={title}
327
+ status={status}
328
+ transport={transport}
329
+ onRetitle={onRetitle}
330
+ shareUrl={shareUrl}
331
+ newCanvasUrl={newCanvasUrl}
332
+ onDelete={onDelete}
333
+ >
334
+ <CanvasStatusMenu status={canvasStatus} history={statusHistory} onChange={onStatusChange} />
335
+ </CanvasMenu>
336
+
337
+ <div className={`${barClass} gap-2 px-1.5`}>
338
+ <div className="pl-1">
339
+ <People peers={peers} clientId={clientId} onJumpTo={onJumpTo} onRename={onRename} />
340
+ </div>
341
+ <div className="h-5 w-px bg-[rgb(var(--ec-page-border))]" />
342
+ <button
343
+ onClick={onConnectAgent}
344
+ title={
345
+ webMcp.state === 'on'
346
+ ? `Connect an AI agent to this canvas. WebMCP is on in this tab${webMcp.lastCall ? ` (last: ${webMcp.lastCall})` : ''}`
347
+ : 'Connect an AI agent to this canvas'
348
+ }
349
+ // A button of its own next to Share (the two ways to bring someone in): people, or an AI agent
350
+ className="relative flex h-7 items-center gap-1.5 rounded-lg border px-2.5 text-xs font-semibold transition-colors border-[rgb(var(--ec-accent)/0.35)] bg-[rgb(var(--ec-accent-subtle))] text-[rgb(var(--ec-accent))] hover:border-[rgb(var(--ec-accent)/0.7)] hover:bg-[rgb(var(--ec-accent)/0.15)]"
351
+ >
352
+ <Bot size={15} />
353
+ {agentsHere ? 'Agent connected' : 'Connect agent'}
354
+ {(webMcp.state === 'on' || agentsHere) && (
355
+ <span
356
+ className={`absolute -right-1 -top-1 h-2.5 w-2.5 rounded-full ring-2 ring-[rgb(var(--ec-card-bg))] ${STATUS.success.dot}`}
357
+ />
358
+ )}
359
+ </button>
360
+ <button
361
+ onClick={onShare}
362
+ className="flex h-7 items-center gap-1.5 rounded-lg px-3 text-xs font-semibold bg-[rgb(var(--ec-button-bg))] text-[rgb(var(--ec-button-text))] hover:bg-[rgb(var(--ec-button-bg-hover))]"
363
+ >
364
+ <UserPlus size={14} />
365
+ Share
366
+ </button>
367
+ </div>
368
+ </div>
369
+ );
370
+ });
@@ -0,0 +1,117 @@
1
+ import { memo, useEffect, useRef, useState } from 'react';
2
+ import { Check, ChevronDown } from 'lucide-react';
3
+ import { CANVAS_STATUSES, type CanvasStatus, type StatusChange } from '../canvas-doc';
4
+ import { timeAgo } from './Comments';
5
+ import { CANVAS_STATUS_LOOK } from './status';
6
+
7
+ /** Status changes shown in the menu, newest first */
8
+ const HISTORY_SHOWN = 8;
9
+
10
+ /**
11
+ * The canvas's status (draft, proposed, accepted, rejected) as a pill beside its title. It opens a menu to change it,
12
+ * with a note on why, and who changed it before.
13
+ */
14
+ export default memo(function CanvasStatusMenu({
15
+ status,
16
+ history,
17
+ onChange,
18
+ }: {
19
+ status: CanvasStatus;
20
+ history: StatusChange[];
21
+ onChange: (status: CanvasStatus, note?: string) => void;
22
+ }) {
23
+ const [open, setOpen] = useState(false);
24
+ const [note, setNote] = useState('');
25
+ const ref = useRef<HTMLDivElement>(null);
26
+ const look = CANVAS_STATUS_LOOK[status];
27
+
28
+ useEffect(() => {
29
+ if (!open) return;
30
+ const onPointerDown = (event: PointerEvent) => {
31
+ if (!ref.current?.contains(event.target as globalThis.Node)) setOpen(false);
32
+ };
33
+ const onKeyDown = (event: KeyboardEvent) => event.key === 'Escape' && setOpen(false);
34
+ window.addEventListener('pointerdown', onPointerDown);
35
+ window.addEventListener('keydown', onKeyDown);
36
+ return () => {
37
+ window.removeEventListener('pointerdown', onPointerDown);
38
+ window.removeEventListener('keydown', onKeyDown);
39
+ };
40
+ }, [open]);
41
+
42
+ const choose = (next: CanvasStatus) => {
43
+ if (next !== status) onChange(next, note);
44
+ setNote('');
45
+ setOpen(false);
46
+ };
47
+ const recent = history.slice(-HISTORY_SHOWN).reverse();
48
+
49
+ return (
50
+ <div ref={ref} className="relative">
51
+ <button
52
+ onClick={() => setOpen((current) => !current)}
53
+ aria-label={`Status: ${look.label}`}
54
+ aria-expanded={open}
55
+ className={`flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] font-semibold ${look.pill}`}
56
+ >
57
+ {look.label}
58
+ <ChevronDown size={12} />
59
+ </button>
60
+ {open && (
61
+ <div className="absolute left-0 top-full z-50 mt-2 w-72 rounded-xl border p-1 shadow-xl bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text))]">
62
+ <p className="px-2.5 pb-1 pt-1.5 text-[11px] font-semibold uppercase tracking-wide text-[rgb(var(--ec-page-text-muted))]">
63
+ Status
64
+ </p>
65
+ {CANVAS_STATUSES.map((value) => {
66
+ const option = CANVAS_STATUS_LOOK[value];
67
+ 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
+ >
73
+ <span className={`h-2 w-2 shrink-0 rounded-full ${option.dot}`} />
74
+ <span className="min-w-0 flex-1">
75
+ <span className="block text-xs font-medium">{option.label}</span>
76
+ <span className="block text-[11px] text-[rgb(var(--ec-page-text-muted))]">{option.description}</span>
77
+ </span>
78
+ {value === status && <Check size={14} className="shrink-0 text-[rgb(var(--ec-accent))]" />}
79
+ </button>
80
+ );
81
+ })}
82
+ <div className="px-2.5 py-2">
83
+ <input
84
+ value={note}
85
+ onChange={(event) => setNote(event.target.value)}
86
+ placeholder="Add a note (optional), then pick a status"
87
+ aria-label="Note for the status change"
88
+ className="w-full rounded-md border px-2 py-1.5 text-xs 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))]"
89
+ />
90
+ </div>
91
+ {recent.length > 0 && (
92
+ <>
93
+ <div className="my-1 h-px bg-[rgb(var(--ec-page-border))]" />
94
+ <p className="px-2.5 pb-1 pt-1.5 text-[11px] font-semibold uppercase tracking-wide text-[rgb(var(--ec-page-text-muted))]">
95
+ History
96
+ </p>
97
+ <ul className="max-h-48 space-y-1.5 overflow-y-auto px-2.5 pb-2">
98
+ {recent.map((change) => (
99
+ <li key={`${change.at}-${change.status}`} className="text-[11px]">
100
+ <span className="flex items-center gap-1.5">
101
+ <span className={`h-1.5 w-1.5 shrink-0 rounded-full ${CANVAS_STATUS_LOOK[change.status].dot}`} />
102
+ <span className="font-medium">{CANVAS_STATUS_LOOK[change.status].label}</span>
103
+ <span className="text-[rgb(var(--ec-page-text-muted))]">
104
+ by {change.by.name} · {timeAgo(change.at)}
105
+ </span>
106
+ </span>
107
+ {change.note && <span className="block pl-3 text-[rgb(var(--ec-page-text-muted))]">{change.note}</span>}
108
+ </li>
109
+ ))}
110
+ </ul>
111
+ </>
112
+ )}
113
+ </div>
114
+ )}
115
+ </div>
116
+ );
117
+ });