@eventcatalog/core 5.0.0-beta.12 → 5.0.0-beta.14

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 (90) hide show
  1. package/LICENSE +13 -12
  2. package/dist/analytics/analytics.cjs +1 -1
  3. package/dist/analytics/analytics.js +2 -2
  4. package/dist/analytics/log-build.cjs +1 -1
  5. package/dist/analytics/log-build.js +3 -3
  6. package/dist/catalog-runtime.cjs +1 -1
  7. package/dist/{chunk-TSY5CC27.js → chunk-4KE3EUDJ.js} +1 -1
  8. package/dist/{chunk-FY6MSP6J.js → chunk-I3XDOICA.js} +1 -1
  9. package/dist/{chunk-QB7X34OD.js → chunk-OFK7U7BX.js} +1 -1
  10. package/dist/{chunk-CX2VB7AI.js → chunk-PI5YNZZZ.js} +1 -1
  11. package/dist/{chunk-BWC4YKW3.js → chunk-WY6M3RPT.js} +1 -1
  12. package/dist/constants.cjs +1 -1
  13. package/dist/constants.js +1 -1
  14. package/dist/eventcatalog.cjs +11 -6
  15. package/dist/eventcatalog.js +14 -9
  16. package/dist/generate.cjs +1 -1
  17. package/dist/generate.js +3 -3
  18. package/dist/runtime-dependencies/@eventcatalog__visualiser__studio-fonts.css.mjs +3 -0
  19. package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.css.mjs +3 -0
  20. package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.mjs +3 -0
  21. package/dist/runtime-dependencies/manifest.json +3 -0
  22. package/dist/utils/cli-logger.cjs +1 -1
  23. package/dist/utils/cli-logger.js +2 -2
  24. package/eventcatalog/astro.config.mjs +13 -1
  25. package/eventcatalog/integrations/skip-startup-reload.mjs +31 -0
  26. package/eventcatalog/integrations/stable-dependency-order.mjs +29 -0
  27. package/eventcatalog/src/components/CatalogGraph/CatalogForceGraph.tsx +39 -0
  28. package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +11 -3
  29. package/eventcatalog/src/components/Search/SearchDataLoader.astro +16 -28
  30. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +37 -76
  31. package/eventcatalog/src/components/SideNav/NestedSideBar/storage.ts +2 -0
  32. package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +122 -1
  33. package/eventcatalog/src/components/SideNav/SideNav.astro +9 -1
  34. package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +3 -3
  35. package/eventcatalog/src/features/mcp/apps/canvas/styles.css +3 -1
  36. package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +7 -5
  37. package/eventcatalog/src/features/mcp/apps/generated/canvas.html +133 -71
  38. package/eventcatalog/src/features/mcp/apps/generated/viewer.html +66 -66
  39. package/eventcatalog/src/features/studio/agent-choreography.ts +5 -4
  40. package/eventcatalog/src/features/studio/agents.ts +28 -0
  41. package/eventcatalog/src/features/studio/api/canvases-api.ts +1 -1
  42. package/eventcatalog/src/features/studio/canvas-actions.ts +22 -14
  43. package/eventcatalog/src/features/studio/canvas-preview.ts +1 -2
  44. package/eventcatalog/src/features/studio/catalog.ts +1 -4
  45. package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +246 -115
  46. package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +7 -3
  47. package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +272 -119
  48. package/eventcatalog/src/features/studio/components/ConnectionsChooser.tsx +1 -2
  49. package/eventcatalog/src/features/studio/components/DeleteCanvasDialog.tsx +1 -1
  50. package/eventcatalog/src/features/studio/components/JoinForm.tsx +101 -26
  51. package/eventcatalog/src/features/studio/components/LeftPanel.tsx +102 -241
  52. package/eventcatalog/src/features/studio/components/NodeDetails.tsx +183 -0
  53. package/eventcatalog/src/features/studio/components/OpenInStudioDialog.tsx +1 -1
  54. package/eventcatalog/src/features/studio/components/Presence.tsx +6 -8
  55. package/eventcatalog/src/features/studio/components/ShareDialog.tsx +277 -141
  56. package/eventcatalog/src/features/studio/components/ShortcutsDialog.tsx +41 -0
  57. package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +1005 -354
  58. package/eventcatalog/src/features/studio/document-name.ts +7 -0
  59. package/eventcatalog/src/features/studio/from-visualiser.ts +1 -1
  60. package/eventcatalog/src/features/studio/hooks/presence-sender.ts +19 -2
  61. package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +9 -3
  62. package/eventcatalog/src/features/studio/hooks/use-comments.ts +2 -2
  63. package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +162 -23
  64. package/eventcatalog/src/features/studio/identity.ts +23 -1
  65. package/eventcatalog/src/features/studio/pages/[id].astro +2 -1
  66. package/eventcatalog/src/features/studio/pages/index.astro +1 -2
  67. package/eventcatalog/src/features/studio/server/canvas-mcp.ts +7 -22
  68. package/eventcatalog/src/features/studio/server/runtime.ts +4 -5
  69. package/package.json +3 -3
  70. package/eventcatalog/src/features/studio/canvas-doc.ts +0 -569
  71. package/eventcatalog/src/features/studio/clipboard.ts +0 -97
  72. package/eventcatalog/src/features/studio/components/CanvasControls.tsx +0 -119
  73. package/eventcatalog/src/features/studio/components/Comments.tsx +0 -777
  74. package/eventcatalog/src/features/studio/components/ContainerChooser.tsx +0 -145
  75. package/eventcatalog/src/features/studio/components/DragGhost.tsx +0 -115
  76. package/eventcatalog/src/features/studio/components/Picture.tsx +0 -21
  77. package/eventcatalog/src/features/studio/components/PropertiesPanel.tsx +0 -212
  78. package/eventcatalog/src/features/studio/components/canvas-nodes.tsx +0 -271
  79. package/eventcatalog/src/features/studio/components/icons.ts +0 -53
  80. package/eventcatalog/src/features/studio/components/status.ts +0 -53
  81. package/eventcatalog/src/features/studio/edges.ts +0 -56
  82. package/eventcatalog/src/features/studio/flow-state.ts +0 -121
  83. package/eventcatalog/src/features/studio/grouping.ts +0 -230
  84. package/eventcatalog/src/features/studio/hooks/use-counter-scale.ts +0 -20
  85. package/eventcatalog/src/features/studio/layout.ts +0 -56
  86. package/eventcatalog/src/features/studio/level-layout.ts +0 -145
  87. package/eventcatalog/src/features/studio/levels.ts +0 -337
  88. package/eventcatalog/src/features/studio/node-types.ts +0 -245
  89. package/eventcatalog/src/features/studio/placement.ts +0 -197
  90. package/eventcatalog/src/features/studio/studio.css +0 -273
