feat(editor): Embed Studio button in the top bar
Browse filesAdds a chart button to the TopBar that opens the Embed Studio. Smart entry
point: when charts already exist it opens the studio on one of them (prefers
a real chart over the always-present banner) without mutating the document;
only when there is no chart does it fall back to inserting a fresh one,
mirroring the slash menu's "New Chart" flow. Both paths route through the
existing `open-embed-studio` event so session/key handling stays in one place.
Co-authored-by: Cursor <cursoragent@cursor.com>
- frontend/src/App.tsx +49 -0
- frontend/src/components/TopBar.tsx +16 -0
frontend/src/App.tsx
CHANGED
|
@@ -230,6 +230,54 @@ export default function App() {
|
|
| 230 |
[],
|
| 231 |
);
|
| 232 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
// When the user opens the chat with an active selection, broadcast it
|
| 234 |
// via Yjs awareness so every collaborator (including us) sees a
|
| 235 |
// persistent highlight "my agent is working on this range". The
|
|
@@ -1104,6 +1152,7 @@ export default function App() {
|
|
| 1104 |
onOpenSettings={() => setSettingsOpen(true)}
|
| 1105 |
onOpenPublish={openPublishDialog}
|
| 1106 |
onOpenMobileToc={() => setTocSidebarOpen(true)}
|
|
|
|
| 1107 |
/>
|
| 1108 |
|
| 1109 |
{!isEditorReady && (
|
|
|
|
| 230 |
[],
|
| 231 |
);
|
| 232 |
|
| 233 |
+
// Global entry point for the Embed Studio (TopBar button). Smart
|
| 234 |
+
// behaviour: if charts already exist, open the studio on one of them
|
| 235 |
+
// (browse/edit mode via the FilesSidebar) without touching the
|
| 236 |
+
// document. Only when there is no chart at all do we fall back to
|
| 237 |
+
// creating a fresh one - mirroring the slash menu's "New Chart" flow.
|
| 238 |
+
// Both paths route through the existing `open-embed-studio` listener
|
| 239 |
+
// so session/key handling stays in one place.
|
| 240 |
+
const handleOpenEmbedStudio = useCallback(() => {
|
| 241 |
+
const keys = embedStore ? embedStore.keys() : [];
|
| 242 |
+
if (keys.length > 0) {
|
| 243 |
+
// Prefer a real chart over the always-present banner so the
|
| 244 |
+
// studio lands on content the user most likely wants to tweak.
|
| 245 |
+
const preferred = keys.find((k) => k !== "banner.html") ?? keys[0];
|
| 246 |
+
window.dispatchEvent(
|
| 247 |
+
new CustomEvent("open-embed-studio", { detail: { src: preferred } }),
|
| 248 |
+
);
|
| 249 |
+
return;
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
const editor = editorRef.current;
|
| 253 |
+
if (!editor) return;
|
| 254 |
+
const id = `d3-chart-${Date.now().toString(36)}`;
|
| 255 |
+
const src = `${id}.html`;
|
| 256 |
+
(editor.chain().focus() as any).insertHtmlEmbed().run();
|
| 257 |
+
setTimeout(() => {
|
| 258 |
+
const { doc } = editor.state;
|
| 259 |
+
let targetPos = -1;
|
| 260 |
+
doc.descendants((node, pos) => {
|
| 261 |
+
if (node.type.name === "htmlEmbed" && !node.attrs.src) {
|
| 262 |
+
targetPos = pos;
|
| 263 |
+
return false;
|
| 264 |
+
}
|
| 265 |
+
});
|
| 266 |
+
if (targetPos >= 0) {
|
| 267 |
+
editor.view.dispatch(
|
| 268 |
+
editor.state.tr.setNodeMarkup(targetPos, undefined, {
|
| 269 |
+
...editor.state.doc.nodeAt(targetPos)?.attrs,
|
| 270 |
+
src,
|
| 271 |
+
title: "New chart",
|
| 272 |
+
}),
|
| 273 |
+
);
|
| 274 |
+
}
|
| 275 |
+
window.dispatchEvent(
|
| 276 |
+
new CustomEvent("open-embed-studio", { detail: { src } }),
|
| 277 |
+
);
|
| 278 |
+
}, 50);
|
| 279 |
+
}, [embedStore]);
|
| 280 |
+
|
| 281 |
// When the user opens the chat with an active selection, broadcast it
|
| 282 |
// via Yjs awareness so every collaborator (including us) sees a
|
| 283 |
// persistent highlight "my agent is working on this range". The
|
|
|
|
| 1152 |
onOpenSettings={() => setSettingsOpen(true)}
|
| 1153 |
onOpenPublish={openPublishDialog}
|
| 1154 |
onOpenMobileToc={() => setTocSidebarOpen(true)}
|
| 1155 |
+
onOpenEmbedStudio={handleOpenEmbedStudio}
|
| 1156 |
/>
|
| 1157 |
|
| 1158 |
{!isEditorReady && (
|
frontend/src/components/TopBar.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
| 7 |
Settings,
|
| 8 |
Upload,
|
| 9 |
Eye,
|
|
|
|
| 10 |
Sun,
|
| 11 |
Moon,
|
| 12 |
Menu,
|
|
@@ -50,6 +51,11 @@ interface TopBarProps {
|
|
| 50 |
onOpenSettings: () => void;
|
| 51 |
onOpenPublish: () => void;
|
| 52 |
onOpenMobileToc: () => void;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
}
|
| 54 |
|
| 55 |
/**
|
|
@@ -72,6 +78,7 @@ export function TopBar({
|
|
| 72 |
onOpenSettings,
|
| 73 |
onOpenPublish,
|
| 74 |
onOpenMobileToc,
|
|
|
|
| 75 |
}: TopBarProps) {
|
| 76 |
const publishTooltip = !canEdit
|
| 77 |
? "Read-only access - you don't have write rights on this Space"
|
|
@@ -202,6 +209,15 @@ export function TopBar({
|
|
| 202 |
<Settings size={18} />
|
| 203 |
</button>
|
| 204 |
</Tooltip>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
<Tooltip title="Preview article">
|
| 206 |
<button
|
| 207 |
className="icon-btn"
|
|
|
|
| 7 |
Settings,
|
| 8 |
Upload,
|
| 9 |
Eye,
|
| 10 |
+
BarChart3,
|
| 11 |
Sun,
|
| 12 |
Moon,
|
| 13 |
Menu,
|
|
|
|
| 51 |
onOpenSettings: () => void;
|
| 52 |
onOpenPublish: () => void;
|
| 53 |
onOpenMobileToc: () => void;
|
| 54 |
+
/**
|
| 55 |
+
* Open the Embed Studio from the toolbar. Smart entry point: lands on
|
| 56 |
+
* an existing chart when there is one, otherwise creates a new chart.
|
| 57 |
+
*/
|
| 58 |
+
onOpenEmbedStudio: () => void;
|
| 59 |
}
|
| 60 |
|
| 61 |
/**
|
|
|
|
| 78 |
onOpenSettings,
|
| 79 |
onOpenPublish,
|
| 80 |
onOpenMobileToc,
|
| 81 |
+
onOpenEmbedStudio,
|
| 82 |
}: TopBarProps) {
|
| 83 |
const publishTooltip = !canEdit
|
| 84 |
? "Read-only access - you don't have write rights on this Space"
|
|
|
|
| 209 |
<Settings size={18} />
|
| 210 |
</button>
|
| 211 |
</Tooltip>
|
| 212 |
+
<Tooltip title="Embed Studio (charts)">
|
| 213 |
+
<button
|
| 214 |
+
className="icon-btn"
|
| 215 |
+
onClick={onOpenEmbedStudio}
|
| 216 |
+
aria-label="Open Embed Studio"
|
| 217 |
+
>
|
| 218 |
+
<BarChart3 size={18} />
|
| 219 |
+
</button>
|
| 220 |
+
</Tooltip>
|
| 221 |
<Tooltip title="Preview article">
|
| 222 |
<button
|
| 223 |
className="icon-btn"
|