lock & unlock shapes, clean up overrides & context menu, make embed element easier to interact with

This commit is contained in:
Jeff-Emmett 2025-03-15 01:03:55 -07:00
parent 36a8dfe853
commit 4e83a577f0
6 changed files with 281 additions and 274 deletions

View File

@ -376,4 +376,24 @@ p:has(+ ol) {
box-shadow: 0 0px 16px rgba(0, 0, 0, 0.15);
overflow: hidden;
background-color: white;
}
.lock-indicator {
position: absolute;
width: 24px;
height: 24px;
background: white;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 1000;
transition: transform 0.2s ease;
}
.lock-indicator:hover {
transform: scale(1.1) !important;
background: #f0f0f0;
}

View File

@ -1,6 +1,6 @@
import { useSync } from "@tldraw/sync"
import { useMemo, useEffect, useState } from "react"
import { Tldraw, Editor } from "tldraw"
import { Tldraw, Editor, TLShapeId } from "tldraw"
import { useParams } from "react-router-dom"
import { ChatBoxTool } from "@/tools/ChatBoxTool"
import { ChatBoxShape } from "@/shapes/ChatBoxShapeUtil"
@ -30,7 +30,13 @@ import { makeRealSettings, applySettingsMigrations } from "@/lib/settings"
import { PromptShapeTool } from "@/tools/PromptShapeTool"
import { PromptShape } from "@/shapes/PromptShapeUtil"
import { llm } from "@/utils/llmUtils"
import { setInitialCameraFromUrl } from "@/ui/cameraUtils"
import {
lockElement,
unlockElement,
setInitialCameraFromUrl,
initLockIndicators,
watchForLockedShapes,
} from "@/ui/cameraUtils"
// Default to production URL if env var isn't available
export const WORKER_URL = "https://jeffemmett-canvas.jeffemmett.workers.dev"
@ -84,6 +90,13 @@ export function Board() {
}
}, [])
// Remove the URL-based locking effect and replace with store-based initialization
useEffect(() => {
if (!editor) return
initLockIndicators(editor)
watchForLockedShapes(editor)
}, [editor])
return (
<div style={{ position: "fixed", inset: 0 }}>
<Tldraw
@ -91,7 +104,16 @@ export function Board() {
shapeUtils={customShapeUtils}
tools={customTools}
components={components}
overrides={overrides}
overrides={{
...overrides,
actions: (editor, actions, helpers) => {
const customActions = overrides.actions?.(editor, actions, helpers) ?? {}
return {
...actions,
...customActions,
}
}
}}
cameraOptions={{
zoomSteps: [
0.001, // Min zoom

View File

@ -2,6 +2,9 @@ import { BaseBoxShapeUtil, TLBaseShape } from "tldraw"
import { useCallback, useState } from "react"
//import Embed from "react-embed"
//TODO: FIX PEN AND MOBILE INTERACTION WITH EDITING EMBED URL - DEFAULT TO TEXT SELECTED
export type IEmbedShape = TLBaseShape<
"Embed",
{
@ -372,27 +375,53 @@ export class EmbedShape extends BaseBoxShapeUtil<IEmbedShape> {
return (
<div style={wrapperStyle}>
{controls("")}
<div style={contentStyle}>
<div
style={{
...contentStyle,
cursor: 'text', // Add text cursor to indicate clickable
touchAction: 'none', // Prevent touch scrolling
}}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
const input = e.currentTarget.querySelector('input')
input?.focus()
}}
>
<form
onSubmit={handleSubmit}
style={{ width: "100%", height: "100%", padding: "10px" }}
style={{
width: "100%",
height: "100%",
padding: "10px",
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
}}
onClick={(e) => e.stopPropagation()}
>
<input
type="text"
value={inputUrl}
onChange={(e) => setInputUrl(e.target.value)}
placeholder="Enter URL"
placeholder="Enter URL to embed"
style={{
width: "100%",
height: "100%",
border: "none",
padding: "10px",
padding: "15px", // Increased padding for better touch target
border: "1px solid #ccc",
borderRadius: "4px",
fontSize: "16px", // Increased font size for better visibility
touchAction: 'none',
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleSubmit(e)
}
}}
onPointerDown={(e) => {
e.stopPropagation()
e.currentTarget.focus()
}}
/>
{error && (
<div style={{ color: "red", marginTop: "10px" }}>{error}</div>
@ -555,7 +584,7 @@ export class EmbedShape extends BaseBoxShapeUtil<IEmbedShape> {
}
}
// Add new method to handle all pointer interactions
// Update the pointer down handler
onPointerDown = (shape: IEmbedShape) => {
if (!shape.props.url) {
const input = document.querySelector('input')

View File

@ -5,16 +5,13 @@ import {
TldrawUiMenuSubmenu,
TLGeoShape,
TLShape,
useDefaultHelpers,
} from "tldraw"
import { TldrawUiMenuGroup } from "tldraw"
import { DefaultContextMenu, DefaultContextMenuContent } from "tldraw"
import { TLUiContextMenuProps, useEditor } from "tldraw"
import {
cameraHistory,
copyLinkToCurrentView,
lockCameraToFrame,
revertCamera,
zoomToSelection,
} from "./cameraUtils"
import { useState, useEffect } from "react"
import { saveToPdf } from "../utils/pdfUtils"
@ -22,6 +19,8 @@ import { TLFrameShape } from "tldraw"
import { searchText } from "../utils/searchUtils"
import { llm } from "../utils/llmUtils"
import { getEdge } from "@/propagators/tlgraph"
import { getCustomActions } from './overrides'
import { overrides } from './overrides'
const getAllFrames = (editor: Editor) => {
return editor
@ -35,6 +34,9 @@ const getAllFrames = (editor: Editor) => {
export function CustomContextMenu(props: TLUiContextMenuProps) {
const editor = useEditor()
const helpers = useDefaultHelpers()
const tools = overrides.tools?.(editor, {}, helpers) ?? {}
const customActions = getCustomActions(editor)
const [selectedShapes, setSelectedShapes] = useState<TLShape[]>([])
const [selectedIds, setSelectedIds] = useState<string[]>([])
@ -61,170 +63,34 @@ export function CustomContextMenu(props: TLUiContextMenuProps) {
const hasSelection = selectedIds.length > 0
const hasCameraHistory = cameraHistory.length > 0
// Check if exactly one frame is selected
const hasFrameSelected =
selectedShapes.length === 1 && selectedShapes[0].type === "frame"
//TO DO: Fix camera history for camera revert
return (
<DefaultContextMenu {...props}>
<DefaultContextMenuContent />
{/* Camera Controls Group */}
<TldrawUiMenuGroup id="camera-controls">
<TldrawUiMenuItem
id="zoom-to-selection"
label="Zoom to Selection"
icon="zoom-in"
kbd="z"
disabled={!hasSelection}
onSelect={() => zoomToSelection(editor)}
/>
<TldrawUiMenuItem
id="copy-link-to-current-view"
label="Copy Link to Current View"
icon="link"
kbd="alt+c"
onSelect={() => copyLinkToCurrentView(editor)}
/>
<TldrawUiMenuItem
id="revert-camera"
label="Revert Camera"
icon="undo"
kbd="alt+b"
disabled={!hasCameraHistory}
onSelect={() => revertCamera(editor)}
/>
<TldrawUiMenuItem
id="save-to-pdf"
label="Save Selection as PDF"
icon="file"
kbd="alt+s"
disabled={!hasSelection}
onSelect={() => saveToPdf(editor)}
/>
<TldrawUiMenuItem
id="run-llm-prompt"
label="Run LLM Prompt"
icon="file"
kbd="g"
disabled={!hasSelection}
onSelect={() => {
const selectedShape = editor.getSelectedShapes()[0];
if (!selectedShape || selectedShape.type !== 'arrow') return;
const edge = getEdge(selectedShape, editor);
if (!edge) return;
const sourceShape = editor.getShape(edge.from);
const sourceText =
sourceShape && sourceShape.type === "geo"
? (sourceShape as TLGeoShape).props.text
: "";
llm(
`Instruction: ${edge.text}
${sourceText ? `Context: ${sourceText}` : ""}`,
localStorage.getItem("openai_api_key") || "",
(partialResponse: string) => {
editor.updateShape({
id: edge.to,
type: "geo",
props: {
...(editor.getShape(edge.to) as TLGeoShape).props,
text: partialResponse
}
});
}
)
}}
/>
<TldrawUiMenuItem {...customActions.zoomToSelection} disabled={!hasSelection} />
<TldrawUiMenuItem {...customActions.copyLinkToCurrentView} />
<TldrawUiMenuItem {...customActions.revertCamera} disabled={!hasCameraHistory} />
<TldrawUiMenuItem {...customActions.lockElement} disabled={!hasSelection} />
<TldrawUiMenuItem {...customActions.unlockElement} disabled={!hasSelection} />
<TldrawUiMenuItem {...customActions.saveToPdf} disabled={!hasSelection} />
<TldrawUiMenuItem {...customActions.llm} disabled={!hasSelection} />
</TldrawUiMenuGroup>
{/* Creation Tools Group */}
<TldrawUiMenuGroup id="creation-tools">
<TldrawUiMenuItem
id="VideoChat"
label="Create Video Chat"
icon="video"
kbd="alt+v"
disabled={hasSelection}
onSelect={() => {
editor.setCurrentTool("VideoChat")
}}
/>
<TldrawUiMenuItem
id="ChatBox"
label="Create Chat Box"
icon="chat"
kbd="alt+c"
disabled={hasSelection}
onSelect={() => {
editor.setCurrentTool("ChatBox")
}}
/>
<TldrawUiMenuItem
id="Embed"
label="Create Embed"
icon="embed"
kbd="alt+e"
disabled={hasSelection}
onSelect={() => {
editor.setCurrentTool("Embed")
}}
/>
<TldrawUiMenuItem
id="Slide"
label="Create Slide"
icon="slides"
kbd="alt+s"
disabled={hasSelection}
onSelect={() => {
editor.setCurrentTool("Slide")
}}
/>
<TldrawUiMenuItem
id="MycrozineTemplate"
label="Create Mycrozine Template"
icon="rectangle"
kbd="alt+z"
disabled={hasSelection}
onSelect={() => {
editor.setCurrentTool("MycrozineTemplate")
}}
/>
<TldrawUiMenuItem
id="Markdown"
label="Create Markdown"
icon="markdown"
kbd="alt+m"
disabled={hasSelection}
onSelect={() => {
editor.setCurrentTool("Markdown")
}}
/>
<TldrawUiMenuItem
id="Prompt"
label="Create LLM Chat Prompt"
icon="prompt"
kbd="alt+p"
disabled={hasSelection}
onSelect={() => {
editor.setCurrentTool("Prompt")
}}
/>
<TldrawUiMenuItem {...tools.VideoChat} disabled={hasSelection} />
<TldrawUiMenuItem {...tools.ChatBox} disabled={hasSelection} />
<TldrawUiMenuItem {...tools.Embed} disabled={hasSelection} />
<TldrawUiMenuItem {...tools.SlideShape} disabled={hasSelection} />
<TldrawUiMenuItem {...tools.Markdown} disabled={hasSelection} />
<TldrawUiMenuItem {...tools.MycrozineTemplate} disabled={hasSelection} />
<TldrawUiMenuItem {...tools.Prompt} disabled={hasSelection} />
</TldrawUiMenuGroup>
{/* Frame Controls */}
<TldrawUiMenuGroup id="frame-controls">
<TldrawUiMenuItem
id="lock-to-frame"
label="Lock to Frame"
icon="lock"
kbd="shift+l"
disabled={!hasFrameSelected}
onSelect={() => lockCameraToFrame(editor)}
/>
</TldrawUiMenuGroup>
<TldrawUiMenuGroup id="frames-list">
<TldrawUiMenuSubmenu id="frames-dropdown" label="Shortcut to Frames">
{getAllFrames(editor).map((frame) => (

View File

@ -3,6 +3,8 @@ import { Editor, TLFrameShape, TLParentId, TLShape, TLShapeId } from "tldraw"
export const cameraHistory: { x: number; y: number; z: number }[] = []
const MAX_HISTORY = 10 // Keep last 10 camera positions
const frameObservers = new Map<string, ResizeObserver>()
// Helper function to store camera position
const storeCameraPosition = (editor: Editor) => {
const currentCamera = editor.getCamera()
@ -80,9 +82,9 @@ export const zoomToSelection = (editor: Editor) => {
const newCamera = editor.getCamera()
const url = new URL(window.location.href)
url.searchParams.set("shapeId", selectedIds[0].toString())
url.searchParams.set("x", newCamera.x.toString())
url.searchParams.set("y", newCamera.y.toString())
url.searchParams.set("zoom", newCamera.z.toString())
url.searchParams.set("x", Math.round(newCamera.x).toString())
url.searchParams.set("y", Math.round(newCamera.y).toString())
url.searchParams.set("zoom", Math.round(newCamera.z).toString())
window.history.replaceState(null, "", url.toString())
}
@ -127,22 +129,14 @@ export const copyLinkToCurrentView = async (editor: Editor) => {
try {
const baseUrl = `${window.location.origin}${window.location.pathname}`
console.log("Base URL:", baseUrl)
const url = new URL(baseUrl)
const camera = editor.getCamera()
console.log("Current camera position:", {
x: camera.x,
y: camera.y,
zoom: camera.z,
})
// Set camera parameters first
url.searchParams.set("x", camera.x.toString())
url.searchParams.set("y", camera.y.toString())
url.searchParams.set("zoom", camera.z.toString())
// Round camera values to integers
url.searchParams.set("x", Math.round(camera.x).toString())
url.searchParams.set("y", Math.round(camera.y).toString())
url.searchParams.set("zoom", Math.round(camera.z).toString())
// Add shape ID last if needed
const selectedIds = editor.getSelectedShapeIds()
if (selectedIds.length > 0) {
url.searchParams.set("shapeId", selectedIds[0].toString())
@ -152,11 +146,8 @@ export const copyLinkToCurrentView = async (editor: Editor) => {
console.log("Final URL to copy:", finalUrl)
if (navigator.clipboard && window.isSecureContext) {
console.log("Using modern clipboard API...")
await navigator.clipboard.writeText(finalUrl)
console.log("URL copied successfully using clipboard API")
} else {
console.log("Falling back to legacy clipboard method...")
const textArea = document.createElement("textarea")
textArea.value = finalUrl
document.body.appendChild(textArea)
@ -173,56 +164,130 @@ export const copyLinkToCurrentView = async (editor: Editor) => {
}
}
/** TODO: doesnt UNlock */
export const lockCameraToFrame = async (editor: Editor) => {
// Add this function to create lock indicators
const createLockIndicator = (editor: Editor, shape: TLShape) => {
const lockIndicator = document.createElement('div')
lockIndicator.id = `lock-indicator-${shape.id}`
lockIndicator.className = 'lock-indicator'
lockIndicator.innerHTML = '🔒'
// Set styles to position at top-right of shape
lockIndicator.style.position = 'absolute'
lockIndicator.style.right = '3px'
lockIndicator.style.top = '3px'
lockIndicator.style.pointerEvents = 'all'
lockIndicator.style.zIndex = '99999'
lockIndicator.style.background = 'white'
lockIndicator.style.border = '1px solid #ddd'
lockIndicator.style.borderRadius = '4px'
lockIndicator.style.padding = '4px'
lockIndicator.style.cursor = 'pointer'
lockIndicator.style.boxShadow = '0 1px 3px rgba(0,0,0,0.12)'
lockIndicator.style.fontSize = '12px'
lockIndicator.style.lineHeight = '1'
lockIndicator.style.display = 'flex'
lockIndicator.style.alignItems = 'center'
lockIndicator.style.justifyContent = 'center'
lockIndicator.style.width = '20px'
lockIndicator.style.height = '20px'
lockIndicator.style.userSelect = 'none'
// Add hover effect
lockIndicator.onmouseenter = () => {
lockIndicator.style.backgroundColor = '#f0f0f0'
}
lockIndicator.onmouseleave = () => {
lockIndicator.style.backgroundColor = 'white'
}
// Add tooltip and click handlers with stopPropagation
lockIndicator.title = 'Unlock shape'
lockIndicator.addEventListener('click', (e) => {
e.stopPropagation()
e.preventDefault()
unlockElement(editor, shape.id)
}, true)
lockIndicator.addEventListener('mousedown', (e) => {
e.stopPropagation()
e.preventDefault()
}, true)
lockIndicator.addEventListener('pointerdown', (e) => {
e.stopPropagation()
e.preventDefault()
}, true)
const shapeElement = document.querySelector(`[data-shape-id="${shape.id}"]`)
if (shapeElement) {
shapeElement.appendChild(lockIndicator)
}
}
// Modify lockElement to use the new function
export const lockElement = async (editor: Editor) => {
const selectedShapes = editor.getSelectedShapes()
if (selectedShapes.length === 0) return
const selectedShape = selectedShapes[0]
const isFrame = selectedShape.type === "frame"
const bounds = editor.getShapePageBounds(selectedShape)
if (!isFrame || !bounds) return
try {
const baseUrl = `${window.location.origin}${window.location.pathname}`
const url = new URL(baseUrl)
// Calculate zoom level to fit the frame (for URL only)
const viewportPageBounds = editor.getViewportPageBounds()
const targetZoom = Math.min(
viewportPageBounds.width / bounds.width,
viewportPageBounds.height / bounds.height,
1, // Cap at 1x zoom
)
// Set URL parameters without affecting the current view
url.searchParams.set("x", Math.round(bounds.x).toString())
url.searchParams.set("y", Math.round(bounds.y).toString())
url.searchParams.set(
"zoom",
(Math.round(targetZoom * 100) / 100).toString(),
)
url.searchParams.set("frameId", selectedShape.id)
url.searchParams.set("isLocked", "true")
const finalUrl = url.toString()
// Copy URL to clipboard without modifying the current view
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(finalUrl)
} else {
const textArea = document.createElement("textarea")
textArea.value = finalUrl
document.body.appendChild(textArea)
textArea.select()
document.execCommand("copy")
document.body.removeChild(textArea)
}
selectedShapes.forEach(shape => {
editor.updateShape({
id: shape.id,
type: shape.type,
isLocked: true,
meta: {
...shape.meta,
isLocked: true,
canInteract: true, // Allow interactions
canMove: false, // Prevent moving
canResize: false, // Prevent resizing
canEdit: true, // Allow text editing
canUpdateProps: true // Allow updating props (for prompt inputs/outputs)
//TO DO: FIX TEXT INPUT ON LOCKED ELEMENTS (e.g. prompt shape) AND ATTACH TO SCREEN EDGE
}
})
createLockIndicator(editor, shape)
})
} catch (error) {
console.error("Failed to copy frame link:", error)
alert("Failed to copy frame link. Please check clipboard permissions.")
console.error("Failed to lock elements:", error)
}
}
export const unlockElement = (editor: Editor, shapeId: string) => {
const indicator = document.getElementById(`lock-indicator-${shapeId}`)
if (indicator) {
indicator.remove()
}
const shape = editor.getShape(shapeId as TLShapeId)
if (shape) {
editor.updateShape({
id: shapeId as TLShapeId,
type: shape.type,
isLocked: false,
meta: {
...shape.meta,
isLocked: false,
canInteract: true,
canMove: true,
canResize: true,
canEdit: true,
canUpdateProps: true
}
})
}
}
// Initialize lock indicators based on stored state
export const initLockIndicators = (editor: Editor) => {
editor.getCurrentPageShapes().forEach(shape => {
if (shape.isLocked || shape.meta?.isLocked) {
createLockIndicator(editor, shape)
}
})
}
export const setInitialCameraFromUrl = (editor: Editor) => {
const url = new URL(window.location.href)
const x = url.searchParams.get("x")
@ -230,7 +295,6 @@ export const setInitialCameraFromUrl = (editor: Editor) => {
const zoom = url.searchParams.get("zoom")
const shapeId = url.searchParams.get("shapeId")
const frameId = url.searchParams.get("frameId")
//const isLocked = url.searchParams.get("isLocked") === "true"
console.log('Setting initial camera from URL:', { x, y, zoom, shapeId, frameId })
@ -238,9 +302,9 @@ export const setInitialCameraFromUrl = (editor: Editor) => {
editor.stopCameraAnimation()
editor.setCamera(
{
x: parseFloat(x),
y: parseFloat(y),
z: parseFloat(zoom)
x: Math.round(parseFloat(x)),
y: Math.round(parseFloat(y)),
z: Math.round(parseFloat(zoom))
},
{ animation: { duration: 0 } }
)
@ -266,10 +330,6 @@ export const setInitialCameraFromUrl = (editor: Editor) => {
}
}
}
// if (isLocked) {
// editor.setCameraOptions({ isLocked: true })
// }
}
export const zoomToFrame = (editor: Editor, frameId: string) => {
@ -288,3 +348,17 @@ export const copyFrameLink = (_editor: Editor, frameId: string) => {
url.searchParams.set("frameId", frameId)
navigator.clipboard.writeText(url.toString())
}
// Initialize lock indicators and watch for changes
export const watchForLockedShapes = (editor: Editor) => {
editor.on('change', () => {
editor.getCurrentPageShapes().forEach(shape => {
const hasIndicator = document.getElementById(`lock-indicator-${shape.id}`)
if (shape.isLocked && !hasIndicator) {
createLockIndicator(editor, shape)
} else if (!shape.isLocked && hasIndicator) {
hasIndicator.remove()
}
})
})
}

View File

@ -1,3 +1,4 @@
import { Editor, useDefaultHelpers } from "tldraw"
import {
shapeIdValidator,
TLArrowShape,
@ -7,8 +8,9 @@ import {
import {
cameraHistory,
copyLinkToCurrentView,
lockCameraToFrame,
lockElement,
revertCamera,
unlockElement,
zoomToSelection,
} from "./cameraUtils"
import { saveToPdf } from "../utils/pdfUtils"
@ -158,8 +160,7 @@ export const overrides: TLUiOverrides = {
}
},
actions(editor, actions) {
return {
...actions,
const customActions = {
"zoom-in": {
...actions["zoom-in"],
kbd: "ctrl+up",
@ -183,9 +184,7 @@ export const overrides: TLUiOverrides = {
id: "copy-link-to-current-view",
label: "Copy Link to Current View",
kbd: "alt+c",
onSelect: () => {
copyLinkToCurrentView(editor)
},
onSelect: () => copyLinkToCurrentView(editor),
readonlyOk: true,
},
revertCamera: {
@ -199,11 +198,26 @@ export const overrides: TLUiOverrides = {
},
readonlyOk: true,
},
lockToFrame: {
id: "lock-to-frame",
label: "Lock to Frame",
lockElement: {
id: "lock-element",
label: "Lock Element",
kbd: "shift+l",
onSelect: () => lockCameraToFrame(editor),
onSelect: () => {
const selectedShapes = editor.getSelectedShapes()
if (selectedShapes.length > 0) {
lockElement(editor)
}
},
readonlyOk: true,
},
unlockElement: {
id: "unlock-element",
label: "Unlock Element",
onSelect: () => {
if (editor.getSelectedShapeIds().length > 0) {
unlockElement(editor, editor.getSelectedShapeIds()[0])
}
},
},
saveToPdf: {
id: "save-to-pdf",
@ -288,35 +302,6 @@ export const overrides: TLUiOverrides = {
}
},
},
//TODO: MAKE THIS WORK, ADD USER PERMISSIONING TO JOIN BROADCAST?
startBroadcast: {
id: "start-broadcast",
label: "Start Broadcasting",
kbd: "alt+b",
readonlyOk: true,
onSelect: () => {
editor.markHistoryStoppingPoint('start-broadcast')
editor.updateInstanceState({ isBroadcasting: true })
const url = new URL(window.location.href)
url.searchParams.set("followId", editor.user.getId())
window.history.replaceState(null, "", url.toString())
},
},
stopBroadcast: {
id: "stop-broadcast",
label: "Stop Broadcasting",
kbd: "alt+shift+b",
readonlyOk: true,
onSelect: () => {
editor.updateInstanceState({ isBroadcasting: false })
editor.stopFollowingUser()
// Remove followId from URL
const url = new URL(window.location.href)
url.searchParams.delete("followId")
window.history.replaceState(null, "", url.toString())
},
},
searchShapes: {
id: "search-shapes",
label: "Search Shapes",
@ -326,7 +311,7 @@ export const overrides: TLUiOverrides = {
},
llm: {
id: "llm",
label: "LLM",
label: "Run LLM Prompt",
kbd: "g",
readonlyOk: true,
onSelect: () => {
@ -424,5 +409,16 @@ export const overrides: TLUiOverrides = {
},
},
}
return {
...actions,
...customActions,
}
},
}
// Export actions for use in context menu
export const getCustomActions = (editor: Editor) => {
const helpers = useDefaultHelpers()
return overrides.actions?.(editor, {}, helpers) ?? {}
}