@@ -1,34 +1,109 @@
1
1
  import { useState } from 'react';
2
+ import { Users } from 'lucide-react';
3
+ import { MAX_NAME_LENGTH, PERSON_COLORS } from '../identity';
2
4
 
3
- export default function JoinForm({ onJoin }: { onJoin: (name: string) => void }) {
5
+ const initials = (name: string) =>
6
+ name
7
+ .trim()
8
+ .split(/\s+/)
9
+ .map((word) => word[0])
10
+ .join('')
11
+ .slice(0, 2)
12
+ .toUpperCase();
13
+
14
+ /**
15
+ * Joining a canvas the first time (without signing in), as Studio on the web joins a live session: the name and colour
16
+ * you'll be seen by (your pointer, your avatar, your comments), picked before you start, with how you'll look to the
17
+ * others. Remembered in this browser for next time.
18
+ */
19
+ export default function JoinForm({
20
+ color,
21
+ onJoin,
22
+ }: {
23
+ /** The colour picked to start with (the one kept in this browser, if there is one) */
24
+ color?: string | null;
25
+ onJoin: (me: { name: string; color: string }) => void;
26
+ }) {
4
27
  const [draft, setDraft] = useState('');
28
+ const [picked, setPicked] = useState(() => color ?? PERSON_COLORS[Math.floor(Math.random() * PERSON_COLORS.length)]);
29
+ const name = draft.trim();
5
30
 
6
31
  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))]"
32
+ <div className="fixed inset-0 z-[80] flex items-center justify-center bg-black/40 p-4">
33
+ <form
34
+ role="dialog"
35
+ aria-modal="true"
36
+ aria-labelledby="join-canvas-title"
37
+ onSubmit={(event) => {
38
+ event.preventDefault();
39
+ if (name) onJoin({ name, color: picked });
40
+ }}
41
+ className="w-full max-w-md rounded-xl border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-card-bg))] p-6 text-center text-[rgb(var(--ec-page-text))] shadow-2xl"
29
42
  >
