@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.
- package/LICENSE +227 -128
- package/dist/analytics/analytics.cjs +18 -3
- package/dist/analytics/analytics.d.cts +1 -1
- package/dist/analytics/analytics.d.ts +1 -1
- package/dist/analytics/analytics.js +3 -2
- package/dist/analytics/log-build.cjs +71 -35
- package/dist/analytics/log-build.js +5 -4
- package/dist/analytics/telemetry.cjs +42 -0
- package/dist/analytics/telemetry.d.cts +20 -0
- package/dist/analytics/telemetry.d.ts +20 -0
- package/dist/analytics/telemetry.js +8 -0
- package/dist/{chunk-K2ACDO7Y.js → chunk-2LBDRHE4.js} +1 -1
- package/dist/{chunk-TQLRG35T.js → chunk-AWOO63ME.js} +71 -45
- package/dist/chunk-CPW6G62V.js +17 -0
- package/dist/{chunk-UDS3SYLZ.js → chunk-CRKJBDN7.js} +56 -33
- package/dist/{chunk-Q6SD5SNQ.js → chunk-EM36OL6A.js} +1 -1
- package/dist/{chunk-S2QGT7ZF.js → chunk-IONYKKOY.js} +1 -1
- package/dist/{chunk-KYJZJLBD.js → chunk-LJMXOEEC.js} +9 -3
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +204 -124
- package/dist/eventcatalog.config.d.cts +59 -1
- package/dist/eventcatalog.config.d.ts +59 -1
- package/dist/eventcatalog.js +30 -11
- package/dist/federation/federate.js +2 -2
- 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 -1
- 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/dist/utils/license-status.cjs +71 -45
- package/dist/utils/license-status.d.cts +14 -5
- package/dist/utils/license-status.d.ts +14 -5
- package/dist/utils/license-status.js +1 -1
- package/eventcatalog/astro.config.mjs +5 -0
- package/eventcatalog/integrations/storage.mjs +20 -0
- package/eventcatalog/integrations/studio-server.mjs +32 -0
- package/eventcatalog/src/components/Grids/utils.tsx +0 -23
- package/eventcatalog/src/components/Header.astro +3 -3
- package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.tsx +6 -1
- package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +12 -4
- package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +16 -7
- package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +10 -0
- package/eventcatalog/src/components/Tables/Discover/DiscoverTable.tsx +4 -62
- package/eventcatalog/src/components/Tables/Discover/columns.tsx +17 -150
- package/eventcatalog/src/components/Tables/Resources/ResourcesTable.tsx +425 -0
- package/eventcatalog/src/components/Tables/RowActionsMenu.tsx +157 -0
- package/eventcatalog/src/components/Tables/TablePagination.tsx +82 -0
- package/eventcatalog/src/features/ai/chat-api.ts +3 -3
- package/eventcatalog/src/features/auth/middleware/middleware-auth.ts +10 -0
- package/eventcatalog/src/features/integrations/eventcatalog-features.ts +34 -1
- package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +47 -0
- package/eventcatalog/src/features/mcp/apps/canvas/styles.css +22 -0
- package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +276 -0
- package/eventcatalog/src/features/mcp/apps/generated/canvas.html +210 -0
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +75 -74
- 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 +36 -5
- package/eventcatalog/src/features/server/start.ts +25 -0
- package/eventcatalog/src/features/storage/database.ts +88 -0
- package/eventcatalog/src/features/storage/migrations.ts +85 -0
- package/eventcatalog/src/features/storage/sql/database.sql +5 -0
- package/eventcatalog/src/features/storage/sql/migrations/0001-studio-canvases.sql +12 -0
- package/eventcatalog/src/features/storage/sql/migrations.sql +15 -0
- package/eventcatalog/src/features/storage/sql.ts +28 -0
- package/eventcatalog/src/features/studio/agent-choreography.ts +381 -0
- package/eventcatalog/src/features/studio/api/canvases-api.ts +170 -0
- package/eventcatalog/src/features/studio/api/canvases.ts +25 -0
- package/eventcatalog/src/features/studio/api/client.ts +35 -0
- package/eventcatalog/src/features/studio/canvas-actions.ts +414 -0
- package/eventcatalog/src/features/studio/canvas-doc.ts +414 -0
- package/eventcatalog/src/features/studio/canvas-preview.ts +53 -0
- package/eventcatalog/src/features/studio/catalog-resources.ts +155 -0
- package/eventcatalog/src/features/studio/catalog.ts +171 -0
- package/eventcatalog/src/features/studio/clipboard.ts +97 -0
- package/eventcatalog/src/features/studio/components/CanvasControls.tsx +154 -0
- package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +370 -0
- package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +117 -0
- package/eventcatalog/src/features/studio/components/Comments.tsx +770 -0
- package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +281 -0
- package/eventcatalog/src/features/studio/components/ConnectionsChooser.tsx +144 -0
- package/eventcatalog/src/features/studio/components/ContainerChooser.tsx +145 -0
- package/eventcatalog/src/features/studio/components/DeleteCanvasDialog.tsx +81 -0
- package/eventcatalog/src/features/studio/components/DragGhost.tsx +115 -0
- package/eventcatalog/src/features/studio/components/JoinForm.tsx +34 -0
- package/eventcatalog/src/features/studio/components/LeftPanel.tsx +314 -0
- package/eventcatalog/src/features/studio/components/Picture.tsx +21 -0
- package/eventcatalog/src/features/studio/components/Presence.tsx +286 -0
- package/eventcatalog/src/features/studio/components/PropertiesPanel.tsx +212 -0
- package/eventcatalog/src/features/studio/components/ShareDialog.tsx +260 -0
- package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +1216 -0
- package/eventcatalog/src/features/studio/components/StudioNotice.astro +24 -0
- package/eventcatalog/src/features/studio/components/canvas-nodes.tsx +255 -0
- package/eventcatalog/src/features/studio/components/icons.ts +53 -0
- package/eventcatalog/src/features/studio/components/status.ts +53 -0
- package/eventcatalog/src/features/studio/edges.ts +56 -0
- package/eventcatalog/src/features/studio/flow-state.ts +92 -0
- package/eventcatalog/src/features/studio/grouping.ts +226 -0
- package/eventcatalog/src/features/studio/hooks/presence-sender.ts +46 -0
- package/eventcatalog/src/features/studio/hooks/presence-store.ts +119 -0
- package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +347 -0
- package/eventcatalog/src/features/studio/hooks/use-comments.ts +54 -0
- package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +104 -0
- package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +589 -0
- package/eventcatalog/src/features/studio/identity.ts +37 -0
- package/eventcatalog/src/features/studio/layout.ts +42 -0
- package/eventcatalog/src/features/studio/levels.ts +282 -0
- package/eventcatalog/src/features/studio/node-types.ts +205 -0
- package/eventcatalog/src/features/studio/pages/[id].astro +68 -0
- package/eventcatalog/src/features/studio/pages/index.astro +784 -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 +510 -0
- package/eventcatalog/src/features/studio/server/runtime.ts +491 -0
- package/eventcatalog/src/features/studio/server/sign-in.ts +18 -0
- package/eventcatalog/src/features/studio/server/sql/storage.sql +18 -0
- package/eventcatalog/src/features/studio/server/storage.ts +90 -0
- package/eventcatalog/src/features/studio/server/tool-icons.ts +30 -0
- package/eventcatalog/src/features/studio/studio.css +273 -0
- package/eventcatalog/src/features/studio/tool-sync.ts +75 -0
- package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +10 -5
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.spec.ts +52 -63
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +34 -46
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +11 -29
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_ResourcesPage.astro +79 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.spec.ts +32 -14
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.ts +63 -58
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/index.astro +2 -62
- package/eventcatalog/src/pages/docs/services/[id]/[version]/resources/index.astro +13 -0
- package/eventcatalog/src/stores/sidebar-store/builders/domain.ts +24 -42
- package/eventcatalog/src/stores/sidebar-store/builders/flow.ts +2 -0
- package/eventcatalog/src/stores/sidebar-store/builders/message.ts +2 -0
- package/eventcatalog/src/stores/sidebar-store/builders/service.ts +23 -23
- package/eventcatalog/src/stores/sidebar-store/builders/shared.ts +13 -0
- package/eventcatalog/src/stores/sidebar-store/builders/system.ts +18 -27
- package/eventcatalog/src/stores/sidebar-store/state.ts +5 -0
- package/eventcatalog/src/styles/tailwind.css +2 -2
- package/eventcatalog/src/utils/collections/channels.ts +56 -1
- package/eventcatalog/src/utils/collections/flows.ts +50 -1
- package/eventcatalog/src/utils/collections/message-connections.ts +60 -0
- package/eventcatalog/src/utils/collections/message-triggers.ts +2 -1
- package/eventcatalog/src/utils/collections/resource-owners.ts +193 -0
- package/eventcatalog/src/utils/collections/resources.ts +310 -0
- package/eventcatalog/src/utils/collections/util.ts +31 -0
- package/eventcatalog/src/utils/feature.ts +8 -0
- package/eventcatalog/src/utils/git.ts +11 -5
- package/eventcatalog/src/utils/node-graphs/export-mermaid.ts +0 -2
- package/eventcatalog/src/utils/trial.ts +1 -1
- package/package.json +17 -12
- package/dist/runtime-dependencies/shelljs.mjs +0 -3
- package/eventcatalog/public/studio-bg.png +0 -0
- package/eventcatalog/src/components/Grids/DomainGrid.tsx +0 -838
- package/eventcatalog/src/components/Grids/MessageGrid.tsx +0 -344
- package/eventcatalog/src/components/Grids/SystemGrid.tsx +0 -185
- package/eventcatalog/src/components/Grids/components.tsx +0 -170
- package/eventcatalog/src/components/Grids/message-link.ts +0 -38
- package/eventcatalog/src/components/Studio/StudioPageModal.tsx +0 -150
- package/eventcatalog/src/components/Tables/SystemResources/SystemResourcesTable.tsx +0 -294
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.spec.ts +0 -65
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.ts +0 -46
- package/eventcatalog/src/pages/studio.astro +0 -316
- 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
|
+
});
|