Browse Source

enhance: introduce read only flag

Konstantinos Kaloutas 2 years ago
parent
commit
133d520e25
23 changed files with 73 additions and 61 deletions
  1. 1 1
      src/main/frontend/extensions/tldraw.cljs
  2. 5 4
      tldraw/apps/tldraw-logseq/src/app.tsx
  3. 2 4
      tldraw/apps/tldraw-logseq/src/components/ActionBar/ActionBar.tsx
  4. 3 5
      tldraw/apps/tldraw-logseq/src/components/AppUI.tsx
  5. 0 3
      tldraw/apps/tldraw-logseq/src/components/ContextBar/contextBarActionFactory.tsx
  6. 8 10
      tldraw/apps/tldraw-logseq/src/components/ContextMenu/ContextMenu.tsx
  7. 0 1
      tldraw/apps/tldraw-logseq/src/lib/logseq-context.ts
  8. 9 7
      tldraw/apps/tldraw-logseq/src/lib/shapes/LogseqPortalShape.tsx
  9. 4 0
      tldraw/packages/core/src/lib/TLApi/TLApi.ts
  10. 26 12
      tldraw/packages/core/src/lib/TLApp/TLApp.ts
  11. 1 1
      tldraw/packages/core/src/lib/tools/TLBoxTool/states/PointingState.tsx
  12. 1 1
      tldraw/packages/core/src/lib/tools/TLDotTool/states/IdleState.tsx
  13. 1 1
      tldraw/packages/core/src/lib/tools/TLDrawTool/states/IdleState.tsx
  14. 1 1
      tldraw/packages/core/src/lib/tools/TLLineTool/states/PointingState.tsx
  15. 1 1
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/HoveringSelectionHandleState.ts
  16. 2 3
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/IdleState.ts
  17. 1 1
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingBoundsBackgroundState.ts
  18. 2 1
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingSelectedShapeState.ts
  19. 1 1
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingShapeBehindBoundsState.ts
  20. 1 1
      tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingShapeState.ts
  21. 1 1
      tldraw/packages/core/src/lib/tools/TLTextTool/states/IdleState.tsx
  22. 1 0
      tldraw/packages/react/src/components/App.tsx
  23. 1 1
      tldraw/packages/react/src/hooks/useAppSetup.ts

+ 1 - 1
src/main/frontend/extensions/tldraw.cljs