30
- Join
31
- </button>
32
- </form>
43
+ <span className="mx-auto flex h-11 w-11 items-center justify-center rounded-full bg-[color-mix(in_srgb,rgb(var(--ec-accent))_12%,transparent)] text-[rgb(var(--ec-accent))]">
44
+ <Users aria-hidden className="h-5 w-5" />
45
+ </span>
46
+ <h1 id="join-canvas-title" className="mt-3 text-base font-semibold">
47
+ Join this canvas
48
+ </h1>
49
+ <p className="mt-1.5 text-sm leading-relaxed text-[rgb(var(--ec-page-text-muted))]">
50
+ Everyone here sees your pointer and edits with you, live.
51
+ </p>
52
+
53
+ <label className="mt-5 block text-left text-xs font-medium text-[rgb(var(--ec-page-text-muted))]">
54
+ Your name
55
+ <input
56
+ autoFocus
57
+ value={draft}
58
+ maxLength={MAX_NAME_LENGTH}
59
+ placeholder="What should others call you?"
60
+ onChange={(event) => setDraft(event.target.value)}
61
+ className="mt-1.5 block h-9 w-full rounded-md border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-page-bg))] px-3 text-sm text-[rgb(var(--ec-page-text))] outline-none placeholder:text-[rgb(var(--ec-page-text-muted))] focus:border-[rgb(var(--ec-accent))]"
62
+ />
63
+ </label>
64
+ <div className="mt-4 text-left text-xs font-medium text-[rgb(var(--ec-page-text-muted))]">
65
+ Your colour
66
+ <div role="radiogroup" aria-label="Your colour" className="mt-2 flex gap-2">
67
+ {PERSON_COLORS.map((option) => (
68
+ <button
69
+ key={option}
70
+ type="button"
71
+ role="radio"
72
+ aria-checked={option === picked}
73
+ aria-label={option}
74
+ onClick={() => setPicked(option)}
75
+ className={`h-6 w-6 rounded-full transition-transform hover:scale-110 ${
76
+ option === picked
77
+ ? 'ring-2 ring-[rgb(var(--ec-page-text))] ring-offset-2 ring-offset-[rgb(var(--ec-card-bg))]'
78
+ : ''
79
+ }`}
80
+ style={{ background: option }}
81
+ />
82
+ ))}
83
+ </div>
84
+ </div>
85
+
86
+ {/* How you'll look to the others */}
87
+ {name && (
88
+ <div className="mt-5 flex items-center justify-center gap-2 text-sm">
89
+ <span
90
+ className="flex h-7 w-7 items-center justify-center rounded-full text-[11px] font-semibold text-white"
91
+ style={{ background: picked }}
92
+ >
93
+ {initials(name)}
94
+ </span>
95
+ <span className="font-medium">{name}</span>
96
+ </div>
97
+ )}
98
+
99
+ <button
100
+ type="submit"
101
+ disabled={!name}
102
+ className="mt-5 inline-flex h-10 w-full items-center justify-center rounded-lg bg-[rgb(var(--ec-page-text))] text-sm font-medium text-[rgb(var(--ec-page-bg))] hover:bg-[rgb(var(--ec-page-text)/0.85)] disabled:opacity-40"
103
+ >
104
+ Join canvas
105
+ </button>
106
+ </form>
107
+ </div>
33
108
  );
34
109
  }
@@ -1,75 +1,32 @@
1
- import { memo, useCallback, useMemo, useState, type ReactNode } from 'react';
2
- import { Check, ChevronRight, Plus, Search } from 'lucide-react';
1
+ import { memo, useMemo, useState } from 'react';
2
+ import { Check, Search } from 'lucide-react';
3
3
  import type { CatalogResource } from '../catalog-resources';
4
4
  import { catalogNodeData } from '../catalog';
