1- import React , {
2- useCallback ,
3- useEffect ,
4- useMemo ,
5- useRef ,
6- useState ,
7- } from "react" ;
8- import ResizableDrawer from "~/components/ResizableDrawer" ;
9- import renderOverlay from "roamjs-components/util/renderOverlay" ;
1+ import React , { useCallback , useEffect , useMemo , useState } from "react" ;
102import {
113 Button ,
12- Card ,
134 Collapse ,
5+ Icon ,
146 Menu ,
157 MenuItem ,
168 NonIdealState ,
@@ -22,13 +14,11 @@ import {
2214 Tag ,
2315 Tooltip ,
2416} from "@blueprintjs/core" ;
17+ import { Editor , useEditor , TLShapeId } from "tldraw" ;
2518import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid" ;
26- import getDiscourseNodes from "~/utils/getDiscourseNodes" ;
2719import getCurrentPageUid from "roamjs-components/dom/getCurrentPageUid" ;
28- import getBlockProps from "~/utils/getBlockProps" ;
29- import { TLBaseShape } from "tldraw" ;
20+ import getDiscourseNodes from "~/utils/getDiscourseNodes" ;
3021import { DiscourseNodeShape } from "./DiscourseNodeUtil" ;
31- import { render as renderToast } from "roamjs-components/components/Toast" ;
3222import { formatHexColor } from "~/components/settings/DiscourseNodeCanvasSettings" ;
3323
3424export type GroupedShapes = Record < string , DiscourseNodeShape [ ] > ;
@@ -42,36 +32,17 @@ type NodeGroup = {
4232 isDuplicate : boolean ;
4333} ;
4434
45- // Module-level ref holder set by the provider
46- // This allows openCanvasDrawer to be called from non-React contexts
47- // (command palette, context menus, etc.)
48- let drawerUnmountRef : React . MutableRefObject < ( ( ) => void ) | null > | null = null ;
49-
50- export const CanvasDrawerProvider = ( {
51- children,
52- } : {
53- children : React . ReactNode ;
54- } ) => {
55- const unmountRef = useRef < ( ( ) => void ) | null > ( null ) ;
56-
57- useEffect ( ( ) => {
58- drawerUnmountRef = unmountRef ;
59-
60- return ( ) => {
61- if ( unmountRef . current ) {
62- unmountRef . current ( ) ;
63- unmountRef . current = null ;
64- }
65- drawerUnmountRef = null ;
66- } ;
67- } , [ ] ) ;
68-
69- return < > { children } </ > ;
35+ type Props = {
36+ groupedShapes : GroupedShapes ;
37+ pageUid : string ;
38+ editor : Editor ;
7039} ;
7140
72- type Props = { groupedShapes : GroupedShapes ; pageUid : string } ;
73-
74- const CanvasDrawerContent = ( { groupedShapes, pageUid } : Props ) => {
41+ export const CanvasDrawerContent = ( {
42+ groupedShapes,
43+ pageUid,
44+ editor,
45+ } : Props ) => {
7546 const [ openSections , setOpenSections ] = useState < Record < string , boolean > > ( { } ) ;
7647 const [ activeShapeId , setActiveShapeId ] = useState < string | null > ( null ) ;
7748 const [ filterType , setFilterType ] = useState ( "All" ) ;
@@ -172,13 +143,19 @@ const CanvasDrawerContent = ({ groupedShapes, pageUid }: Props) => {
172143 } ) ) ;
173144 } , [ ] ) ;
174145
175- const moveCameraToShape = useCallback ( ( shapeId : string ) => {
176- document . dispatchEvent (
177- new CustomEvent ( "roamjs:query-builder:action" , {
178- detail : { action : "move-camera-to-shape" , shapeId } ,
179- } ) ,
180- ) ;
181- } , [ ] ) ;
146+ const moveCameraToShape = useCallback (
147+ ( shapeId : string ) => {
148+ const shape = editor . getShape ( shapeId as TLShapeId ) ;
149+ if ( ! shape ) {
150+ return ;
151+ }
152+ const x = shape . x || 0 ;
153+ const y = shape . y || 0 ;
154+ editor . centerOnPoint ( { x, y } , { animation : { duration : 200 } } ) ;
155+ editor . select ( shapeId as TLShapeId ) ;
156+ } ,
157+ [ editor ] ,
158+ ) ;
182159
183160 const handleShapeSelection = useCallback (
184161 ( shape : DiscourseNodeShape ) => {
@@ -328,8 +305,8 @@ const CanvasDrawerContent = ({ groupedShapes, pageUid }: Props) => {
328305 ) ;
329306
330307 return (
331- < div className = "space-y-4 " >
332- < Card elevation = { 1 } className = "space-y-3" >
308+ < div className = "flex h-full flex-col gap-3 " >
309+ < div className = "flex-shrink-0 space-y-3" >
333310 < Tabs
334311 id = "canvas-drawer-tabs"
335312 selectedTabId = { activeTabId }
@@ -377,7 +354,7 @@ const CanvasDrawerContent = ({ groupedShapes, pageUid }: Props) => {
377354 </ Tag >
378355 ) }
379356 </ div >
380- </ Card >
357+ </ div >
381358
382359 { ! visibleGroups . length ? (
383360 < NonIdealState
@@ -401,83 +378,117 @@ const CanvasDrawerContent = ({ groupedShapes, pageUid }: Props) => {
401378 }
402379 />
403380 ) : (
404- < Card elevation = { 1 } className = "divide-y divide-gray-300" >
381+ < div className = "min-h-0 flex-1 divide-y divide-gray-300 overflow-y-auto overflow-x-hidden " >
405382 { visibleGroups . map ( ( group ) => renderListView ( group ) ) }
406- </ Card >
383+ </ div >
407384 ) }
408385 </ div >
409386 ) ;
410387} ;
411388
412- const CanvasDrawer = ( {
413- onClose,
414- unmountRef,
415- ...props
416- } : {
417- onClose : ( ) => void ;
418- unmountRef : React . MutableRefObject < ( ( ) => void ) | null > ;
419- } & Props ) => {
420- const handleClose = ( ) => {
421- unmountRef . current = null ;
422- onClose ( ) ;
423- } ;
389+ export const CanvasDrawerPanel = ( ) => {
390+ const editor = useEditor ( ) ;
391+ const toggleDrawer = useCallback ( ( ) => {
392+ setIsOpen ( ( prev ) => ! prev ) ;
393+ } , [ ] ) ;
394+ const [ isOpen , setIsOpen ] = useState ( false ) ;
395+ const pageUid = getCurrentPageUid ( ) ;
396+ const [ groupedShapes , setGroupedShapes ] = useState < GroupedShapes > ( { } ) ;
424397
425- return (
426- < ResizableDrawer onClose = { handleClose } title = { "Canvas Drawer" } >
427- < CanvasDrawerContent { ...props } />
428- </ ResizableDrawer >
429- ) ;
430- } ;
398+ useEffect ( ( ) => {
399+ const updateGroupedShapes = ( ) => {
400+ const allRecords = editor . store . allRecords ( ) ;
401+ const shapes = allRecords . filter ( ( record ) => {
402+ if ( record . typeName !== "shape" ) return false ;
403+ const shape = record as DiscourseNodeShape ;
404+ return ! ! shape . props ?. uid ;
405+ } ) as DiscourseNodeShape [ ] ;
406+
407+ const grouped = shapes . reduce ( ( acc : GroupedShapes , shape ) => {
408+ const uid = shape . props . uid ;
409+ if ( ! acc [ uid ] ) acc [ uid ] = [ ] ;
410+ acc [ uid ] . push ( shape ) ;
411+ return acc ;
412+ } , { } ) ;
413+
414+ setGroupedShapes ( grouped ) ;
415+ } ;
416+
417+ updateGroupedShapes ( ) ;
431418
432- export const openCanvasDrawer = ( ) : void => {
433- if ( ! drawerUnmountRef ) {
434- renderToast ( {
435- id : "canvas-drawer-not-found" ,
436- content :
437- "Unable to open Canvas Drawer. Please load canvas in main window first." ,
438- intent : "warning" ,
419+ const unsubscribe = editor . store . listen ( ( ) => {
420+ updateGroupedShapes ( ) ;
439421 } ) ;
440- console . error (
441- "CanvasDrawer: Cannot open drawer - CanvasDrawerProvider not found" ,
442- ) ;
443- return ;
444- }
445422
446- if ( drawerUnmountRef . current ) {
447- drawerUnmountRef . current ( ) ;
448- drawerUnmountRef . current = null ;
449- return ;
450- }
423+ return ( ) => {
424+ unsubscribe ( ) ;
425+ } ;
426+ } , [ editor . store ] ) ;
451427
452- const pageUid = getCurrentPageUid ( ) ;
453- const props = getBlockProps ( pageUid ) as Record < string , unknown > ;
454- const rjsqb = props [ "roamjs-query-builder" ] as Record < string , unknown > ;
455- const tldraw = ( rjsqb ?. tldraw as Record < string , unknown > ) || { } ;
456- const store = ( tldraw ?. [ "store" ] as Record < string , unknown > ) || { } ;
457- const shapes = Object . values ( store ) . filter ( ( s ) => {
458- const shape = s as TLBaseShape < string , { uid : string } > ;
459- const uid = shape . props ?. uid ;
460- return ! ! uid ;
461- } ) as DiscourseNodeShape [ ] ;
462-
463- const groupShapesByUid = ( shapes : DiscourseNodeShape [ ] ) => {
464- const groupedShapes = shapes . reduce ( ( acc : GroupedShapes , shape ) => {
465- const uid = shape . props . uid ;
466- if ( ! acc [ uid ] ) acc [ uid ] = [ ] ;
467- acc [ uid ] . push ( shape ) ;
468- return acc ;
469- } , { } ) ;
470-
471- return groupedShapes ;
472- } ;
473-
474- const groupedShapes = groupShapesByUid ( shapes ) ;
475- drawerUnmountRef . current =
476- renderOverlay ( {
477- // eslint-disable-next-line @typescript-eslint/naming-convention
478- Overlay : CanvasDrawer ,
479- props : { groupedShapes, pageUid, unmountRef : drawerUnmountRef } ,
480- } ) || null ;
428+ return (
429+ < >
430+ < div
431+ className = { `pointer-events-auto absolute top-11 m-2 rounded-lg ${ isOpen ? "hidden" : "" } ` }
432+ style = { {
433+ zIndex : 250 ,
434+ // copying tldraw var(--shadow-2)
435+ boxShadow :
436+ "0px 0px 2px hsl(0, 0%, 0%, 16%), 0px 2px 3px hsl(0, 0%, 0%, 24%), 0px 2px 6px hsl(0, 0%, 0%, 0.1), inset 0px 0px 0px 1px hsl(0, 0%, 100%)" ,
437+ backgroundColor : "white" ,
438+ } }
439+ >
440+ < Button
441+ icon = { < Icon icon = "add-column-left" /> }
442+ onClick = { toggleDrawer }
443+ minimal
444+ title = "Toggle Canvas Drawer"
445+ />
446+ </ div >
447+ { isOpen && (
448+ < div
449+ className = "pointer-events-auto absolute bottom-10 left-2 flex w-80 flex-col rounded-lg bg-white"
450+ style = { {
451+ top : "3.25rem" ,
452+ height : "calc(100% - 50px)" ,
453+
454+ zIndex : 250 ,
455+ boxShadow :
456+ "0px 0px 2px hsl(0, 0%, 0%, 16%), 0px 2px 3px hsl(0, 0%, 0%, 24%), 0px 2px 6px hsl(0, 0%, 0%, 0.1), inset 0px 0px 0px 1px hsl(0, 0%, 100%)" ,
457+ } }
458+ >
459+ < div className = "flex max-h-10 flex-shrink-0 items-center rounded-lg bg-white px-1" >
460+ < div className = "flex-shrink-0" >
461+ < Button
462+ icon = { < Icon icon = "add-column-left" /> }
463+ onClick = { ( ) => setIsOpen ( false ) }
464+ minimal
465+ />
466+ </ div >
467+ < h2 className = "m-0 flex-1 border-b border-gray-300 pb-1 text-center text-sm font-semibold leading-tight" >
468+ Canvas Drawer
469+ </ h2 >
470+ < div className = "flex-shrink-0" >
471+ < Button
472+ icon = { < Icon icon = "cross" /> }
473+ onClick = { ( ) => setIsOpen ( false ) }
474+ minimal
475+ small
476+ className = "h-6 min-h-0 p-1"
477+ />
478+ </ div >
479+ </ div >
480+ < div
481+ className = "flex min-h-0 flex-1 flex-col overflow-hidden p-4"
482+ style = { { borderTop : "1px solid hsl(0, 0%, 91%)" } }
483+ >
484+ < CanvasDrawerContent
485+ groupedShapes = { groupedShapes }
486+ pageUid = { pageUid }
487+ editor = { editor }
488+ />
489+ </ div >
490+ </ div >
491+ ) }
492+ </ >
493+ ) ;
481494} ;
482-
483- export default CanvasDrawer ;
0 commit comments