@@ -153,7 +153,7 @@
        (tldraw {:renderers tldraw-renderers
                 :handlers (get-tldraw-handlers page-name)
                 :onMount on-mount
-                :isPublishing config/publishing?
+                :readOnly config/publishing?
                 :onPersist (fn [app info]
                              (state/set-state! [:whiteboard/last-persisted-at (state/get-current-repo)] (util/time-ms))
                              (util/profile "tldraw persist"

+ 5 - 4
tldraw/apps/tldraw-logseq/src/app.tsx

@@ -53,7 +53,7 @@ const tools: TLReactToolConstructor<Shape>[] = [
 interface LogseqTldrawProps {
   renderers: LogseqContextValue['renderers']
   handlers: LogseqContextValue['handlers']
-  isPublishing: LogseqContextValue['isPublishing']
+  readOnly: boolean
   model?: TLDocumentModel<Shape>
   onMount?: TLReactCallbacks<Shape>['onMount']
   onPersist?: TLReactCallbacks<Shape>['onPersist']
@@ -92,13 +92,14 @@ const AppImpl = () => {
 
 const AppInner = ({
   onPersist,
+  readOnly,
   model,
   ...rest
 }: Omit<LogseqTldrawProps, 'renderers' | 'handlers'>) => {
   const onDrop = useDrop()
   const onPaste = usePaste()
   const onCopy = useCopy()
-  const onQuickAdd = useQuickAdd()
+  const onQuickAdd = readOnly ? null : useQuickAdd()
 
   const onPersistOnDiff: TLReactCallbacks<Shape>['onPersist'] = React.useCallback(
     (app, info) => {
@@ -114,6 +115,7 @@ const AppInner = ({
       onDrop={onDrop}
       onPaste={onPaste}
       onCopy={onCopy}
+      readOnly={readOnly}
       onCanvasDBClick={onQuickAdd}
       onPersist={onPersistOnDiff}
       model={model}
@@ -124,7 +126,7 @@ const AppInner = ({
   )
 }
 
-export const App = function App({ renderers, handlers, isPublishing, ...rest }: LogseqTldrawProps): JSX.Element {
+export const App = function App({ renderers, handlers, ...rest }: LogseqTldrawProps): JSX.Element {
   const memoRenders: any = React.useMemo(() => {
     return Object.fromEntries(
       Object.entries(renderers).map(([key, comp]) => {
@@ -136,7 +138,6 @@ export const App = function App({ renderers, handlers, isPublishing, ...rest }:
   const contextValue = {
     renderers: memoRenders,
     handlers: handlers,
-    isPublishing: isPublishing,
   }
 
   return (

+ 2 - 4
tldraw/apps/tldraw-logseq/src/components/ActionBar/ActionBar.tsx

@@ -8,11 +8,9 @@ import { TablerIcon } from '../icons'
 import { Button } from '../Button'
 import { ZoomMenu } from '../ZoomMenu'
 import * as Separator from '@radix-ui/react-separator'
-import { LogseqContext } from './../../lib/logseq-context'
 
 export const ActionBar = observer(function ActionBar(): JSX.Element {
   const app = useApp<Shape>()
-  const { isPublishing } = React.useContext(LogseqContext)
 
   const undo = React.useCallback(() => {
     app.api.undo()
@@ -32,7 +30,7 @@ export const ActionBar = observer(function ActionBar(): JSX.Element {
 
   return (
     <div className="tl-action-bar">
-      {!isPublishing && (
+      {!app.readOnly && (
         <div className="tl-toolbar tl-history-bar">
           <Button tooltip="Undo" onClick={undo}>
             <TablerIcon name="arrow-back-up" />
@@ -43,7 +41,7 @@ export const ActionBar = observer(function ActionBar(): JSX.Element {
         </div>
       )}
 
-      <div className={`tl-toolbar tl-zoom-bar ${isPublishing ? "" : "ml-4"}`}>
+      <div className={`tl-toolbar tl-zoom-bar ${app.readOnly ? "" : "ml-4"}`}>
         <Button tooltip="Zoom in" onClick={zoomIn} id="tl-zoom-in">
           <TablerIcon name="plus" />
         </Button>

+ 3 - 5
tldraw/apps/tldraw-logseq/src/components/AppUI.tsx

@@ -4,18 +4,16 @@ import { DevTools } from './Devtools'
 import { PrimaryTools } from './PrimaryTools'
 import { StatusBar } from './StatusBar'
 import { isDev } from '@tldraw/core'
-import { LogseqContext } from './../lib/logseq-context'
-import React from 'react'
-
+import { useApp } from '@tldraw/react'
 
 export const AppUI = observer(function AppUI() {
-  const { isPublishing } = React.useContext(LogseqContext)
+  const app = useApp()
 
   return (
     <>
       {isDev() && <StatusBar />}
       {isDev() && <DevTools />}
-      {!isPublishing && <PrimaryTools />}
+      {!app.readOnly && <PrimaryTools />}
       <ActionBar />
     </>
   )

+ 0 - 3
tldraw/apps/tldraw-logseq/src/components/ContextBar/contextBarActionFactory.tsx

@@ -566,9 +566,6 @@ const getContextBarActionTypes = (type: ShapeType) => {
 }
 
 export const getContextBarActionsForShapes = (shapes: Shape[]) => {
-  const {isPublishing} = React.useContext(LogseqContext)
-  if (isPublishing) return []
-
   const types = shapes.map(s => s.props.type)
   const actionTypes = new Set(shapes.length > 0 ? getContextBarActionTypes(types[0]) : [])
   for (let i = 1; i < types.length && actionTypes.size > 0; i++) {

+ 8 - 10
tldraw/apps/tldraw-logseq/src/components/ContextMenu/ContextMenu.tsx

@@ -4,7 +4,6 @@ import { observer } from 'mobx-react-lite'
 import { TablerIcon } from '../icons'
 import { Button } from '../Button'
 import * as React from 'react'
-import { LogseqContext } from './../../lib/logseq-context'
 
 import * as ReactContextMenu from '@radix-ui/react-context-menu'
 import * as Separator from '@radix-ui/react-separator'
@@ -21,7 +20,6 @@ export const ContextMenu = observer(function ContextMenu({
 }: ContextMenuProps) {
   const app = useApp()
   const rContent = React.useRef<HTMLDivElement>(null)
-  const { isPublishing } = React.useContext(LogseqContext)
 
   const runAndTransition = (f: Function) => {
     f()
@@ -56,7 +54,7 @@ export const ContextMenu = observer(function ContextMenu({
         tabIndex={-1}
       >
         <div>
-          {app.selectedShapes?.size > 1 && !isPublishing && (
+          {app.selectedShapes?.size > 1 && !app.readOnly && (
             <>
               <ReactContextMenu.Item>
                 <div className="tl-menu-button-row pb-0">
@@ -145,7 +143,7 @@ export const ContextMenu = observer(function ContextMenu({
           )}
           {(app.selectedShapesArray.some(s => s.type === 'group' || app.getParentGroup(s)) ||
             app.selectedShapesArray.length > 1) &&
-            !isPublishing && (
+            !app.readOnly && (
             <>
               {app.selectedShapesArray.some(s => s.type === 'group' || app.getParentGroup(s)) && (
                 <ReactContextMenu.Item
@@ -180,7 +178,7 @@ export const ContextMenu = observer(function ContextMenu({
           )}
           {app.selectedShapes?.size > 0 && (
             <>
-              {!isPublishing && (
+              {!app.readOnly && (
                 <ReactContextMenu.Item
                   className="tl-menu-item"
                   onClick={() => runAndTransition(app.cut)}
@@ -208,7 +206,7 @@ export const ContextMenu = observer(function ContextMenu({
               </ReactContextMenu.Item>
             </>
           )}
-          {!isPublishing && (
+          {!app.readOnly && (
             <ReactContextMenu.Item
               className="tl-menu-item"
               onClick={() => runAndTransition(app.paste)}
@@ -222,7 +220,7 @@ export const ContextMenu = observer(function ContextMenu({
               </div>
             </ReactContextMenu.Item>
           )}
-          {app.selectedShapes?.size === 1 && !isPublishing && (
+          {app.selectedShapes?.size === 1 && !app.readOnly && (
             <ReactContextMenu.Item
               className="tl-menu-item"
               onClick={() => runAndTransition(() => app.paste(undefined, true))}
@@ -255,7 +253,7 @@ export const ContextMenu = observer(function ContextMenu({
               Deselect all
             </ReactContextMenu.Item>
           )}
-          {app.selectedShapes?.size > 0 && !isPublishing && (
+          {app.selectedShapes?.size > 0 && !app.readOnly && (
             <>
               <ReactContextMenu.Item
                 className="tl-menu-item"
@@ -269,7 +267,7 @@ export const ContextMenu = observer(function ContextMenu({
                   </span>
                 </div>
               </ReactContextMenu.Item>
-              {app.selectedShapes?.size > 1 && !isPublishing && (
+              {app.selectedShapes?.size > 1 && !app.readOnly && (
                 <>
                   <ReactContextMenu.Separator className="menu-separator" />
                   <ReactContextMenu.Item
@@ -288,7 +286,7 @@ export const ContextMenu = observer(function ContextMenu({
                   </ReactContextMenu.Item>
                 </>
               )}
-              {!isPublishing && (
+              {!app.readOnly && (
                 <>
               <ReactContextMenu.Separator className="menu-separator" />
               <ReactContextMenu.Item

+ 0 - 1
tldraw/apps/tldraw-logseq/src/lib/logseq-context.ts

@@ -57,7 +57,6 @@ export interface LogseqContextValue {
     redirectToPage: (uuidOrPageName: string) => void
     copyToClipboard: (text: string, html: string) => void
   }
-  isPublishing: boolean
 }
 
 export const LogseqContext = React.createContext<LogseqContextValue>({} as LogseqContextValue)

+ 9 - 7
tldraw/apps/tldraw-logseq/src/lib/shapes/LogseqPortalShape.tsx

@@ -508,13 +508,15 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
                 {targetNotFound && <div className="tl-target-not-found">Target not found</div>}
                 {showingPortal && <PortalComponent {...componentProps} />}
               </div>
-              <CircleButton
-                active={!!this.collapsed}
-                style={{ opacity: isSelected ? 1 : 0 }}
-                icon={this.props.blockType === 'B' ? 'block' : 'page'}
-                onClick={this.toggleCollapsed}
-                otherIcon={'whiteboard-element'}
-              />
+              {!app.readOnly && (
+                <CircleButton
+                  active={!!this.collapsed}
+                  style={{ opacity: isSelected ? 1 : 0 }}
+                  icon={this.props.blockType === 'B' ? 'block' : 'page'}
+                  onClick={this.toggleCollapsed}
+                  otherIcon={'whiteboard-element'}
+                />
+              )}
             </>
           )}
         </div>

+ 4 - 0
tldraw/packages/core/src/lib/TLApi/TLApi.ts

@@ -367,6 +367,8 @@ export class TLApi<S extends TLShape = TLShape, K extends TLEventMap = TLEventMa
   }
 
   doGroup = (shapes: S[] = this.app.allSelectedShapesArray) => {
+    if (this.app.readOnly) return
+
     const selectedGroups: S[] = [
       ...shapes.filter(s => s.type === 'group'),
       ...shapes.map(s => this.app.getParentGroup(s)),
@@ -393,6 +395,8 @@ export class TLApi<S extends TLShape = TLShape, K extends TLEventMap = TLEventMa
   }
 
   unGroup = (shapes: S[] = this.app.allSelectedShapesArray) => {
+    if (this.app.readOnly) return
+
     const selectedGroups: S[] = [
       ...shapes.filter(s => s.type === 'group'),
       ...shapes.map(s => this.app.getParentGroup(s)),

+ 26 - 12
tldraw/packages/core/src/lib/TLApp/TLApp.ts

@@ -52,10 +52,12 @@ export class TLApp<
   constructor(
     serializedApp?: TLDocumentModel<S>,
     Shapes?: TLShapeConstructor<S>[],
-    Tools?: TLToolConstructor<S, K>[]
+    Tools?: TLToolConstructor<S, K>[],
+    readOnly?: boolean
   ) {
     super()
     this._states = [TLSelectTool, TLMoveTool]
+    this.readOnly = readOnly
     this.history.pause()
     if (this.states && this.states.length > 0) {
       this.registerStates(this.states)
@@ -78,6 +80,8 @@ export class TLApp<
   keybindingRegistered = false
   uuid = uniqueId()
 
+  readOnly: boolean | undefined
+
   static id = 'app'
   static initial = 'select'
 
@@ -205,7 +209,7 @@ export class TLApp<
           // @ts-expect-error ???
           keys: child.constructor['shortcut'] as string | string[],
           fn: (_: any, __: any, e: KeyboardEvent) => {
-            this.transition(child.id)
+            this.selectTool(child.id)
             // hack: allows logseq related shortcut combinations to work
             // fixme?: unsure if it will cause unexpected issues
             // e.stopPropagation()
@@ -323,6 +327,8 @@ export class TLApp<
   }
 
   @action readonly createShapes = (shapes: S[] | TLShapeModel[]): this => {
+    if (this.readOnly) return this
+
     const newShapes = this.currentPage.addShapes(...shapes)
     if (newShapes) this.notify('create-shapes', newShapes)
     this.persist()
@@ -330,13 +336,15 @@ export class TLApp<
   }
 
   @action updateShapes = <T extends S>(shapes: ({ id: string } & Partial<T['props']>)[]): this => {
+    if (this.readOnly) return this
+
     shapes.forEach(shape => this.getShapeById(shape.id)?.update(shape))
     this.persist()
     return this
   }
 
   @action readonly deleteShapes = (shapes: S[] | string[]): this => {
-    if (shapes.length === 0) return this
+    if (shapes.length === 0 || this.readOnly) return this
     const normalizedShapes: S[] = shapes
       .map(shape => (typeof shape === 'string' ? this.getShapeById(shape) : shape))
       .filter(isNonNullable)
@@ -408,22 +416,22 @@ export class TLApp<
   }
 
   bringForward = (shapes: S[] | string[] = this.selectedShapesArray): this => {
-    if (shapes.length > 0) this.currentPage.bringForward(shapes)
+    if (shapes.length > 0 && !this.readOnly) this.currentPage.bringForward(shapes)
     return this
   }
 
   sendBackward = (shapes: S[] | string[] = this.selectedShapesArray): this => {
-    if (shapes.length > 0) this.currentPage.sendBackward(shapes)
+    if (shapes.length > 0 && !this.readOnly) this.currentPage.sendBackward(shapes)
     return this
   }
 
   sendToBack = (shapes: S[] | string[] = this.selectedShapesArray): this => {
-    if (shapes.length > 0) this.currentPage.sendToBack(shapes)
+    if (shapes.length > 0 && !this.readOnly) this.currentPage.sendToBack(shapes)
     return this
   }
 
   bringToFront = (shapes: S[] | string[] = this.selectedShapesArray): this => {
-    if (shapes.length > 0) this.currentPage.bringToFront(shapes)
+    if (shapes.length > 0 && !this.readOnly) this.currentPage.bringToFront(shapes)
     return this
   }
 
@@ -438,7 +446,7 @@ export class TLApp<
   }
 
   align = (type: AlignType, shapes: S[] = this.selectedShapesArray): this => {
-    if (shapes.length < 2) return this
+    if (shapes.length < 2  || this.readOnly) return this
 
     const boundsForShapes = shapes.map(shape => {
       const bounds = shape.getBounds()
@@ -482,7 +490,7 @@ export class TLApp<
   }
 
   distribute = (type: DistributeType, shapes: S[] = this.selectedShapesArray): this => {
-    if (shapes.length < 2) return this
+    if (shapes.length < 2 || this.readOnly) return this
 
     const deltaMap = Object.fromEntries(
       BoundsUtils.getDistributions(shapes, type).map(d => [d.id, d])
@@ -497,7 +505,7 @@ export class TLApp<
   }
 
   packIntoRectangle = (shapes: S[] = this.selectedShapesArray): this => {
-    if (shapes.length < 2) return this
+    if (shapes.length < 2 || this.readOnly) return this
 
     const deltaMap = Object.fromEntries(
       BoundsUtils.getPackedDistributions(shapes).map(d => [d.id, d])
@@ -585,7 +593,7 @@ export class TLApp<
   }
 
   paste = (e?: ClipboardEvent, shiftKey?: boolean) => {
-    if (!this.editingShape) {
+    if (!this.editingShape && !this.readOnly) {
       this.notify('paste', {
         point: this.inputs.currentPoint,
         shiftKey: !!shiftKey,
@@ -616,7 +624,10 @@ export class TLApp<
     return this.currentState
   }
 
-  selectTool = this.transition
+  selectTool = (id: string, data: AnyObject = {}) => {
+    if (!this.readOnly || ['select', 'move'].includes(id) )
+      this.transition(id, data)
+  }
 
   registerTools(tools: TLToolConstructor<S, K>[]) {
     this.Tools = tools
@@ -918,6 +929,7 @@ export class TLApp<
       this.isInAny('select.idle', 'select.hoveringSelectionHandle') &&
       !this.isIn('select.contextMenu') &&
       selectedShapesArray.length > 0 &&
+      !this.readOnly &&
       !selectedShapesArray.every(shape => shape.hideContextBar)
     )
   }
@@ -932,6 +944,7 @@ export class TLApp<
         'select.pointingResizeHandle'
       ) &&
       selectedShapesArray.length > 0 &&
+      !this.readOnly &&
       !selectedShapesArray.some(shape => shape.hideRotateHandle)
     )
   }
@@ -948,6 +961,7 @@ export class TLApp<
         'select.pointingResizeHandle'
       ) &&
       selectedShapesArray.length === 1 &&
+      !this.readOnly &&
       !selectedShapesArray.every(shape => shape.hideResizeHandles)
     )
   }

+ 1 - 1
tldraw/packages/core/src/lib/tools/TLBoxTool/states/PointingState.tsx

@@ -16,7 +16,7 @@ export class PointingState<
 
   onPointerMove: TLStateEvents<S, K>['onPointerMove'] = () => {
     const { currentPoint, originPoint } = this.app.inputs
-    if (Vec.dist(currentPoint, originPoint) > 5) {
+    if (Vec.dist(currentPoint, originPoint) > 5 && !this.app.readOnly) {
       this.tool.transition('creating')
       this.app.setSelectedShapes(this.app.currentPage.shapes)
     }

+ 1 - 1
tldraw/packages/core/src/lib/tools/TLDotTool/states/IdleState.tsx

@@ -14,7 +14,7 @@ export class IdleState<
   static id = 'idle'
 
   onPointerDown: TLStateEvents<S, K>['onPointerDown'] = (info, e) => {
-    if (info.order) return
+    if (info.order || this.app.readOnly) return
     this.tool.transition('creating')
   }
 

+ 1 - 1
tldraw/packages/core/src/lib/tools/TLDrawTool/states/IdleState.tsx

@@ -14,7 +14,7 @@ export class IdleState<
   static id = 'idle'
 
   onPointerDown: TLStateEvents<S, K>['onPointerDown'] = (info, e) => {
-    if (info.order) return
+    if (info.order || this.app.readOnly) return
     this.tool.transition('creating')
   }
 

+ 1 - 1
tldraw/packages/core/src/lib/tools/TLLineTool/states/PointingState.tsx

@@ -16,7 +16,7 @@ export class PointingState<
 
   onPointerMove: TLStateEvents<S, K>['onPointerMove'] = () => {
     const { currentPoint, originPoint } = this.app.inputs
-    if (Vec.dist(currentPoint, originPoint) > 5) {
+    if (Vec.dist(currentPoint, originPoint) > 5 && !this.app.readOnly) {
       this.tool.transition('creating')
       this.app.setSelectedShapes(this.app.currentPage.shapes)
     }

+ 1 - 1
tldraw/packages/core/src/lib/tools/TLSelectTool/states/HoveringSelectionHandleState.ts

@@ -69,7 +69,7 @@ export class HoveringSelectionHandleState<
   }
 
   onDoubleClick: TLEvents<S>['pointer'] = info => {
-    if (info.order) return
+    if (info.order || this.app.readOnly) return
     const isSingle = this.app.selectedShapes.size === 1
     if (!isSingle) return
     const selectedShape = getFirstFromSet(this.app.selectedShapes)

+ 2 - 3
tldraw/packages/core/src/lib/tools/TLSelectTool/states/IdleState.ts

@@ -121,9 +121,8 @@ export class IdleState<
   }
 
   onDoubleClick: TLEvents<S>['pointer'] = info => {
-    if (info.order) return
+    if (info.order || this.app.selectedShapesArray.length !== 1 || this.app.readOnly) return
 
-    if (this.app.selectedShapesArray.length !== 1) return
     const selectedShape = this.app.selectedShapesArray[0]
     if (!selectedShape.canEdit) return
 
@@ -148,7 +147,7 @@ export class IdleState<
     const { selectedShapesArray } = this.app
     switch (e.key) {
       case 'Enter': {
-        if (selectedShapesArray.length === 1 && selectedShapesArray[0].canEdit) {
+        if (selectedShapesArray.length === 1 && selectedShapesArray[0].canEdit && !this.app.readOnly) {
           this.tool.transition('editingShape', {
             type: TLTargetType.Shape,
             shape: selectedShapesArray[0],

+ 1 - 1
tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingBoundsBackgroundState.ts

@@ -17,7 +17,7 @@ export class PointingBoundsBackgroundState<
 
   onPointerMove: TLEvents<S>['pointer'] = () => {
     const { currentPoint, originPoint } = this.app.inputs
-    if (Vec.dist(currentPoint, originPoint) > 5) {
+    if (Vec.dist(currentPoint, originPoint) > 5 && !this.app.readOnly) {
       this.tool.transition('translating')
     }
   }

+ 2 - 1
tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingSelectedShapeState.ts

@@ -31,7 +31,7 @@ export class PointingSelectedShapeState<
 
   onPointerMove: TLEvents<S>['pointer'] = () => {
     const { currentPoint, originPoint } = this.app.inputs
-    if (Vec.dist(currentPoint, originPoint) > 5) {
+    if (Vec.dist(currentPoint, originPoint) > 5 && !this.app.readOnly) {
       this.tool.transition('translating')
     }
   }
@@ -48,6 +48,7 @@ export class PointingSelectedShapeState<
     } else if (
       selectedShapesArray.length === 1 &&
       this.pointedSelectedShape.canEdit &&
+      !this.app.readOnly &&
       this.pointedSelectedShape instanceof TLBoxShape &&
       PointUtils.pointInBounds(currentPoint, this.pointedSelectedShape.bounds)
     ) {

+ 1 - 1
tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingShapeBehindBoundsState.ts

@@ -21,7 +21,7 @@ export class PointingShapeBehindBoundsState<
 
   onPointerMove: TLEvents<S>['pointer'] = () => {
     const { currentPoint, originPoint } = this.app.inputs
-    if (Vec.dist(currentPoint, originPoint) > 5) {
+    if (Vec.dist(currentPoint, originPoint) > 5 && !this.app.readOnly) {
       this.tool.transition('translating')
     }
   }

+ 1 - 1
tldraw/packages/core/src/lib/tools/TLSelectTool/states/PointingShapeState.ts

@@ -28,7 +28,7 @@ export class PointingShapeState<
 
   onPointerMove: TLEvents<S>['pointer'] = () => {
     const { currentPoint, originPoint } = this.app.inputs
-    if (Vec.dist(currentPoint, originPoint) > 5) {
+    if (Vec.dist(currentPoint, originPoint) > 5 && !this.app.readOnly) {
       this.tool.transition('translating')
     }
   }

+ 1 - 1
tldraw/packages/core/src/lib/tools/TLTextTool/states/IdleState.tsx

@@ -14,7 +14,7 @@ export class IdleState<
   static id = 'idle'
 
   onPointerDown: TLStateEvents<S, K>['onPointerDown'] = (info, e) => {
-    if (info.order) return
+    if (info.order || this.app.readOnly) return
     this.tool.transition('creating')
   }
 

+ 1 - 0
tldraw/packages/react/src/components/App.tsx

@@ -26,6 +26,7 @@ export interface TLAppPropsWithoutApp<
   Shapes?: TLReactShapeConstructor<S>[]
   Tools?: TLToolConstructor<S, TLReactEventMap, TLReactApp<S>>[]
   children?: React.ReactNode
+  readOnly?: boolean
 }
 
 export interface TLAppPropsWithApp<

+ 1 - 1
tldraw/packages/react/src/hooks/useAppSetup.ts

@@ -6,7 +6,7 @@ export function useAppSetup<S extends TLReactShape, R extends TLReactApp<S> = TL
   props: TLAppPropsWithoutApp<S, R> | TLAppPropsWithApp<S, R>
 ): R {
   if ('app' in props) return props.app
-  const [app] = React.useState<R>(() => new TLReactApp(props.model, props.Shapes, props.Tools) as R)
+  const [app] = React.useState<R>(() => new TLReactApp(props.model, props.Shapes, props.Tools, props.readOnly) as R)
 
   React.useLayoutEffect(() => {
     app.initKeyboardShortcuts()