5
- import { isShownOnLevel, NODE_CATEGORIES, nodeDefinitions } from '../node-types';
6
- import type { Level } from '../levels';
7
- import type { Thread } from '../hooks/use-comments';
8
- import { CommentList } from './Comments';
9
- import { useDragGhost, type StartDrag } from './DragGhost';
10
- import { CATALOG_GROUPS, NODE_ICONS } from './icons';
11
- import { STATUS } from './status';
12
-
13
- /** What's dragged from the left panel onto the canvas: a component's type, or a catalog resource's key */
14
- export const COMPONENT_DRAG_TYPE = 'application/eventcatalog-studio';
5
+ import {
6
+ isShownOnLevel,
7
+ type Level,
8
+ type StartDrag,
9
+ CATALOG_GROUPS,
10
+ STATUS,
11
+ Palette,
12
+ PaletteItem,
13
+ PaletteSection,
14
+ type PaletteTab,
15
+ } from '@eventcatalog/visualiser/studio';
16
+
17
+ /** What's dragged from the left panel onto the canvas: a component's key, or a catalog resource's key */
18
+ export { COMPONENT_DRAG_TYPE } from '@eventcatalog/visualiser/studio';
15
19
  export const CATALOG_DRAG_TYPE = 'application/eventcatalog-studio-resource';
16
20
 
17
21
  /** A catalog group shows this many resources until it's expanded, so big catalogs stay quick */
18
22
  const GROUP_PREVIEW_SIZE = 50;
19
23
 
20
- const itemClass =
21
- '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))]';
22
- const addButtonClass =
23
- 'shrink-0 rounded p-0.5 opacity-0 group-hover:opacity-100 text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-accent))]';
24
- // The sections that are collapsed, by id (e.g. "components:messages"), remembered in this browser
25
- const COLLAPSED_KEY = 'eventcatalog-studio-collapsed';
26
- type Collapsed = { collapsed: ReadonlySet<string>; toggle: (id: string) => void };
27
-
28
- const readCollapsed = (): Set<string> => {
29
- try {
30
- return new Set(JSON.parse(localStorage.getItem(COLLAPSED_KEY) ?? '[]') as string[]);
31
- } catch {
32
- return new Set();
33
- }
34
- };
35
-
36
- function useCollapsed(): Collapsed {
37
- const [collapsed, setCollapsed] = useState(readCollapsed);
38
- const toggle = useCallback((id: string) => {
39
- setCollapsed((current) => {
40
- const next = new Set(current);
41
- if (next.has(id)) next.delete(id);
42
- else next.add(id);
43
- try {
44
- localStorage.setItem(COLLAPSED_KEY, JSON.stringify([...next]));
45
- } catch {
46
- // Not remembered (e.g. storage is blocked): it still works for now
47
- }
48
- return next;
49
- });
50
- }, []);
51
- return useMemo(() => ({ collapsed, toggle }), [collapsed, toggle]);
52
- }
53
-
54
- /** A section's heading, which opens and closes it */
55
- function SectionHeading({ open, onToggle, children }: { open: boolean; onToggle: () => void; children: ReactNode }) {
56
- return (
57
- <h3>
58
- <button
59
- onClick={onToggle}
60
- aria-expanded={open}
61
- 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))]"
62
- >
63
- <ChevronRight size={12} className={`shrink-0 transition-transform ${open ? 'rotate-90' : ''}`} />
64
- {children}
65
- </button>
66
- </h3>
67
- );
68
- }
69
-
70
- type Tab = 'components' | 'catalog' | 'comments';
24
+ export type LeftPanelTab = 'components' | 'catalog';
71
25
 
