cleanup tools/menu/actions

This commit is contained in:
Jeff Emmett 2024-12-07 21:16:44 -05:00
parent 94bec533c4
commit 923f61ac9e
3 changed files with 148 additions and 327 deletions

View File

@ -15,45 +15,22 @@ import { Board } from './components/Board';
import { Inbox } from './components/Inbox'; import { Inbox } from './components/Inbox';
import { Books } from './components/Books'; import { Books } from './components/Books';
import { import {
BindingUtil,
Editor, Editor,
IndexKey,
TLBaseBinding,
TLBaseShape,
Tldraw, Tldraw,
TLShapeId, TLShapeId,
} from 'tldraw'; } from 'tldraw';
import { components, uiOverrides } from './ui-overrides'; import { components, overrides } from './ui-overrides'
import { ChatBoxShape } from './shapes/ChatBoxShapeUtil'; import { ChatBoxShape } from './shapes/ChatBoxShapeUtil';
import { VideoChatShape } from './shapes/VideoChatShapeUtil'; import { VideoChatShape } from './shapes/VideoChatShapeUtil';
import { ChatBoxTool } from './tools/ChatBoxTool'; import { ChatBoxTool } from './tools/ChatBoxTool';
import { VideoChatTool } from './tools/VideoChatTool'; import { VideoChatTool } from './tools/VideoChatTool';
import { EmbedTool } from './tools/EmbedTool';
import { EmbedShape } from './shapes/EmbedShapeUtil';
inject(); inject();
// The container shapes that can contain element shapes const customShapeUtils = [ChatBoxShape, VideoChatShape, EmbedShape];
const CONTAINER_PADDING = 24; const customTools = [ChatBoxTool, VideoChatTool, EmbedTool];
type ContainerShape = TLBaseShape<'element', { height: number; width: number }>;
// ... existing code for ContainerShapeUtil ...
// The element shapes that can be placed inside the container shapes
type ElementShape = TLBaseShape<'element', { color: string }>;
// ... existing code for ElementShapeUtil ...
// The binding between the element shapes and the container shapes
type LayoutBinding = TLBaseBinding<
'layout',
{
index: IndexKey;
placeholder: boolean;
}
>;
const customShapeUtils = [ChatBoxShape, VideoChatShape];
const customTools = [ChatBoxTool, VideoChatTool];
// [2] // [2]
export default function InteractiveShapeExample() { export default function InteractiveShapeExample() {
@ -62,7 +39,7 @@ export default function InteractiveShapeExample() {
<Tldraw <Tldraw
shapeUtils={customShapeUtils} shapeUtils={customShapeUtils}
tools={customTools} tools={customTools}
overrides={uiOverrides} overrides={overrides}
components={components} components={components}
onMount={(editor) => { onMount={(editor) => {
handleInitialShapeLoad(editor); handleInitialShapeLoad(editor);
@ -134,8 +111,6 @@ function Home() {
const shapes = createShapes(elementsInfo) const shapes = createShapes(elementsInfo)
const [isEditorMounted, setIsEditorMounted] = useState(false); const [isEditorMounted, setIsEditorMounted] = useState(false);
//console.log("THIS WORKS SO FAR")
useEffect(() => { useEffect(() => {
const handleEditorDidMount = () => { const handleEditorDidMount = () => {
setIsEditorMounted(true); setIsEditorMounted(true);
@ -149,10 +124,12 @@ function Home() {
}, []); }, []);
return ( return (
<><Toggle /> <>
<Toggle />
<div style={{ zIndex: 999999 }} className={`${isCanvasEnabled && isEditorMounted ? 'transparent' : ''}`}> <div style={{ zIndex: 999999 }} className={`${isCanvasEnabled && isEditorMounted ? 'transparent' : ''}`}>
{<Default />} {<Default />}
</div> </div>
{isCanvasEnabled && elementsInfo.length > 0 ? <Canvas shapes={shapes} /> : null}</> {isCanvasEnabled && elementsInfo.length > 0 ? <Canvas shapes={shapes} /> : null}
</>
) )
} }

View File

@ -4,11 +4,8 @@ import {
AssetRecordType, AssetRecordType,
getHashForString, getHashForString,
TLBookmarkAsset, TLBookmarkAsset,
TLRecord,
Tldraw, Tldraw,
Editor, Editor,
TLFrameShape,
TLUiEventSource,
} from 'tldraw' } from 'tldraw'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { ChatBoxTool } from '@/tools/ChatBoxTool' import { ChatBoxTool } from '@/tools/ChatBoxTool'
@ -16,16 +13,12 @@ import { ChatBoxShape } from '@/shapes/ChatBoxShapeUtil'
import { VideoChatTool } from '@/tools/VideoChatTool' import { VideoChatTool } from '@/tools/VideoChatTool'
import { VideoChatShape } from '@/shapes/VideoChatShapeUtil' import { VideoChatShape } from '@/shapes/VideoChatShapeUtil'
import { multiplayerAssetStore } from '../client/multiplayerAssetStore' import { multiplayerAssetStore } from '../client/multiplayerAssetStore'
import { customSchema } from '../../worker/TldrawDurableObject'
import { EmbedShape } from '@/shapes/EmbedShapeUtil' import { EmbedShape } from '@/shapes/EmbedShapeUtil'
import { EmbedTool } from '@/tools/EmbedTool' import { EmbedTool } from '@/tools/EmbedTool'
import { defaultShapeUtils, defaultBindingUtils } from 'tldraw' import { defaultShapeUtils, defaultBindingUtils } from 'tldraw'
import React, { useState, useEffect, useCallback } from 'react'; import { useState } from 'react';
import { ChatBox } from '@/shapes/ChatBoxShapeUtil'; import { components, overrides } from '@/ui-overrides'
import { components, uiOverrides } from '@/ui-overrides'
import { useCameraControls } from '@/hooks/useCameraControls'
import { zoomToSelection } from '../ui-overrides'
// Default to production URL if env var isn't available // Default to production URL if env var isn't available
export const WORKER_URL = 'https://jeffemmett-canvas.jeffemmett.workers.dev'; export const WORKER_URL = 'https://jeffemmett-canvas.jeffemmett.workers.dev';
@ -47,7 +40,6 @@ export function Board() {
const store = useSync(storeConfig); const store = useSync(storeConfig);
const [editor, setEditor] = useState<Editor | null>(null) const [editor, setEditor] = useState<Editor | null>(null)
const { zoomToFrame, copyFrameLink, copyLocationLink, revertCamera } = useCameraControls(editor)
return ( return (
<div style={{ position: 'fixed', inset: 0 }}> <div style={{ position: 'fixed', inset: 0 }}>
@ -56,81 +48,7 @@ export function Board() {
shapeUtils={shapeUtils} shapeUtils={shapeUtils}
tools={tools} tools={tools}
components={components} components={components}
overrides={{ overrides={overrides}
tools: (editor, baseTools) => ({
...baseTools,
ChatBox: {
id: 'ChatBox',
icon: 'chat',
label: 'Chat',
kbd: 'c',
readonlyOk: true,
onSelect: () => {
editor.setCurrentTool('ChatBox')
},
},
VideoChat: {
id: 'VideoChat',
icon: 'video',
label: 'Video Chat',
kbd: 'v',
readonlyOk: true,
onSelect: () => {
editor.setCurrentTool('VideoChat')
},
},
Embed: {
id: 'Embed',
icon: 'embed',
label: 'Embed',
kbd: 'e',
readonlyOk: true,
onSelect: () => {
editor.setCurrentTool('Embed')
},
},
}),
actions: (editor, actions) => ({
...actions,
'zoomToShape': {
id: 'zoom-to-shape',
label: 'Zoom to Selection',
kbd: 'z',
onSelect: () => {
if (editor.getSelectedShapeIds().length > 0) {
zoomToSelection(editor);
editor.setCurrentTool('select');
}
},
readonlyOk: true,
},
'copyLinkToCurrentView': {
id: 'copy-link-to-current-view',
label: 'Copy Link to Current View',
kbd: 'c',
onSelect: () => {
const camera = editor.getCamera();
const url = new URL(window.location.href);
url.searchParams.set('x', camera.x.toString());
url.searchParams.set('y', camera.y.toString());
url.searchParams.set('zoom', camera.z.toString());
navigator.clipboard.writeText(url.toString());
editor.setCurrentTool('select');
},
readonlyOk: true,
},
'revertCamera': {
id: 'revert-camera',
label: 'Revert Camera',
kbd: 'b',
onSelect: () => {
revertCamera();
editor.setCurrentTool('select');
},
readonlyOk: true,
},
}),
}}
onMount={(editor) => { onMount={(editor) => {
setEditor(editor) setEditor(editor)
editor.registerExternalAssetHandler('url', unfurlBookmarkUrl) editor.registerExternalAssetHandler('url', unfurlBookmarkUrl)

View File

@ -6,12 +6,10 @@ import {
TldrawUiMenuItem, TldrawUiMenuItem,
useEditor, useEditor,
useTools, useTools,
TLShapeId,
DefaultContextMenu, DefaultContextMenu,
DefaultContextMenuContent, DefaultContextMenuContent,
TLUiContextMenuProps, TLUiContextMenuProps,
TldrawUiMenuGroup, TldrawUiMenuGroup,
TLShape,
} from 'tldraw' } from 'tldraw'
import { CustomMainMenu } from './components/CustomMainMenu' import { CustomMainMenu } from './components/CustomMainMenu'
import { Editor } from 'tldraw' import { Editor } from 'tldraw'
@ -37,55 +35,6 @@ const storeCameraPosition = (editor: Editor) => {
} }
}; };
const copyFrameLink = async (editor: Editor, frameId: string) => {
console.log('Starting copyFrameLink with frameId:', frameId);
if (!editor.store.getSnapshot()) {
console.warn('Store not ready');
return;
}
try {
const baseUrl = `${window.location.origin}${window.location.pathname}`;
console.log('Base URL:', baseUrl);
const url = new URL(baseUrl);
url.searchParams.set('frameId', frameId);
const frame = editor.getShape(frameId as TLShapeId);
console.log('Found frame:', frame);
if (frame) {
const camera = editor.getCamera();
console.log('Camera position:', { x: camera.x, y: camera.y, zoom: camera.z });
url.searchParams.set('x', camera.x.toString());
url.searchParams.set('y', camera.y.toString());
url.searchParams.set('zoom', camera.z.toString());
}
const finalUrl = url.toString();
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);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
console.log('URL copied successfully using fallback method');
}
} catch (error) {
console.error('Failed to copy to clipboard:', error);
alert('Failed to copy link. Please check clipboard permissions.');
}
};
export const zoomToSelection = (editor: Editor) => { export const zoomToSelection = (editor: Editor) => {
// Store camera position before zooming // Store camera position before zooming
@ -154,7 +103,7 @@ export const zoomToSelection = (editor: Editor) => {
const copyLinkToCurrentView = async (editor: Editor) => { const copyLinkToCurrentView = async (editor: Editor) => {
console.log('Starting copyLinkToCurrentView'); console.log('Starting copyLinkToCurrentView');
if (!editor.store.getSnapshot()) { if (!editor.store.serialize()) {
console.warn('Store not ready'); console.warn('Store not ready');
return; return;
} }
@ -184,10 +133,16 @@ const copyLinkToCurrentView = async (editor: Editor) => {
const textArea = document.createElement('textarea'); const textArea = document.createElement('textarea');
textArea.value = finalUrl; textArea.value = finalUrl;
document.body.appendChild(textArea); document.body.appendChild(textArea);
textArea.select(); try {
document.execCommand('copy'); await navigator.clipboard.writeText(textArea.value);
console.log('URL copied successfully');
} catch (err) {
// Fallback for older browsers
textArea.select();
document.execCommand('copy');
console.log('URL copied using fallback method');
}
document.body.removeChild(textArea); document.body.removeChild(textArea);
console.log('URL copied successfully using fallback method');
} }
} catch (error) { } catch (error) {
console.error('Failed to copy to clipboard:', error); console.error('Failed to copy to clipboard:', error);
@ -226,7 +181,8 @@ const revertCamera = (editor: Editor) => {
} }
}; };
export const uiOverrides: TLUiOverrides = { // Export a function that creates the uiOverrides
export const overrides: TLUiOverrides = ({
tools(editor, tools) { tools(editor, tools) {
return { return {
...tools, ...tools,
@ -257,74 +213,69 @@ export const uiOverrides: TLUiOverrides = {
} }
}, },
actions(editor, actions) { actions(editor, actions) {
actions['copyFrameLink'] = { return {
id: 'copy-frame-link', ...actions,
label: 'Copy Frame Link', 'zoomToSelection': {
onSelect: () => { id: 'zoom-to-selection',
const shape = editor.getSelectedShapes()[0] label: 'Zoom to Selection',
if (shape && shape.type === 'frame') { kbd: 'z',
copyFrameLink(editor, shape.id) onSelect: () => {
if (editor.getSelectedShapeIds().length > 0) {
zoomToSelection(editor);
}
},
readonlyOk: true,
},
'copyLinkToCurrentView': {
id: 'copy-link-to-current-view',
label: 'Copy Link to Current View',
kbd: 's',
onSelect: () => {
copyLinkToCurrentView(editor);
},
readonlyOk: true,
},
'revertCamera': {
id: 'revert-camera',
label: 'Revert Camera',
kbd: 'b',
onSelect: () => {
if (cameraHistory.length > 0) {
revertCamera(editor);
}
},
readonlyOk: true,
},
'lockToFrame': {
id: 'lock-to-frame',
label: 'Lock to Frame',
kbd: 'l',
onSelect: () => {
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
editor.zoomToBounds(bounds, {
animation: { duration: 300 },
targetZoom: 1
})
editor.updateInstanceState({
meta: { ...editor.getInstanceState().meta, lockedFrameId: selectedShape.id }
})
} }
}, }
readonlyOk: true,
} }
actions['zoomToFrame'] = {
id: 'zoom-to-frame',
label: 'Zoom to Frame',
onSelect: () => {
const shape = editor.getSelectedShapes()[0]
if (shape && shape.type === 'frame') {
zoomToSelection(editor)
}
},
readonlyOk: true,
}
actions['copyLinkToCurrentView'] = {
id: 'copy-link-to-current-view',
label: 'Copy Link to Current View',
kbd: 'c',
onSelect: () => {
console.log('Creating link to current view');
copyLinkToCurrentView(editor);
},
readonlyOk: true,
}
actions['zoomToShape'] = {
id: 'zoom-to-shape',
label: 'Zoom to Selection',
kbd: 'z',
onSelect: () => {
if (editor.getSelectedShapeIds().length > 0) {
console.log('Zooming to selection');
zoomToSelection(editor);
}
},
readonlyOk: true,
}
actions['revertCamera'] = {
id: 'revert-camera',
label: 'Revert Camera',
kbd: 'b',
onSelect: () => {
if (cameraHistory.length > 0) {
revertCamera(editor);
}
},
readonlyOk: true,
}
return actions
}, },
} })
export const components: TLComponents = { export const components: TLComponents = {
Toolbar: function Toolbar() { Toolbar: function Toolbar() {
const editor = useEditor() const editor = useEditor()
const tools = useTools() const tools = useTools()
return ( return (
<DefaultToolbar> <DefaultToolbar>
<DefaultToolbarContent /> <DefaultToolbarContent />
@ -356,7 +307,7 @@ export const components: TLComponents = {
) )
}, },
MainMenu: CustomMainMenu, MainMenu: CustomMainMenu,
ContextMenu: function CustomContextMenu({ ...rest }) { ContextMenu: function CustomContextMenu(props: TLUiContextMenuProps) {
const editor = useEditor() const editor = useEditor()
const hasSelection = editor.getSelectedShapeIds().length > 0 const hasSelection = editor.getSelectedShapeIds().length > 0
const hasCameraHistory = cameraHistory.length > 0 const hasCameraHistory = cameraHistory.length > 0
@ -364,101 +315,76 @@ export const components: TLComponents = {
const isFrame = selectedShape?.type === 'frame' const isFrame = selectedShape?.type === 'frame'
return ( return (
<DefaultContextMenu {...rest}> <DefaultContextMenu {...props}>
<DefaultContextMenuContent /> <DefaultContextMenuContent />
{/* Camera Controls */} {/* Camera Controls Group */}
<TldrawUiMenuItem <TldrawUiMenuGroup id="camera-controls">
id="zoom-to-selection" <TldrawUiMenuItem
label="Zoom to Selection" id="zoom-to-selection"
icon="zoom-in" label="Zoom to Selection"
kbd="z" icon="zoom-in"
disabled={!hasSelection} kbd="z"
onSelect={() => zoomToSelection(editor)} disabled={!hasSelection}
/> onSelect={() => zoomToSelection(editor)}
<TldrawUiMenuItem />
id="copy-link-to-current-view" <TldrawUiMenuItem
label="Copy Link to Current View" id="copy-link-to-current-view"
icon="link" label="Copy Link to Current View"
kbd="s" icon="link"
onSelect={() => copyLinkToCurrentView(editor)} kbd="s"
/> onSelect={() => copyLinkToCurrentView(editor)}
<TldrawUiMenuItem />
id="revert-camera" <TldrawUiMenuItem
label="Revert Camera" id="revert-camera"
icon="undo" label="Revert Camera"
kbd="b" icon="undo"
onSelect={() => { kbd="b"
if (hasCameraHistory) { disabled={!hasCameraHistory}
revertCamera(editor); onSelect={() => revertCamera(editor)}
} />
}} </TldrawUiMenuGroup>
/>
{/* Shape Creation Tools */} {/* Creation Tools Group */}
<TldrawUiMenuItem <TldrawUiMenuGroup id="creation-tools">
id="video-chat" <TldrawUiMenuItem
label="Create Video Chat" id="video-chat"
icon="video" label="Create Video Chat"
kbd="v" icon="video"
onSelect={() => { kbd="v"
editor.setCurrentTool('VideoChat'); onSelect={() => { editor.setCurrentTool('VideoChat'); }}
}} />
/> <TldrawUiMenuItem
<TldrawUiMenuItem id="chat-box"
id="chat-box" label="Create Chat Box"
label="Create Chat Box" icon="chat"
icon="chat" kbd="c"
kbd="c" onSelect={() => { editor.setCurrentTool('ChatBox'); }}
onSelect={() => { />
editor.setCurrentTool('ChatBox'); <TldrawUiMenuItem
}} id="embed"
/> label="Create Embed"
<TldrawUiMenuItem icon="embed"
id="embed" kbd="e"
label="Create Embed" onSelect={() => { editor.setCurrentTool('Embed'); }}
icon="embed" />
kbd="e" </TldrawUiMenuGroup>
onSelect={() => {
editor.setCurrentTool('Embed'); {/* Frame Controls */}
}} {isFrame && (
/> <TldrawUiMenuGroup id="frame-controls">
<TldrawUiMenuItem
id="lock-to-frame"
label="Lock to Frame"
icon="lock"
kbd="l"
onSelect={() => {
console.warn('lock to frame NOT IMPLEMENTED')
}}
/>
</TldrawUiMenuGroup>
)}
</DefaultContextMenu> </DefaultContextMenu>
) )
},
}
const handleInitialShapeLoad = (editor: Editor) => {
const url = new URL(window.location.href);
// Check for both shapeId and legacy frameId (for backwards compatibility)
const shapeId = url.searchParams.get('shapeId') || url.searchParams.get('frameId');
const x = url.searchParams.get('x');
const y = url.searchParams.get('y');
const zoom = url.searchParams.get('zoom');
if (shapeId) {
console.log('Found shapeId in URL:', shapeId);
const shape = editor.getShape(shapeId as TLShapeId);
if (shape) {
console.log('Found shape:', shape);
if (x && y && zoom) {
console.log('Setting camera to:', { x, y, zoom });
editor.setCamera({
x: parseFloat(x),
y: parseFloat(y),
z: parseFloat(zoom)
});
} else {
console.log('Zooming to shape bounds');
editor.zoomToBounds(editor.getShapeGeometry(shape).bounds, {
targetZoom: 1,
//padding: 32
});
}
} else {
console.warn('Shape not found:', shapeId);
}
} }
}; }