@@ -10,7 +10,9 @@ import {
1010 atom ,
1111 type Atom ,
1212 ConversionsMenuGroup ,
13+ DefaultColorStyle ,
1314 DefaultContextMenu ,
15+ getColorValue ,
1416 DefaultShapeWrapper ,
1517 SelectAllMenuItem ,
1618 TldrawUiButton ,
@@ -42,6 +44,7 @@ import {
4244 CanvasSelectionAttachButton ,
4345} from "./canvasAttach" ;
4446import { CommentUserDialog } from "./CommentUserDialog" ;
47+ import { MagicPen } from "./magicPen" ;
4548import { GROUNDS , groundEditable , groundOf , setGround } from "./canvasGround" ;
4649import {
4750 linkedBoard ,
@@ -235,7 +238,13 @@ function setFresh(fresh: ReadonlyMap<TLShapeId, Fresh>) {
235238
236239/** Rings `ids` until the pointer passes over each, so the reader sees what just arrived or changed. */
237240export function markFresh ( ids : TLShapeId [ ] , kind : Fresh ) {
238- if ( ids . length ) setFresh ( new Map ( [ ...freshShapes ( ) . get ( ) , ...ids . map ( ( id ) => [ id , kind ] as const ) ] ) ) ;
241+ if ( ids . length )
242+ setFresh (
243+ new Map ( [
244+ ...freshShapes ( ) . get ( ) ,
245+ ...ids . map ( ( id ) => [ id , kind ] as const ) ,
246+ ] ) ,
247+ ) ;
239248}
240249
241250export const canvasChromeComponents : TLComponents = {
@@ -258,7 +267,11 @@ export const canvasChromeComponents: TLComponents = {
258267 useEffect ( ( ) => {
259268 if ( ! fresh ) return ;
260269 const seen = ( info : TLEventInfo ) => {
261- if ( info . name !== "pointer_move" || shapeUnderPointer ( editor ) ?. id !== id ) return ;
270+ if (
271+ info . name !== "pointer_move" ||
272+ shapeUnderPointer ( editor ) ?. id !== id
273+ )
274+ return ;
262275 const rest = new Map ( freshShapes ( ) . get ( ) ) ;
263276 rest . delete ( id ) ;
264277 setFresh ( rest ) ;
@@ -270,7 +283,9 @@ export const canvasChromeComponents: TLComponents = {
270283 < DefaultShapeWrapper
271284 ref = { ref }
272285 { ...props }
273- className = { fresh ? `${ props . className ?? "" } sp-fresh` : props . className }
286+ className = {
287+ fresh ? `${ props . className ?? "" } sp-fresh` : props . className
288+ }
274289 data-fresh = { fresh }
275290 />
276291 ) ;
@@ -351,15 +366,22 @@ export const canvasChromeComponents: TLComponents = {
351366 const cover =
352367 canvasIndex ( ) . served && canvasIndex ( ) . project
353368 ? ( board ?? ( over && canvasImageRef ( over . id ) ) )
354- : undefined ;
369+ : undefined ;
355370 const element =
356371 board && pointedElement . current ?. path === ( over as CanvasFileShape ) . props . path
357372 ? pointedElement . current . box
358373 : undefined ;
359374
360375 // The canvas's ground, the strip's swatch as presets. Custom opens that swatch's picker.
361- const page = chrome . activeTab . kind === "canvas" ? pageOf ( chrome . activeTab ) : undefined ;
376+ const page =
377+ chrome . activeTab . kind === "canvas" ? pageOf ( chrome . activeTab ) : undefined ;
362378 const ground = page ? groundOf ( page ) : undefined ;
379+ // The colour of what the right-click picked out that can take one: an arrow, a line, a note
380+ // the agent or the person drew. A board or anything else the layout placed is locked.
381+ const colourable = editor
382+ . getSelectedShapes ( )
383+ . filter ( ( shape ) => ! shape . isLocked && "color" in shape . props ) ;
384+ const colours = editor . getCurrentTheme ( ) . colors [ editor . getColorMode ( ) ] ;
363385
364386 return (
365387 < DefaultContextMenu { ...props } >
@@ -379,10 +401,14 @@ export const canvasChromeComponents: TLComponents = {
379401 { links . length > 0 && (
380402 < TldrawUiMenuItem
381403 id = "copy-link"
382- label = { links . length > 1 ? `Copy ${ links . length } links` : "Copy link" }
404+ label = {
405+ links . length > 1 ? `Copy ${ links . length } links` : "Copy link"
406+ }
383407 icon = { < Copy /> }
384408 kbd = "cmd+c,ctrl+c"
385- onSelect = { ( ) => void navigator . clipboard . writeText ( links . join ( "\n" ) ) }
409+ onSelect = { ( ) =>
410+ void navigator . clipboard . writeText ( links . join ( "\n" ) )
411+ }
386412 />
387413 ) }
388414 { cover && ! isExample ( cover . slug ) && (
@@ -422,13 +448,46 @@ export const canvasChromeComponents: TLComponents = {
422448 checked = { ! GROUNDS . some ( ( [ , color ] ) => color === ground ) }
423449 onSelect = { ( ) =>
424450 document
425- . querySelector < HTMLInputElement > ( ".sp-canvas-tabs-ground input" )
451+ . querySelector < HTMLInputElement > (
452+ ".sp-canvas-tabs-ground input" ,
453+ )
426454 ?. showPicker ( )
427455 }
428456 />
429457 </ TldrawUiMenuSubmenu >
430458 </ TldrawUiMenuGroup >
431459 ) }
460+ { colourable . length > 0 && (
461+ < TldrawUiMenuGroup id = "colour" >
462+ < TldrawUiMenuSubmenu id = "colour" label = "Colour" >
463+ { DefaultColorStyle . values . map ( ( color ) => (
464+ < TldrawUiMenuItem
465+ key = { color }
466+ id = { `colour-${ color } ` }
467+ label = {
468+ color [ 0 ] . toUpperCase ( ) + color . slice ( 1 ) . replace ( "-" , " " )
469+ }
470+ iconLeft = {
471+ < span
472+ className = "sp-menu-swatch"
473+ style = { {
474+ background : getColorValue ( colours , color , "solid" ) ,
475+ } }
476+ />
477+ }
478+ isSelected = { colourable . every (
479+ ( shape ) => ( shape . props as { color : string } ) . color === color ,
480+ ) }
481+ // tldraw leaves a locked shape as it is, so a board in the selection too is
482+ // no matter.
483+ onSelect = { ( ) => {
484+ editor . setStyleForSelectedShapes ( DefaultColorStyle , color ) ;
485+ } }
486+ />
487+ ) ) }
488+ </ TldrawUiMenuSubmenu >
489+ </ TldrawUiMenuGroup >
490+ ) }
432491 { /* tldraw's items one at a time, not its groups: this canvas is read, and every shape on
433492 it is locked and rebuilt from layout.json, so only what works on a locked shape is
434493 here. A group would bring Cut, Delete and Duplicate, greyed out on every shape here,
@@ -522,6 +581,7 @@ export const canvasChromeComponents: TLComponents = {
522581 whose boards its server draws as it draws the project's own (server/projects.ts). */ }
523582 { local ( ) && < CanvasAttachButtons /> }
524583 { local ( ) && < CanvasSelectionAttachButton /> }
584+ { local ( ) && < MagicPen /> }
525585 { /* Out of the tool as well as the bubble. Escape closes only the bubble and leaves the
526586 next click placing another one, which is not what an accidental comment wants. The
527587 draft is kept either way, so a real comment interrupted here is there next time. */ }
0 commit comments