72
- /** Components and catalog resources to drag onto the canvas, and the comments on it */
26
+ /**
27
+ * Studio's palette (as Studio on the web's): components to drag onto the canvas, and here also the catalog's
28
+ * resources, as a tab in the same card (the comments are listed from the bottom left controls)
29
+ */
73
30
  export default memo(function LeftPanel({
74
31
  level,
75
32
  onShowAll,
@@ -77,10 +34,9 @@ export default memo(function LeftPanel({
77
34
  keysOnCanvas,
78
35
  onAddComponent,
79
36
  onAddResource,
80
- threads,
81
- showResolved,
82
- onToggleResolved,
83
- onOpenThread,
37
+ tab,
38
+ onTabChange,
39
+ startCollapsed,
84
40
  }: {
85
41
  /** The level shown: only what it shows is listed, so what's added stays on it */
86
42
  level: Level;
@@ -91,71 +47,48 @@ export default memo(function LeftPanel({
91
47
  keysOnCanvas: Set<string>;
92
48
  onAddComponent: (type: string) => void;
93
49
  onAddResource: (key: string) => void;
94
- threads: Thread[];
95
- showResolved: boolean;
96
- onToggleResolved: () => void;
97
- onOpenThread: (thread: Thread) => void;
50
+ /** The tab shown, when the canvas chooses it */
51
+ tab?: LeftPanelTab;
52
+ onTabChange?: (tab: LeftPanelTab) => void;
53
+ /** Starts folded away (e.g. in a chat, where there's little room) */
54
+ startCollapsed?: boolean;
98
55
  }) {
99
- const [tab, setTab] = useState<Tab>('components');
100
- // What's being dragged onto the canvas, drawn as the node it becomes (see DragGhost)
101
- const { startDrag, ghost } = useDragGhost();
102
- const sections = useCollapsed();
103
- const openThreads = threads.filter((thread) => !thread.resolved).length;
104
56
  const shownResources = useMemo(
105
57
  () => (level === 3 ? resources : resources.filter((resource) => isShownOnLevel(resource.node.type, level))),
106
58
  [resources, level]
107
59
  );
108
- const tabClass = (active: boolean) =>
109
- `flex flex-1 items-center justify-center gap-1 whitespace-nowrap rounded-md px-1.5 py-1 text-xs font-medium ${
110
- active ? 'bg-[rgb(var(--ec-page-bg))] shadow-sm' : 'text-[rgb(var(--ec-page-text-muted))]'
111
- }`;
60
+ const tabs = useMemo(
61
+ (): PaletteTab[] => [
62
+ {
63
+ id: 'catalog',
64
+ label: 'Catalog Resources',
65
+ render: (startDrag) => (
66
+ <>
67
+ {level !== 3 && <LevelHint level={level} onShowAll={onShowAll} />}
68
+ <CatalogList resources={shownResources} keysOnCanvas={keysOnCanvas} onAdd={onAddResource} startDrag={startDrag} />
69
+ </>
70
+ ),
71
+ },
72
+ ],
73
+ [level, onShowAll, shownResources, keysOnCanvas, onAddResource]
74
+ );
112
75
 
113
76
  return (
114
- <aside className="flex w-72 shrink-0 flex-col border-r bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))]">
115
- <div className="m-3 flex gap-1 rounded-lg p-1 bg-[rgb(var(--ec-page-border)/0.5)]">
116
- <button className={tabClass(tab === 'components')} onClick={() => setTab('components')}>
117
- Components
118
- </button>
119
- <button className={tabClass(tab === 'catalog')} onClick={() => setTab('catalog')}>
120
- Catalog
121
- </button>
122
- <button className={tabClass(tab === 'comments')} onClick={() => setTab('comments')}>
123
- Comments
124
- {openThreads > 0 && (
125
- <span className="rounded-full px-1.5 text-[10px] leading-4 bg-[rgb(var(--ec-button-bg))] text-[rgb(var(--ec-button-text))]">
126
- {openThreads}
127
- </span>
128
- )}
129
- </button>
130
- </div>
131
- {tab !== 'comments' && level !== 3 && <LevelHint level={level} onShowAll={onShowAll} />}
132
- {tab === 'components' && <ComponentList level={level} onAdd={onAddComponent} startDrag={startDrag} sections={sections} />}
133
- {tab === 'catalog' && (
134
- <CatalogList
135
- resources={shownResources}
136
- keysOnCanvas={keysOnCanvas}
137
- onAdd={onAddResource}
138
- startDrag={startDrag}
139
- sections={sections}
140
- />
141
- )}
142
- {tab === 'comments' && (
143
- <CommentList threads={threads} showResolved={showResolved} onToggleResolved={onToggleResolved} onOpen={onOpenThread} />
144
- )}
145
- {ghost}
146
- </aside>
77
+ <Palette
78
+ level={level}
79
+ onAddComponent={onAddComponent}
80
+ tabs={tabs}
81
+ tab={tab}
82
+ onTabChange={onTabChange as ((tab: string) => void) | undefined}
83
+ startCollapsed={startCollapsed}
84
+ />
147
85
  );
148
86
  });
149
87
 
150
- const listedDefinitions = NODE_CATEGORIES.map((category) => ({
151
- ...category,
152
- definitions: nodeDefinitions.filter((definition) => definition.category === category.id && !definition.unlisted),
153
- }));
154
-
155
88
  /** On L1 and L2: only what the level shows is listed (added there, it stays there), the rest is on L3 */
156
89
  function LevelHint({ level, onShowAll }: { level: Level; onShowAll: () => void }) {
157
90
  return (
158
- <p className="mx-3 mb-2 text-[11px] leading-snug text-[rgb(var(--ec-page-text-muted))]">
91
+ <p className="text-[11px] leading-snug text-[rgb(var(--ec-page-text-muted))]">
159
92
  Showing what L{level} shows.{' '}
160
93
  <button onClick={onShowAll} className="font-medium text-[rgb(var(--ec-accent))] hover:underline">
161
94
  Go to L3 for everything
@@ -164,77 +97,19 @@ function LevelHint({ level, onShowAll }: { level: Level; onShowAll: () => void }
164
97
  );
165
98
  }
166
99
 
167
- // Each component's data, made once, for the node shown while it's dragged
168
- const componentPreviews = new Map(
169
- nodeDefinitions.map((definition) => [definition.type, { type: definition.type, data: definition.createData() }])
170
- );
171
-
172
- const ComponentList = memo(function ComponentList({
173
- level,
174
- onAdd,
175
- startDrag,
176
- sections: { collapsed, toggle },
177
- }: {
178
- level: Level;
179
- onAdd: (type: string) => void;
180
- startDrag: StartDrag;
181
- sections: Collapsed;
182
- }) {
183
- return (
184
- <div className="min-h-0 flex-1 space-y-3 overflow-y-auto px-3 pb-3">
185
- {listedDefinitions.map((category) => {
186
- const definitions = category.definitions.filter((definition) => isShownOnLevel(definition.type, level));
187
- if (definitions.length === 0) return null;
188
- const id = `components:${category.id}`;
189
- const open = !collapsed.has(id);
190
- return (
191
- <section key={category.id} className="space-y-1.5">
192
- <SectionHeading open={open} onToggle={() => toggle(id)}>
193
- {category.label}
194
- </SectionHeading>
195
- {open &&
196
- definitions.map(({ type, label }) => {
197
- const { icon: Icon, className } = NODE_ICONS[type];
198
- return (
199
- <div
200
- key={type}
201
- draggable
202
- onDragStart={startDrag(COMPONENT_DRAG_TYPE, type, componentPreviews.get(type)!)}
203
- className={itemClass}
204
- >
205
- <span className="flex items-center gap-2 text-xs font-medium">
206
- <Icon size={16} className={className} />
207
- {label}
208
- </span>
209
- <button onClick={() => onAdd(type)} title={`Add ${label}`} className={addButtonClass}>
210
- <Plus size={14} />
211
- </button>
212
- </div>
213
- );
214
- })}
215
- </section>
216
- );
217
- })}
218
- </div>
219
- );
220
- });
221
-
222
- const CatalogList = memo(function CatalogList({
100
+ function CatalogList({
223
101
  resources,
224
102
  keysOnCanvas,
225
103
  onAdd,
226
104
  startDrag,
227
- sections: { collapsed, toggle },
228
105
  }: {
229
106
  resources: CatalogResource[];
230
107
  keysOnCanvas: Set<string>;
231
108
  onAdd: (key: string) => void;
232
109
  startDrag: StartDrag;
233
- sections: Collapsed;
234
110
  }) {
235
111
  const [search, setSearch] = useState('');
236
112
  const [expanded, setExpanded] = useState<Set<string>>(() => new Set());
237
- const searching = search.trim() !== '';
238
113
  const groups = useMemo(() => {
239
114
  const term = search.trim().toLowerCase();
240
115
  const matches = term
@@ -247,60 +122,52 @@ const CatalogList = memo(function CatalogList({
247
122
  }, [resources, search]);
248
123
 
249
124
  return (
250
- <div className="flex min-h-0 flex-1 flex-col">
251
- <div className="relative mx-3 mb-2">
125
+ <>
126
+ <div className="relative">
252
127
  <Search size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-[rgb(var(--ec-icon-color))]" />
253
128
  <input
254
129
  value={search}
255
130
  onChange={(e) => setSearch(e.target.value)}
256
131
  placeholder="Search the catalog"
257
- 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))]"
132
+ className="w-full rounded-lg 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))]"
258
133
  />
259
134
  </div>
260
- <div className="min-h-0 flex-1 space-y-3 overflow-y-auto px-3 pb-3">
261
- {groups.length === 0 && (
262
- <p className="py-6 text-center text-xs text-[rgb(var(--ec-page-text-muted))]">No resources found.</p>
263
- )}
264
- {groups.map(({ collection, label, icon, className, items }) => {
265
- const id = `catalog:${collection}`;
266
- // While searching, every group with matches shows them
267
- const open = searching || !collapsed.has(id);
268
- const shown = !open ? [] : expanded.has(collection) ? items : items.slice(0, GROUP_PREVIEW_SIZE);
269
- return (
270
- <section key={collection} className="space-y-1.5">
271
- <SectionHeading open={open} onToggle={() => toggle(id)}>
272
- {label} <span className="font-normal">({items.length})</span>
273
- </SectionHeading>
274
- {shown.map((resource) => (
275
- <CatalogItem
276
- key={resource.key}
277
- resource={resource}
278
- icon={icon}
279
- iconClassName={className}
280
- onCanvas={keysOnCanvas.has(resource.key)}
281
- onAdd={onAdd}
282
- startDrag={startDrag}
283
- />
284
- ))}
285
- {open && shown.length < items.length && (
286
- <button
287
- onClick={() => setExpanded((current) => new Set(current).add(collection))}
288
- 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))]"
289
- >
290
- Show all {items.length} {label.toLowerCase()}
291
- </button>
292
- )}
293
- </section>
294
- );
295
- })}
296
- </div>
297
- </div>
135
+ {groups.length === 0 && (
136
+ <p className="py-6 text-center text-xs text-[rgb(var(--ec-page-text-muted))]">No resources found.</p>
137
+ )}
138
+ {groups.map(({ collection, label, icon, className, items }) => {
139
+ const shown = expanded.has(collection) ? items : items.slice(0, GROUP_PREVIEW_SIZE);
140
+ return (
141
+ <PaletteSection key={collection} title={`${label} (${items.length})`}>
142
+ {shown.map((resource) => (
143
+ <CatalogItem
144
+ key={resource.key}
145
+ resource={resource}
146
+ icon={icon}
147
+ iconClassName={className}
148
+ onCanvas={keysOnCanvas.has(resource.key)}
149
+ onAdd={onAdd}
150
+ startDrag={startDrag}
151
+ />
152
+ ))}
153
+ {shown.length < items.length && (
154
+ <button
155
+ onClick={() => setExpanded((current) => new Set(current).add(collection))}
156
+ 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))]"
157
+ >
158
+ Show all {items.length} {label.toLowerCase()}
159
+ </button>
160
+ )}
161
+ </PaletteSection>
162
+ );
163
+ })}
164
+ </>
298
165
  );
299
- });
166
+ }
300
167
 
301
168
  const CatalogItem = memo(function CatalogItem({
302
169
  resource,
303
- icon: Icon,
170
+ icon,
304
171
  iconClassName,
305
172
  onCanvas,
306
173
  onAdd,
@@ -316,27 +183,21 @@ const CatalogItem = memo(function CatalogItem({
316
183
  // Drawn like the card it becomes (domains and systems can become containers too, but start as cards)
317
184
  const preview = useMemo(() => ({ type: resource.node.type, data: catalogNodeData(resource) }), [resource]);
318
185
  return (
319
- <div
320
- draggable
186
+ <PaletteItem
187
+ icon={icon}
188
+ iconClassName={iconClassName}
189
+ label={resource.name}
190
+ description={resource.summary ? `v${resource.version} · ${resource.summary}` : `v${resource.version}`}
321
191
  onDragStart={startDrag(CATALOG_DRAG_TYPE, resource.key, preview)}
322
- title={resource.summary}
323
- className={itemClass}
324
- >
325
- <span className="flex min-w-0 items-center gap-2 text-xs font-medium">
326
- <Icon size={16} className={`shrink-0 ${iconClassName}`} />
327
- <span className="truncate">{resource.name}</span>
328
- <span className="shrink-0 text-[10px] text-[rgb(var(--ec-page-text-muted))]">v{resource.version}</span>
329
- </span>
330
- <span className="flex shrink-0 items-center gap-1">
331
- {onCanvas && (
332
- <span title="On the canvas">
192
+ onClick={() => onAdd(resource.key)}
193
+ title={`${resource.name}: drag onto the canvas, or click to add`}
194
+ trailing={
195
+ onCanvas ? (
196
+ <span title="On the canvas" className="flex-none">
333
197
  <Check size={14} className={STATUS.success.text} />
334
198
  </span>
335
- )}
336
- <button onClick={() => onAdd(resource.key)} title={`Add ${resource.name}`} className={addButtonClass}>
337
- <Plus size={14} />
338
- </button>
339
- </span>
340
- </div>
199
+ ) : undefined
200
+ }
201
+ />
341
202
  );
342
203
  });