Skip to content

[Grida Canvas] Editor API (Scripting interface V2) #355

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
May 29, 2025
8 changes: 3 additions & 5 deletions editor/app/(tools)/(playground)/playground/image/_page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import {
ViewportRoot,
EditorSurface,
AutoInitialFitTransformer,
useDocument,
useCurrentEditor,
} from "@/grida-canvas-react";
import { FontFamilyListProvider } from "@/scaffolds/sidecontrol/controls/font-family";
import { useEditorHotKeys } from "@/grida-canvas-react/viewport/hotkeys";
Expand Down Expand Up @@ -124,15 +124,13 @@ export default function ImagePlayground() {
function CanvasConsumer() {
const { withAuth, session } = useContinueWithAuth();
const credits = useCredits();
const editor = useDocument();
const editor = useCurrentEditor();
const [prompt, setPrompt] = useState("");
const model = useImageModelConfig("black-forest-labs/flux-schnell");
const { generate, key, loading, image, start, end } = useGenerateImage();

const onCommit = (value: { text: string }) => {
const id = editor.createNodeId();
editor.insertNode({
_$id: id,
const id = editor.insertNode({
type: "image",
name: value.text,
width: model.width,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
AutoInitialFitTransformer,
StandaloneSceneBackground,
UserCustomTemplatesProvider,
useDocument,
useCurrentEditor,
} from "@/grida-canvas-react";
import { Zoom } from "@/scaffolds/sidecontrol/sidecontrol-node-selection";
import { WorkbenchUI } from "@/components/workbench";
Expand All @@ -21,7 +21,6 @@ import { PreviewProvider } from "@/grida-canvas-react-starter-kit/starterkit-pre
import { Platform } from "@/lib/platform";
import { TemplateData } from "@/theme/templates/west-referral/templates";
import { ReadonlyPropsEditorInstance } from "@/scaffolds/props-editor";
import { useTransform } from "@/grida-canvas-react/provider";
import MessageAppFrame from "@/components/frames/message-app-frame";
import { editor } from "@/grida-canvas";
import { useEditor } from "@/grida-canvas-react";
Expand Down Expand Up @@ -276,14 +275,13 @@ export function CampaignTemplateDuo001Viewer({

// will be removed after useEditor is ready
function EditorUXServer({ focus }: { focus: { node?: string } }) {
const { select } = useDocument();
const { fit } = useTransform();
const editor = useCurrentEditor();

useEffect(
() => {
if (focus.node) {
select([focus.node]);
fit([focus.node], { margin: 64, animate: true });
editor.select([focus.node]);
editor.fit([focus.node], { margin: 64, animate: true });
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ import {
StandaloneDocumentEditor,
ViewportRoot,
EditorSurface,
useDocument,
useRootTemplateInstanceNode,
} from "@/grida-canvas-react";
import { composeEditorDocumentAction } from "@/scaffolds/editor/action";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { useDocument } from "@/grida-canvas-react";
import { useCurrentEditor } from "@/grida-canvas-react";

type ArtboardData = {
name: string;
Expand All @@ -15,10 +15,10 @@ type ArtboardData = {
};

const ArtboardList = () => {
const { insertNode } = useDocument();
const editor = useCurrentEditor();

const onClickItem = (item: ArtboardData) => {
insertNode({
editor.insertNode({
type: "container",
position: "absolute",
name: item.name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import React, {
useCallback,
useRef,
} from "react";
import { useDocument } from "@/grida-canvas-react";
import { useCurrentEditor, useEditorState } from "@/grida-canvas-react";
import {
Tree,
TreeDragLine,
Expand Down Expand Up @@ -35,11 +35,7 @@ import {
EyeClosedIcon,
LockOpen1Icon,
} from "@radix-ui/react-icons";
import {
useCurrentScene,
useNodeAction,
useTransform,
} from "@/grida-canvas-react/provider";
import { useCurrentSceneState } from "@/grida-canvas-react/provider";
import { NodeTypeIcon } from "@/grida-canvas-react-starter-kit/starterkit-icons/node-type-icon";
import { cn } from "@/components/lib/utils";
import grida from "@grida/schema";
Expand All @@ -52,7 +48,7 @@ function SceneItemContextMenuWrapper({
scene_id: string;
onStartRenaming?: () => void;
}>) {
const { deleteScene, duplicateScene } = useDocument();
const editor = useCurrentEditor();

return (
<ContextMenu>
Expand All @@ -71,7 +67,7 @@ function SceneItemContextMenuWrapper({
</ContextMenuItem>
<ContextMenuItem
onSelect={() => {
duplicateScene(scene_id);
editor.duplicateScene(scene_id);
}}
className="text-xs"
>
Expand All @@ -80,7 +76,7 @@ function SceneItemContextMenuWrapper({
<ContextMenuSeparator />
<ContextMenuItem
onSelect={() => {
deleteScene(scene_id);
editor.deleteScene(scene_id);
}}
className="text-xs"
>
Expand All @@ -92,7 +88,9 @@ function SceneItemContextMenuWrapper({
}

export function ScenesList() {
const { scenes: scenesmap, scene_id, loadScene, renameScene } = useDocument();
const editor = useCurrentEditor();
const scenesmap = useEditorState(editor, (state) => state.document.scenes);
const scene_id = useEditorState(editor, (state) => state.scene_id);

const scenes = useMemo(() => {
return Object.values(scenesmap).sort(
Expand All @@ -110,7 +108,7 @@ export function ScenesList() {
selectedItems: scene_id ? [scene_id] : [],
},
setSelectedItems: (items) => {
loadScene((items as string[])[0]);
editor.loadScene((items as string[])[0]);
},
getItemName: (item) => {
if (item.getId() === "<document>") return "<document>";
Expand Down Expand Up @@ -174,7 +172,7 @@ export function ScenesList() {
isRenaming={isRenaming}
initialValue={scene.name}
onValueCommit={(name) => {
renameScene(scene.id, name);
editor.renameScene(scene.id, name);
tree.abortRenaming();
}}
className="font-normal h-8 text-xs! px-2! py-1.5!"
Expand All @@ -196,17 +194,15 @@ function NodeHierarchyItemContextMenuWrapper({
node_id: string;
onStartRenaming?: () => void;
}>) {
const { copy, deleteNode, order } = useDocument();
const { fit } = useTransform();
const change = useNodeAction(node_id)!;
const editor = useCurrentEditor();

return (
<ContextMenu>
<ContextMenuTrigger>{children}</ContextMenuTrigger>
<ContextMenuContent className="min-w-52">
<ContextMenuItem
onSelect={() => {
copy(node_id);
editor.copy(node_id);
}}
className="text-xs"
>
Expand All @@ -225,42 +221,48 @@ function NodeHierarchyItemContextMenuWrapper({
{/* <ContextMenuItem onSelect={() => {}}>Copy</ContextMenuItem> */}
{/* <ContextMenuItem>Paste here</ContextMenuItem> */}
<ContextMenuItem
onSelect={() => order(node_id, "front")}
onSelect={() => editor.order(node_id, "front")}
className="text-xs"
>
Bring to front
<ContextMenuShortcut>{"]"}</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem
onSelect={() => order(node_id, "back")}
onSelect={() => editor.order(node_id, "back")}
className="text-xs"
>
Send to back
<ContextMenuShortcut>{"["}</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuSeparator />
{/* <ContextMenuItem>Add Container</ContextMenuItem> */}
<ContextMenuItem onSelect={change.toggleActive} className="text-xs">
<ContextMenuItem
onSelect={() => editor.toggleNodeActive(node_id)}
className="text-xs"
>
Set Active/Inactive
<ContextMenuShortcut>{"⌘⇧H"}</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem
onSelect={() => {
fit([node_id], { margin: 64, animate: true });
editor.fit([node_id], { margin: 64, animate: true });
}}
className="text-xs"
>
Zoom to fit
<ContextMenuShortcut>{"⇧1"}</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem onSelect={change.toggleLocked} className="text-xs">
<ContextMenuItem
onSelect={() => editor.toggleNodeLocked(node_id)}
className="text-xs"
>
Lock/Unlock
<ContextMenuShortcut>{"⌘⇧L"}</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem
onSelect={() => {
deleteNode(node_id);
editor.deleteNode(node_id);
}}
className="text-xs"
>
Expand All @@ -273,37 +275,24 @@ function NodeHierarchyItemContextMenuWrapper({
}

export function NodeHierarchyList() {
const {
document,
mv,
select,
hoverNode,
toggleNodeLocked,
toggleNodeActive,
changeNodeName,
} = useDocument();

const { id, name, children, selection, hovered_node_id } = useCurrentScene();
const editor = useCurrentEditor();
const document_ctx = useEditorState(editor, (state) => state.document_ctx);

const expandedItems = useMemo(() => {
return children.filter(
(id) => (document.nodes[id] as grida.program.nodes.UnknwonNode).expanded
);
}, [id, children]);
const { id, name, children, selection, hovered_node_id } =
useCurrentSceneState();

// root item id must be "<root>"
const tree = useTree<grida.program.nodes.Node>({
rootItemId: "<root>",
canReorder: true,
initialState: {
expandedItems: expandedItems,
selectedItems: selection,
},
state: {
selectedItems: selection,
},
setSelectedItems: (items) => {
select(items as string[]);
editor.select(items as string[]);
},
getItemName: (item) => {
if (item.getId() === "<root>") {
Expand All @@ -320,21 +309,18 @@ export function NodeHierarchyList() {
const target_id = target.item.getId();
const index =
"insertionIndex" in target ? target.insertionIndex : undefined;
mv(ids, target_id, index);
editor.mv(ids, target_id, index);
},
indent: 6,
dataLoader: {
getItem(itemId) {
return document.nodes[itemId];
return editor.state.document.nodes[itemId];
},
getChildren: (itemId) => {
if (itemId === "<root>") {
return children;
}
const node = document.nodes[itemId];
return (
(node as grida.program.nodes.i.IChildrenReference)?.children || []
);
return editor.state.document_ctx.__ctx_nid_to_children_ids[itemId];
},
},
features: [
Expand All @@ -347,7 +333,7 @@ export function NodeHierarchyList() {

useEffect(() => {
tree.rebuildTree();
}, [document]);
}, [document_ctx]);

return (
<Tree tree={tree} indent={6}>
Expand All @@ -373,10 +359,10 @@ export function NodeHierarchyList() {
item={item}
className="w-full h-7 max-h-7 py-0.5"
onPointerEnter={() => {
hoverNode(node.id, "enter");
editor.hoverNode(node.id, "enter");
}}
onPointerLeave={() => {
hoverNode(node.id, "leave");
editor.hoverNode(node.id, "leave");
}}
>
<TreeItemLabel
Expand All @@ -397,7 +383,7 @@ export function NodeHierarchyList() {
isRenaming={isRenaming}
initialValue={node.name}
onValueCommit={(name) => {
changeNodeName(node.id, name);
editor.changeNodeName(node.id, name);
tree.abortRenaming();
}}
className="px-1 py-0.5 font-normal text-[11px]"
Expand All @@ -411,7 +397,7 @@ export function NodeHierarchyList() {
<button
onClick={(e) => {
e.stopPropagation();
toggleNodeLocked(node.id);
editor.toggleNodeLocked(node.id);
}}
>
{node.locked ? (
Expand All @@ -423,7 +409,7 @@ export function NodeHierarchyList() {
<button
onClick={(e) => {
e.stopPropagation();
toggleNodeActive(node.id);
editor.toggleNodeActive(node.id);
}}
>
{node.active ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import React, { useState } from "react";
import {
useDocument,
useDocumentState,
StandaloneRootNodeContent,
StandaloneSceneBackground,
type StandaloneDocumentContentProps,
Expand All @@ -22,7 +22,7 @@ import {
} from "@radix-ui/react-icons";
import { toast } from "sonner";
import { useHotkeys } from "react-hotkeys-hook";
import { useCurrentScene } from "@/grida-canvas-react/provider";
import { useCurrentSceneState } from "@/grida-canvas-react/provider";
import Resizable from "./resizable";
import ErrorBoundary from "@/scaffolds/playground-canvas/error-boundary";
import { Input } from "@/components/ui/input";
Expand All @@ -44,8 +44,8 @@ const Context = React.createContext<{
export function PreviewProvider({
children,
}: React.PropsWithChildren<StandaloneDocumentContentProps>) {
const { document, document_ctx } = useDocument();
const scene = useCurrentScene();
const { document, document_ctx } = useDocumentState();
const scene = useCurrentSceneState();
const [mode, setMode] = useState<"framed" | "fullscreen">("framed");
const [open, setOpen] = useState(false);
const [id, setId] = useState<string>();
Expand Down
Loading