UNPKG

tldraw

Version:

A tiny little drawing editor.

4 lines • 77.2 kB
{ "version": 3, "sources": ["../../../../src/lib/ui/context/actions.tsx"], "sourcesContent": ["import {\n\tBox,\n\tDefaultColorStyle,\n\tDefaultFillStyle,\n\tEditor,\n\tHALF_PI,\n\tPageRecordType,\n\tTLBookmarkShape,\n\tTLEmbedShape,\n\tTLFrameShape,\n\tTLGroupShape,\n\tTLShapeId,\n\tTLShapePartial,\n\tTLTextShape,\n\tVec,\n\tapproximately,\n\tcompact,\n\tcreateShapeId,\n\topenWindow,\n\tuseEditor,\n} from '@tldraw/editor'\nimport * as React from 'react'\nimport { kickoutOccludedShapes } from '../../tools/SelectTool/selectHelpers'\nimport { fitFrameToContent, removeFrame } from '../../utils/frames/frames'\nimport { EditLinkDialog } from '../components/EditLinkDialog'\nimport { EmbedDialog } from '../components/EmbedDialog'\nimport { flattenShapesToImages } from '../hooks/useFlatten'\nimport { useShowCollaborationUi } from '../hooks/useIsMultiplayer'\nimport { TLUiTranslationKey } from '../hooks/useTranslation/TLUiTranslationKey'\nimport { TLUiIconType } from '../icon-types'\nimport { TLUiOverrideHelpers, useDefaultHelpers } from '../overrides'\nimport { TLUiEventSource, useUiEvents } from './events'\n\n/** @public */\nexport interface TLUiActionItem<\n\tTransationKey extends string = string,\n\tIconType extends string = string,\n> {\n\ticon?: IconType\n\tid: string\n\tkbd?: string\n\tlabel?: TransationKey | { [key: string]: TransationKey }\n\treadonlyOk?: boolean\n\tcheckbox?: boolean\n\tonSelect(source: TLUiEventSource): Promise<void> | void\n}\n\n/** @public */\nexport type TLUiActionsContextType = Record<string, TLUiActionItem>\n\n/** @internal */\nexport const ActionsContext = React.createContext<TLUiActionsContextType | null>(null)\n\n/** @public */\nexport interface ActionsProviderProps {\n\toverrides?(\n\t\teditor: Editor,\n\t\tactions: TLUiActionsContextType,\n\t\thelpers: TLUiOverrideHelpers\n\t): TLUiActionsContextType\n\tchildren: React.ReactNode\n}\n\nfunction makeActions(actions: TLUiActionItem[]) {\n\treturn Object.fromEntries(actions.map((action) => [action.id, action])) as TLUiActionsContextType\n}\n\nfunction getExportName(editor: Editor, defaultName: string) {\n\tconst selectedShapes = editor.getSelectedShapes()\n\t// When we don't have any shapes selected, we want to use the document name\n\tif (selectedShapes.length === 0) {\n\t\treturn editor.getDocumentSettings().name || defaultName\n\t}\n\treturn undefined\n}\n\n/** @internal */\nexport function ActionsProvider({ overrides, children }: ActionsProviderProps) {\n\tconst editor = useEditor()\n\tconst showCollaborationUi = useShowCollaborationUi()\n\tconst helpers = useDefaultHelpers()\n\tconst trackEvent = useUiEvents()\n\n\tconst defaultDocumentName = helpers.msg('document.default-name')\n\n\t// should this be a useMemo? looks like it doesn't actually deref any reactive values\n\tconst actions = React.useMemo<TLUiActionsContextType>(() => {\n\t\tfunction mustGoBackToSelectToolFirst() {\n\t\t\tif (!editor.isIn('select')) {\n\t\t\t\teditor.complete()\n\t\t\t\teditor.setCurrentTool('select')\n\t\t\t\treturn false // false will still let the action happen, true will stop it\n\t\t\t\t// todo: remove this return value once we're suuuuure\n\t\t\t}\n\n\t\t\treturn false\n\t\t}\n\n\t\tfunction canApplySelectionAction() {\n\t\t\treturn editor.isIn('select') && editor.getSelectedShapeIds().length > 0\n\t\t}\n\n\t\tconst actionItems: TLUiActionItem<TLUiTranslationKey, TLUiIconType>[] = [\n\t\t\t{\n\t\t\t\tid: 'edit-link',\n\t\t\t\tlabel: 'action.edit-link',\n\t\t\t\ticon: 'link',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\ttrackEvent('edit-link', { source })\n\t\t\t\t\teditor.markHistoryStoppingPoint('edit-link')\n\t\t\t\t\thelpers.addDialog({ component: EditLinkDialog })\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'insert-embed',\n\t\t\t\tlabel: 'action.insert-embed',\n\t\t\t\tkbd: '$i',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\ttrackEvent('insert-embed', { source })\n\t\t\t\t\thelpers.addDialog({ component: EmbedDialog })\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'insert-media',\n\t\t\t\tlabel: 'action.insert-media',\n\t\t\t\tkbd: '$u',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\ttrackEvent('insert-media', { source })\n\t\t\t\t\thelpers.insertMedia()\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'undo',\n\t\t\t\tlabel: 'action.undo',\n\t\t\t\ticon: 'undo',\n\t\t\t\tkbd: '$z',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\ttrackEvent('undo', { source })\n\t\t\t\t\teditor.undo()\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'redo',\n\t\t\t\tlabel: 'action.redo',\n\t\t\t\ticon: 'redo',\n\t\t\t\tkbd: '$!z',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\ttrackEvent('redo', { source })\n\t\t\t\t\teditor.redo()\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'export-as-svg',\n\t\t\t\tlabel: {\n\t\t\t\t\tdefault: 'action.export-as-svg',\n\t\t\t\t\tmenu: 'action.export-as-svg.short',\n\t\t\t\t\t['context-menu']: 'action.export-as-svg.short',\n\t\t\t\t},\n\t\t\t\treadonlyOk: true,\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tlet ids = editor.getSelectedShapeIds()\n\t\t\t\t\tif (ids.length === 0) ids = Array.from(editor.getCurrentPageShapeIds().values())\n\t\t\t\t\tif (ids.length === 0) return\n\t\t\t\t\ttrackEvent('export-as', { format: 'svg', source })\n\t\t\t\t\thelpers.exportAs(ids, 'svg', getExportName(editor, defaultDocumentName))\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'export-as-png',\n\t\t\t\tlabel: {\n\t\t\t\t\tdefault: 'action.export-as-png',\n\t\t\t\t\tmenu: 'action.export-as-png.short',\n\t\t\t\t\t['context-menu']: 'action.export-as-png.short',\n\t\t\t\t},\n\t\t\t\treadonlyOk: true,\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tlet ids = editor.getSelectedShapeIds()\n\t\t\t\t\tif (ids.length === 0) ids = Array.from(editor.getCurrentPageShapeIds().values())\n\t\t\t\t\tif (ids.length === 0) return\n\t\t\t\t\ttrackEvent('export-as', { format: 'png', source })\n\t\t\t\t\thelpers.exportAs(ids, 'png', getExportName(editor, defaultDocumentName))\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'export-as-json',\n\t\t\t\tlabel: {\n\t\t\t\t\tdefault: 'action.export-as-json',\n\t\t\t\t\tmenu: 'action.export-as-json.short',\n\t\t\t\t\t['context-menu']: 'action.export-as-json.short',\n\t\t\t\t},\n\t\t\t\treadonlyOk: true,\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tlet ids = editor.getSelectedShapeIds()\n\t\t\t\t\tif (ids.length === 0) ids = Array.from(editor.getCurrentPageShapeIds().values())\n\t\t\t\t\tif (ids.length === 0) return\n\t\t\t\t\ttrackEvent('export-as', { format: 'json', source })\n\t\t\t\t\thelpers.exportAs(ids, 'json', getExportName(editor, defaultDocumentName))\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'export-all-as-svg',\n\t\t\t\tlabel: {\n\t\t\t\t\tdefault: 'action.export-all-as-svg',\n\t\t\t\t\tmenu: 'action.export-all-as-svg.short',\n\t\t\t\t\t['context-menu']: 'action.export-all-as-svg.short',\n\t\t\t\t},\n\t\t\t\treadonlyOk: true,\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tlet ids = editor.getSelectedShapeIds()\n\t\t\t\t\tif (ids.length === 0) ids = Array.from(editor.getCurrentPageShapeIds().values())\n\t\t\t\t\tif (ids.length === 0) return\n\t\t\t\t\ttrackEvent('export-all-as', { format: 'svg', source })\n\t\t\t\t\thelpers.exportAs(\n\t\t\t\t\t\tArray.from(editor.getCurrentPageShapeIds()),\n\t\t\t\t\t\t'svg',\n\t\t\t\t\t\tgetExportName(editor, defaultDocumentName)\n\t\t\t\t\t)\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'export-all-as-png',\n\t\t\t\tlabel: {\n\t\t\t\t\tdefault: 'action.export-all-as-png',\n\t\t\t\t\tmenu: 'action.export-all-as-png.short',\n\t\t\t\t\t['context-menu']: 'action.export-all-as-png.short',\n\t\t\t\t},\n\t\t\t\treadonlyOk: true,\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tconst ids = Array.from(editor.getCurrentPageShapeIds().values())\n\t\t\t\t\tif (ids.length === 0) return\n\t\t\t\t\ttrackEvent('export-all-as', { format: 'png', source })\n\t\t\t\t\thelpers.exportAs(ids, 'png', getExportName(editor, defaultDocumentName))\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'export-all-as-json',\n\t\t\t\tlabel: {\n\t\t\t\t\tdefault: 'action.export-all-as-json',\n\t\t\t\t\tmenu: 'action.export-all-as-json.short',\n\t\t\t\t\t['context-menu']: 'action.export-all-as-json.short',\n\t\t\t\t},\n\t\t\t\treadonlyOk: true,\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tconst ids = Array.from(editor.getCurrentPageShapeIds().values())\n\t\t\t\t\tif (ids.length === 0) return\n\t\t\t\t\ttrackEvent('export-all-as', { format: 'json', source })\n\t\t\t\t\thelpers.exportAs(ids, 'json', getExportName(editor, defaultDocumentName))\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'copy-as-svg',\n\t\t\t\tlabel: {\n\t\t\t\t\tdefault: 'action.copy-as-svg',\n\t\t\t\t\tmenu: 'action.copy-as-svg.short',\n\t\t\t\t\t['context-menu']: 'action.copy-as-svg.short',\n\t\t\t\t},\n\t\t\t\tkbd: '$!c',\n\t\t\t\treadonlyOk: true,\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tlet ids = editor.getSelectedShapeIds()\n\t\t\t\t\tif (ids.length === 0) ids = Array.from(editor.getCurrentPageShapeIds().values())\n\t\t\t\t\tif (ids.length === 0) return\n\t\t\t\t\ttrackEvent('copy-as', { format: 'svg', source })\n\t\t\t\t\thelpers.copyAs(ids, 'svg')\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'copy-as-png',\n\t\t\t\tlabel: {\n\t\t\t\t\tdefault: 'action.copy-as-png',\n\t\t\t\t\tmenu: 'action.copy-as-png.short',\n\t\t\t\t\t['context-menu']: 'action.copy-as-png.short',\n\t\t\t\t},\n\t\t\t\treadonlyOk: true,\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tlet ids = editor.getSelectedShapeIds()\n\t\t\t\t\tif (ids.length === 0) ids = Array.from(editor.getCurrentPageShapeIds().values())\n\t\t\t\t\tif (ids.length === 0) return\n\t\t\t\t\ttrackEvent('copy-as', { format: 'png', source })\n\t\t\t\t\thelpers.copyAs(ids, 'png')\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'copy-as-json',\n\t\t\t\tlabel: {\n\t\t\t\t\tdefault: 'action.copy-as-json',\n\t\t\t\t\tmenu: 'action.copy-as-json.short',\n\t\t\t\t\t['context-menu']: 'action.copy-as-json.short',\n\t\t\t\t},\n\t\t\t\treadonlyOk: true,\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tlet ids = editor.getSelectedShapeIds()\n\t\t\t\t\tif (ids.length === 0) ids = Array.from(editor.getCurrentPageShapeIds().values())\n\t\t\t\t\tif (ids.length === 0) return\n\t\t\t\t\ttrackEvent('copy-as', { format: 'json', source })\n\t\t\t\t\thelpers.copyAs(ids, 'json')\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'toggle-auto-size',\n\t\t\t\tlabel: 'action.toggle-auto-size',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\ttrackEvent('toggle-auto-size', { source })\n\t\t\t\t\teditor.markHistoryStoppingPoint('toggling auto size')\n\t\t\t\t\tconst shapes = editor\n\t\t\t\t\t\t.getSelectedShapes()\n\t\t\t\t\t\t.filter(\n\t\t\t\t\t\t\t(shape): shape is TLTextShape =>\n\t\t\t\t\t\t\t\teditor.isShapeOfType<TLTextShape>(shape, 'text') && shape.props.autoSize === false\n\t\t\t\t\t\t)\n\t\t\t\t\teditor.updateShapes(\n\t\t\t\t\t\tshapes.map((shape) => {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tid: shape.id,\n\t\t\t\t\t\t\t\ttype: shape.type,\n\t\t\t\t\t\t\t\tprops: {\n\t\t\t\t\t\t\t\t\t...shape.props,\n\t\t\t\t\t\t\t\t\tw: 8,\n\t\t\t\t\t\t\t\t\tautoSize: true,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t})\n\t\t\t\t\t)\n\t\t\t\t\tkickoutOccludedShapes(\n\t\t\t\t\t\teditor,\n\t\t\t\t\t\tshapes.map((shape) => shape.id)\n\t\t\t\t\t)\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'open-embed-link',\n\t\t\t\tlabel: 'action.open-embed-link',\n\t\t\t\treadonlyOk: true,\n\t\t\t\tonSelect(source) {\n\t\t\t\t\ttrackEvent('open-embed-link', { source })\n\t\t\t\t\tconst ids = editor.getSelectedShapeIds()\n\t\t\t\t\tconst warnMsg = 'No embed shapes selected'\n\t\t\t\t\tif (ids.length !== 1) {\n\t\t\t\t\t\tconsole.error(warnMsg)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tconst shape = editor.getShape(ids[0])\n\t\t\t\t\tif (!shape || !editor.isShapeOfType<TLEmbedShape>(shape, 'embed')) {\n\t\t\t\t\t\tconsole.error(warnMsg)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\topenWindow(shape.props.url, '_blank')\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'select-zoom-tool',\n\t\t\t\treadonlyOk: true,\n\t\t\t\tkbd: 'z',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (editor.root.getCurrent()?.id === 'zoom') return\n\n\t\t\t\t\ttrackEvent('zoom-tool', { source })\n\t\t\t\t\tif (!(editor.inputs.shiftKey || editor.inputs.ctrlKey)) {\n\t\t\t\t\t\tconst currentTool = editor.root.getCurrent()\n\t\t\t\t\t\tif (currentTool && currentTool.getCurrent()?.id === 'idle') {\n\t\t\t\t\t\t\teditor.setCurrentTool('zoom', { onInteractionEnd: currentTool.id, maskAs: 'zoom' })\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'convert-to-bookmark',\n\t\t\t\tlabel: 'action.convert-to-bookmark',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\teditor.run(() => {\n\t\t\t\t\t\ttrackEvent('convert-to-bookmark', { source })\n\t\t\t\t\t\tconst shapes = editor.getSelectedShapes()\n\n\t\t\t\t\t\tconst createList: TLShapePartial[] = []\n\t\t\t\t\t\tconst deleteList: TLShapeId[] = []\n\t\t\t\t\t\tfor (const shape of shapes) {\n\t\t\t\t\t\t\tif (!shape || !editor.isShapeOfType<TLEmbedShape>(shape, 'embed') || !shape.props.url)\n\t\t\t\t\t\t\t\tcontinue\n\n\t\t\t\t\t\t\tconst newPos = new Vec(shape.x, shape.y)\n\t\t\t\t\t\t\tnewPos.rot(-shape.rotation)\n\t\t\t\t\t\t\tnewPos.add(new Vec(shape.props.w / 2 - 300 / 2, shape.props.h / 2 - 320 / 2)) // see bookmark shape util\n\t\t\t\t\t\t\tnewPos.rot(shape.rotation)\n\t\t\t\t\t\t\tconst partial: TLShapePartial<TLBookmarkShape> = {\n\t\t\t\t\t\t\t\tid: createShapeId(),\n\t\t\t\t\t\t\t\ttype: 'bookmark',\n\t\t\t\t\t\t\t\trotation: shape.rotation,\n\t\t\t\t\t\t\t\tx: newPos.x,\n\t\t\t\t\t\t\t\ty: newPos.y,\n\t\t\t\t\t\t\t\topacity: 1,\n\t\t\t\t\t\t\t\tprops: {\n\t\t\t\t\t\t\t\t\turl: shape.props.url,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tcreateList.push(partial)\n\t\t\t\t\t\t\tdeleteList.push(shape.id)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\teditor.markHistoryStoppingPoint('convert shapes to bookmark')\n\t\t\t\t\t\teditor.deleteShapes(deleteList)\n\t\t\t\t\t\teditor.createShapes(createList)\n\t\t\t\t\t})\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'convert-to-embed',\n\t\t\t\tlabel: 'action.convert-to-embed',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\ttrackEvent('convert-to-embed', { source })\n\n\t\t\t\t\teditor.run(() => {\n\t\t\t\t\t\tconst ids = editor.getSelectedShapeIds()\n\t\t\t\t\t\tconst shapes = compact(ids.map((id) => editor.getShape(id)))\n\n\t\t\t\t\t\tconst createList: TLShapePartial[] = []\n\t\t\t\t\t\tconst deleteList: TLShapeId[] = []\n\t\t\t\t\t\tfor (const shape of shapes) {\n\t\t\t\t\t\t\tif (!editor.isShapeOfType<TLBookmarkShape>(shape, 'bookmark')) continue\n\n\t\t\t\t\t\t\tconst { url } = shape.props\n\n\t\t\t\t\t\t\tconst embedInfo = helpers.getEmbedDefinition(url)\n\n\t\t\t\t\t\t\tif (!embedInfo) continue\n\t\t\t\t\t\t\tif (!embedInfo.definition) continue\n\n\t\t\t\t\t\t\tconst { width, height } = embedInfo.definition\n\n\t\t\t\t\t\t\tconst newPos = new Vec(shape.x, shape.y)\n\t\t\t\t\t\t\tnewPos.rot(-shape.rotation)\n\t\t\t\t\t\t\tnewPos.add(new Vec(shape.props.w / 2 - width / 2, shape.props.h / 2 - height / 2))\n\t\t\t\t\t\t\tnewPos.rot(shape.rotation)\n\n\t\t\t\t\t\t\tconst shapeToCreate: TLShapePartial<TLEmbedShape> = {\n\t\t\t\t\t\t\t\tid: createShapeId(),\n\t\t\t\t\t\t\t\ttype: 'embed',\n\t\t\t\t\t\t\t\tx: newPos.x,\n\t\t\t\t\t\t\t\ty: newPos.y,\n\t\t\t\t\t\t\t\trotation: shape.rotation,\n\t\t\t\t\t\t\t\tprops: {\n\t\t\t\t\t\t\t\t\turl: url,\n\t\t\t\t\t\t\t\t\tw: width,\n\t\t\t\t\t\t\t\t\th: height,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tcreateList.push(shapeToCreate)\n\t\t\t\t\t\t\tdeleteList.push(shape.id)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\teditor.markHistoryStoppingPoint('convert shapes to embed')\n\t\t\t\t\t\teditor.deleteShapes(deleteList)\n\t\t\t\t\t\teditor.createShapes(createList)\n\t\t\t\t\t})\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'duplicate',\n\t\t\t\tkbd: '$d',\n\t\t\t\tlabel: 'action.duplicate',\n\t\t\t\ticon: 'duplicate',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\ttrackEvent('duplicate-shapes', { source })\n\t\t\t\t\tconst instanceState = editor.getInstanceState()\n\t\t\t\t\tlet ids: TLShapeId[]\n\t\t\t\t\tlet offset: { x: number; y: number }\n\n\t\t\t\t\tif (instanceState.duplicateProps) {\n\t\t\t\t\t\tids = instanceState.duplicateProps.shapeIds\n\t\t\t\t\t\toffset = instanceState.duplicateProps.offset\n\t\t\t\t\t} else {\n\t\t\t\t\t\tids = editor.getSelectedShapeIds()\n\t\t\t\t\t\tconst commonBounds = Box.Common(compact(ids.map((id) => editor.getShapePageBounds(id))))\n\t\t\t\t\t\toffset = editor.getCameraOptions().isLocked\n\t\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t\t// same as the adjacent note margin\n\t\t\t\t\t\t\t\t\tx: editor.options.adjacentShapeMargin,\n\t\t\t\t\t\t\t\t\ty: editor.options.adjacentShapeMargin,\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t: {\n\t\t\t\t\t\t\t\t\tx: commonBounds.width + editor.options.adjacentShapeMargin,\n\t\t\t\t\t\t\t\t\ty: 0,\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\teditor.markHistoryStoppingPoint('duplicate shapes')\n\t\t\t\t\teditor.duplicateShapes(ids, offset)\n\n\t\t\t\t\tif (instanceState.duplicateProps) {\n\t\t\t\t\t\t// If we are using duplicate props then we update the shape ids to the\n\t\t\t\t\t\t// ids of the newly created shapes to keep the duplication going\n\t\t\t\t\t\teditor.updateInstanceState({\n\t\t\t\t\t\t\tduplicateProps: {\n\t\t\t\t\t\t\t\t...instanceState.duplicateProps,\n\t\t\t\t\t\t\t\tshapeIds: editor.getSelectedShapeIds(),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'ungroup',\n\t\t\t\tlabel: 'action.ungroup',\n\t\t\t\tkbd: '$!g',\n\t\t\t\ticon: 'ungroup',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\ttrackEvent('ungroup-shapes', { source })\n\t\t\t\t\teditor.markHistoryStoppingPoint('ungroup')\n\t\t\t\t\teditor.ungroupShapes(editor.getSelectedShapeIds())\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'group',\n\t\t\t\tlabel: 'action.group',\n\t\t\t\tkbd: '$g',\n\t\t\t\ticon: 'group',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\ttrackEvent('group-shapes', { source })\n\t\t\t\t\tconst onlySelectedShape = editor.getOnlySelectedShape()\n\t\t\t\t\tif (onlySelectedShape && editor.isShapeOfType<TLGroupShape>(onlySelectedShape, 'group')) {\n\t\t\t\t\t\teditor.markHistoryStoppingPoint('ungroup')\n\t\t\t\t\t\teditor.ungroupShapes(editor.getSelectedShapeIds())\n\t\t\t\t\t} else {\n\t\t\t\t\t\teditor.markHistoryStoppingPoint('group')\n\t\t\t\t\t\teditor.groupShapes(editor.getSelectedShapeIds())\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'remove-frame',\n\t\t\t\tlabel: 'action.remove-frame',\n\t\t\t\tkbd: '$!f',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\n\t\t\t\t\ttrackEvent('remove-frame', { source })\n\t\t\t\t\tconst selectedShapes = editor.getSelectedShapes()\n\t\t\t\t\tif (\n\t\t\t\t\t\tselectedShapes.length > 0 &&\n\t\t\t\t\t\tselectedShapes.every((shape) => editor.isShapeOfType<TLFrameShape>(shape, 'frame'))\n\t\t\t\t\t) {\n\t\t\t\t\t\teditor.markHistoryStoppingPoint('remove-frame')\n\t\t\t\t\t\tremoveFrame(\n\t\t\t\t\t\t\teditor,\n\t\t\t\t\t\t\tselectedShapes.map((shape) => shape.id)\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'fit-frame-to-content',\n\t\t\t\tlabel: 'action.fit-frame-to-content',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\n\t\t\t\t\ttrackEvent('fit-frame-to-content', { source })\n\t\t\t\t\tconst onlySelectedShape = editor.getOnlySelectedShape()\n\t\t\t\t\tif (onlySelectedShape && editor.isShapeOfType<TLFrameShape>(onlySelectedShape, 'frame')) {\n\t\t\t\t\t\teditor.markHistoryStoppingPoint('fit-frame-to-content')\n\t\t\t\t\t\tfitFrameToContent(editor, onlySelectedShape.id)\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'align-left',\n\t\t\t\tlabel: 'action.align-left',\n\t\t\t\tkbd: '?A',\n\t\t\t\ticon: 'align-left',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\ttrackEvent('align-shapes', { operation: 'left', source })\n\t\t\t\t\teditor.markHistoryStoppingPoint('align left')\n\t\t\t\t\tconst selectedShapeIds = editor.getSelectedShapeIds()\n\t\t\t\t\teditor.alignShapes(selectedShapeIds, 'left')\n\t\t\t\t\tkickoutOccludedShapes(editor, selectedShapeIds)\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'align-center-horizontal',\n\t\t\t\tlabel: {\n\t\t\t\t\tdefault: 'action.align-center-horizontal',\n\t\t\t\t\t['context-menu']: 'action.align-center-horizontal.short',\n\t\t\t\t},\n\t\t\t\tkbd: '?H',\n\t\t\t\ticon: 'align-center-horizontal',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\ttrackEvent('align-shapes', { operation: 'center-horizontal', source })\n\t\t\t\t\teditor.markHistoryStoppingPoint('align center horizontal')\n\t\t\t\t\tconst selectedShapeIds = editor.getSelectedShapeIds()\n\t\t\t\t\teditor.alignShapes(selectedShapeIds, 'center-horizontal')\n\t\t\t\t\tkickoutOccludedShapes(editor, selectedShapeIds)\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'align-right',\n\t\t\t\tlabel: 'action.align-right',\n\t\t\t\tkbd: '?D',\n\t\t\t\ticon: 'align-right',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\ttrackEvent('align-shapes', { operation: 'right', source })\n\t\t\t\t\teditor.markHistoryStoppingPoint('align right')\n\t\t\t\t\tconst selectedShapeIds = editor.getSelectedShapeIds()\n\t\t\t\t\teditor.alignShapes(selectedShapeIds, 'right')\n\t\t\t\t\tkickoutOccludedShapes(editor, selectedShapeIds)\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'align-center-vertical',\n\t\t\t\tlabel: {\n\t\t\t\t\tdefault: 'action.align-center-vertical',\n\t\t\t\t\t['context-menu']: 'action.align-center-vertical.short',\n\t\t\t\t},\n\t\t\t\tkbd: '?V',\n\t\t\t\ticon: 'align-center-vertical',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\ttrackEvent('align-shapes', { operation: 'center-vertical', source })\n\t\t\t\t\teditor.markHistoryStoppingPoint('align center vertical')\n\t\t\t\t\tconst selectedShapeIds = editor.getSelectedShapeIds()\n\t\t\t\t\teditor.alignShapes(selectedShapeIds, 'center-vertical')\n\t\t\t\t\tkickoutOccludedShapes(editor, selectedShapeIds)\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'align-top',\n\t\t\t\tlabel: 'action.align-top',\n\t\t\t\ticon: 'align-top',\n\t\t\t\tkbd: '?W',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\ttrackEvent('align-shapes', { operation: 'top', source })\n\t\t\t\t\teditor.markHistoryStoppingPoint('align top')\n\t\t\t\t\tconst selectedShapeIds = editor.getSelectedShapeIds()\n\t\t\t\t\teditor.alignShapes(selectedShapeIds, 'top')\n\t\t\t\t\tkickoutOccludedShapes(editor, selectedShapeIds)\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'align-bottom',\n\t\t\t\tlabel: 'action.align-bottom',\n\t\t\t\ticon: 'align-bottom',\n\t\t\t\tkbd: '?S',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\ttrackEvent('align-shapes', { operation: 'bottom', source })\n\t\t\t\t\teditor.markHistoryStoppingPoint('align bottom')\n\t\t\t\t\tconst selectedShapeIds = editor.getSelectedShapeIds()\n\t\t\t\t\teditor.alignShapes(selectedShapeIds, 'bottom')\n\t\t\t\t\tkickoutOccludedShapes(editor, selectedShapeIds)\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'distribute-horizontal',\n\t\t\t\tlabel: {\n\t\t\t\t\tdefault: 'action.distribute-horizontal',\n\t\t\t\t\t['context-menu']: 'action.distribute-horizontal.short',\n\t\t\t\t},\n\t\t\t\ticon: 'distribute-horizontal',\n\t\t\t\tkbd: '?!h',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\ttrackEvent('distribute-shapes', { operation: 'horizontal', source })\n\t\t\t\t\teditor.markHistoryStoppingPoint('distribute horizontal')\n\t\t\t\t\tconst selectedShapeIds = editor.getSelectedShapeIds()\n\t\t\t\t\teditor.distributeShapes(selectedShapeIds, 'horizontal')\n\t\t\t\t\tkickoutOccludedShapes(editor, selectedShapeIds)\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'distribute-vertical',\n\t\t\t\tlabel: {\n\t\t\t\t\tdefault: 'action.distribute-vertical',\n\t\t\t\t\t['context-menu']: 'action.distribute-vertical.short',\n\t\t\t\t},\n\t\t\t\ticon: 'distribute-vertical',\n\t\t\t\tkbd: '?!V',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\ttrackEvent('distribute-shapes', { operation: 'vertical', source })\n\t\t\t\t\teditor.markHistoryStoppingPoint('distribute vertical')\n\t\t\t\t\tconst selectedShapeIds = editor.getSelectedShapeIds()\n\t\t\t\t\teditor.distributeShapes(selectedShapeIds, 'vertical')\n\t\t\t\t\tkickoutOccludedShapes(editor, selectedShapeIds)\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'stretch-horizontal',\n\t\t\t\tlabel: {\n\t\t\t\t\tdefault: 'action.stretch-horizontal',\n\t\t\t\t\t['context-menu']: 'action.stretch-horizontal.short',\n\t\t\t\t},\n\t\t\t\ticon: 'stretch-horizontal',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\ttrackEvent('stretch-shapes', { operation: 'horizontal', source })\n\t\t\t\t\teditor.markHistoryStoppingPoint('stretch horizontal')\n\t\t\t\t\tconst selectedShapeIds = editor.getSelectedShapeIds()\n\t\t\t\t\teditor.stretchShapes(selectedShapeIds, 'horizontal')\n\t\t\t\t\tkickoutOccludedShapes(editor, selectedShapeIds)\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'stretch-vertical',\n\t\t\t\tlabel: {\n\t\t\t\t\tdefault: 'action.stretch-vertical',\n\t\t\t\t\t['context-menu']: 'action.stretch-vertical.short',\n\t\t\t\t},\n\t\t\t\ticon: 'stretch-vertical',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\ttrackEvent('stretch-shapes', { operation: 'vertical', source })\n\t\t\t\t\teditor.markHistoryStoppingPoint('stretch vertical')\n\t\t\t\t\tconst selectedShapeIds = editor.getSelectedShapeIds()\n\t\t\t\t\teditor.stretchShapes(selectedShapeIds, 'vertical')\n\t\t\t\t\tkickoutOccludedShapes(editor, selectedShapeIds)\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'flip-horizontal',\n\t\t\t\tlabel: {\n\t\t\t\t\tdefault: 'action.flip-horizontal',\n\t\t\t\t\t['context-menu']: 'action.flip-horizontal.short',\n\t\t\t\t},\n\t\t\t\tkbd: '!h',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\ttrackEvent('flip-shapes', { operation: 'horizontal', source })\n\t\t\t\t\teditor.markHistoryStoppingPoint('flip horizontal')\n\t\t\t\t\tconst selectedShapeIds = editor.getSelectedShapeIds()\n\t\t\t\t\teditor.flipShapes(selectedShapeIds, 'horizontal')\n\t\t\t\t\tkickoutOccludedShapes(editor, selectedShapeIds)\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'flip-vertical',\n\t\t\t\tlabel: { default: 'action.flip-vertical', ['context-menu']: 'action.flip-vertical.short' },\n\t\t\t\tkbd: '!v',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\ttrackEvent('flip-shapes', { operation: 'vertical', source })\n\t\t\t\t\teditor.markHistoryStoppingPoint('flip vertical')\n\t\t\t\t\tconst selectedShapeIds = editor.getSelectedShapeIds()\n\t\t\t\t\teditor.flipShapes(selectedShapeIds, 'vertical')\n\t\t\t\t\tkickoutOccludedShapes(editor, selectedShapeIds)\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'pack',\n\t\t\t\tlabel: 'action.pack',\n\t\t\t\ticon: 'pack',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\ttrackEvent('pack-shapes', { source })\n\t\t\t\t\teditor.markHistoryStoppingPoint('pack')\n\t\t\t\t\tconst selectedShapeIds = editor.getSelectedShapeIds()\n\t\t\t\t\teditor.packShapes(selectedShapeIds, editor.options.adjacentShapeMargin)\n\t\t\t\t\tkickoutOccludedShapes(editor, selectedShapeIds)\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'stack-vertical',\n\t\t\t\tlabel: {\n\t\t\t\t\tdefault: 'action.stack-vertical',\n\t\t\t\t\t['context-menu']: 'action.stack-vertical.short',\n\t\t\t\t},\n\t\t\t\ticon: 'stack-vertical',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\ttrackEvent('stack-shapes', { operation: 'vertical', source })\n\t\t\t\t\teditor.markHistoryStoppingPoint('stack-vertical')\n\t\t\t\t\tconst selectedShapeIds = editor.getSelectedShapeIds()\n\t\t\t\t\teditor.stackShapes(selectedShapeIds, 'vertical', 16)\n\t\t\t\t\tkickoutOccludedShapes(editor, selectedShapeIds)\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'stack-horizontal',\n\t\t\t\tlabel: {\n\t\t\t\t\tdefault: 'action.stack-horizontal',\n\t\t\t\t\t['context-menu']: 'action.stack-horizontal.short',\n\t\t\t\t},\n\t\t\t\ticon: 'stack-horizontal',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\ttrackEvent('stack-shapes', { operation: 'horizontal', source })\n\t\t\t\t\teditor.markHistoryStoppingPoint('stack-horizontal')\n\t\t\t\t\tconst selectedShapeIds = editor.getSelectedShapeIds()\n\t\t\t\t\teditor.stackShapes(selectedShapeIds, 'horizontal', 16)\n\t\t\t\t\tkickoutOccludedShapes(editor, selectedShapeIds)\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'bring-to-front',\n\t\t\t\tlabel: 'action.bring-to-front',\n\t\t\t\tkbd: ']',\n\t\t\t\ticon: 'bring-to-front',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\ttrackEvent('reorder-shapes', { operation: 'toFront', source })\n\t\t\t\t\teditor.markHistoryStoppingPoint('bring to front')\n\t\t\t\t\teditor.bringToFront(editor.getSelectedShapeIds())\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'bring-forward',\n\t\t\t\tlabel: 'action.bring-forward',\n\t\t\t\ticon: 'bring-forward',\n\t\t\t\tkbd: '?]',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\ttrackEvent('reorder-shapes', { operation: 'forward', source })\n\t\t\t\t\teditor.markHistoryStoppingPoint('bring forward')\n\t\t\t\t\teditor.bringForward(editor.getSelectedShapeIds())\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'send-backward',\n\t\t\t\tlabel: 'action.send-backward',\n\t\t\t\ticon: 'send-backward',\n\t\t\t\tkbd: '?[',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\ttrackEvent('reorder-shapes', { operation: 'backward', source })\n\t\t\t\t\teditor.markHistoryStoppingPoint('send backward')\n\t\t\t\t\teditor.sendBackward(editor.getSelectedShapeIds())\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'send-to-back',\n\t\t\t\tlabel: 'action.send-to-back',\n\t\t\t\ticon: 'send-to-back',\n\t\t\t\tkbd: '[',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\ttrackEvent('reorder-shapes', { operation: 'toBack', source })\n\t\t\t\t\teditor.markHistoryStoppingPoint('send to back')\n\t\t\t\t\teditor.sendToBack(editor.getSelectedShapeIds())\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'cut',\n\t\t\t\tlabel: 'action.cut',\n\t\t\t\tkbd: '$x',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\teditor.markHistoryStoppingPoint('cut')\n\t\t\t\t\thelpers.cut(source)\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'copy',\n\t\t\t\tlabel: 'action.copy',\n\t\t\t\tkbd: '$c',\n\t\t\t\treadonlyOk: true,\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\thelpers.copy(source)\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'paste',\n\t\t\t\tlabel: 'action.paste',\n\t\t\t\tkbd: '$v',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tnavigator.clipboard\n\t\t\t\t\t\t?.read()\n\t\t\t\t\t\t.then((clipboardItems) => {\n\t\t\t\t\t\t\thelpers.paste(\n\t\t\t\t\t\t\t\tclipboardItems,\n\t\t\t\t\t\t\t\tsource,\n\t\t\t\t\t\t\t\tsource === 'context-menu' ? editor.inputs.currentPagePoint : undefined\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.catch(() => {\n\t\t\t\t\t\t\thelpers.addToast({\n\t\t\t\t\t\t\t\ttitle: helpers.msg('action.paste-error-title'),\n\t\t\t\t\t\t\t\tdescription: helpers.msg('action.paste-error-description'),\n\t\t\t\t\t\t\t\tseverity: 'error',\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t})\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'select-all',\n\t\t\t\tlabel: 'action.select-all',\n\t\t\t\tkbd: '$a',\n\t\t\t\treadonlyOk: true,\n\t\t\t\tonSelect(source) {\n\t\t\t\t\teditor.run(() => {\n\t\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\t\ttrackEvent('select-all-shapes', { source })\n\n\t\t\t\t\t\teditor.markHistoryStoppingPoint('select all kbd')\n\t\t\t\t\t\teditor.selectAll()\n\t\t\t\t\t})\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'select-none',\n\t\t\t\tlabel: 'action.select-none',\n\t\t\t\treadonlyOk: true,\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\ttrackEvent('select-none-shapes', { source })\n\t\t\t\t\teditor.markHistoryStoppingPoint('select none')\n\t\t\t\t\teditor.selectNone()\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'delete',\n\t\t\t\tlabel: 'action.delete',\n\t\t\t\tkbd: '\u232B,del,backspace',\n\t\t\t\ticon: 'trash',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\ttrackEvent('delete-shapes', { source })\n\t\t\t\t\teditor.markHistoryStoppingPoint('delete')\n\t\t\t\t\teditor.deleteShapes(editor.getSelectedShapeIds())\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'rotate-cw',\n\t\t\t\tlabel: 'action.rotate-cw',\n\t\t\t\ticon: 'rotate-cw',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\ttrackEvent('rotate-cw', { source })\n\t\t\t\t\teditor.markHistoryStoppingPoint('rotate-cw')\n\t\t\t\t\tconst offset = editor.getSelectionRotation() % (HALF_PI / 2)\n\t\t\t\t\tconst dontUseOffset = approximately(offset, 0) || approximately(offset, HALF_PI / 2)\n\t\t\t\t\tconst selectedShapeIds = editor.getSelectedShapeIds()\n\t\t\t\t\teditor.rotateShapesBy(selectedShapeIds, HALF_PI / 2 - (dontUseOffset ? 0 : offset))\n\t\t\t\t\tkickoutOccludedShapes(editor, selectedShapeIds)\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'rotate-ccw',\n\t\t\t\tlabel: 'action.rotate-ccw',\n\t\t\t\ticon: 'rotate-ccw',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\ttrackEvent('rotate-ccw', { source })\n\t\t\t\t\teditor.markHistoryStoppingPoint('rotate-ccw')\n\t\t\t\t\tconst offset = editor.getSelectionRotation() % (HALF_PI / 2)\n\t\t\t\t\tconst offsetCloseToZero = approximately(offset, 0)\n\t\t\t\t\tconst selectedShapeIds = editor.getSelectedShapeIds()\n\t\t\t\t\teditor.rotateShapesBy(selectedShapeIds, offsetCloseToZero ? -(HALF_PI / 2) : -offset)\n\t\t\t\t\tkickoutOccludedShapes(editor, selectedShapeIds)\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'zoom-in',\n\t\t\t\tlabel: 'action.zoom-in',\n\t\t\t\tkbd: '$=,=',\n\t\t\t\treadonlyOk: true,\n\t\t\t\tonSelect(source) {\n\t\t\t\t\ttrackEvent('zoom-in', { source })\n\t\t\t\t\teditor.zoomIn(undefined, {\n\t\t\t\t\t\tanimation: { duration: editor.options.animationMediumMs },\n\t\t\t\t\t})\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'zoom-out',\n\t\t\t\tlabel: 'action.zoom-out',\n\t\t\t\tkbd: '$-,-',\n\t\t\t\treadonlyOk: true,\n\t\t\t\tonSelect(source) {\n\t\t\t\t\ttrackEvent('zoom-out', { source })\n\t\t\t\t\teditor.zoomOut(undefined, {\n\t\t\t\t\t\tanimation: { duration: editor.options.animationMediumMs },\n\t\t\t\t\t})\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'zoom-to-100',\n\t\t\t\tlabel: 'action.zoom-to-100',\n\t\t\t\ticon: 'reset-zoom',\n\t\t\t\tkbd: '!0',\n\t\t\t\treadonlyOk: true,\n\t\t\t\tonSelect(source) {\n\t\t\t\t\ttrackEvent('reset-zoom', { source })\n\t\t\t\t\teditor.resetZoom(undefined, {\n\t\t\t\t\t\tanimation: { duration: editor.options.animationMediumMs },\n\t\t\t\t\t})\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'zoom-to-fit',\n\t\t\t\tlabel: 'action.zoom-to-fit',\n\t\t\t\tkbd: '!1',\n\t\t\t\treadonlyOk: true,\n\t\t\t\tonSelect(source) {\n\t\t\t\t\ttrackEvent('zoom-to-fit', { source })\n\t\t\t\t\teditor.zoomToFit({ animation: { duration: editor.options.animationMediumMs } })\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'zoom-to-selection',\n\t\t\t\tlabel: 'action.zoom-to-selection',\n\t\t\t\tkbd: '!2',\n\t\t\t\treadonlyOk: true,\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tif (!canApplySelectionAction()) return\n\t\t\t\t\tif (mustGoBackToSelectToolFirst()) return\n\n\t\t\t\t\ttrackEvent('zoom-to-selection', { source })\n\t\t\t\t\teditor.zoomToSelection({ animation: { duration: editor.options.animationMediumMs } })\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'toggle-snap-mode',\n\t\t\t\tlabel: {\n\t\t\t\t\tdefault: 'action.toggle-snap-mode',\n\t\t\t\t\tmenu: 'action.toggle-snap-mode.menu',\n\t\t\t\t},\n\t\t\t\tonSelect(source) {\n\t\t\t\t\ttrackEvent('toggle-snap-mode', { source })\n\t\t\t\t\teditor.user.updateUserPreferences({ isSnapMode: !editor.user.getIsSnapMode() })\n\t\t\t\t},\n\t\t\t\tcheckbox: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'toggle-dark-mode',\n\t\t\t\tlabel: {\n\t\t\t\t\tdefault: 'action.toggle-dark-mode',\n\t\t\t\t\tmenu: 'action.toggle-dark-mode.menu',\n\t\t\t\t},\n\t\t\t\tkbd: '$/',\n\t\t\t\treadonlyOk: true,\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tconst value = editor.user.getIsDarkMode() ? 'light' : 'dark'\n\t\t\t\t\ttrackEvent('color-scheme', { source, value })\n\t\t\t\t\teditor.user.updateUserPreferences({\n\t\t\t\t\t\tcolorScheme: value,\n\t\t\t\t\t})\n\t\t\t\t},\n\t\t\t\tcheckbox: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'toggle-wrap-mode',\n\t\t\t\tlabel: {\n\t\t\t\t\tdefault: 'action.toggle-wrap-mode',\n\t\t\t\t\tmenu: 'action.toggle-wrap-mode.menu',\n\t\t\t\t},\n\t\t\t\treadonlyOk: true,\n\t\t\t\tonSelect(source) {\n\t\t\t\t\ttrackEvent('toggle-wrap-mode', { source })\n\t\t\t\t\teditor.user.updateUserPreferences({\n\t\t\t\t\t\tisWrapMode: !editor.user.getIsWrapMode(),\n\t\t\t\t\t})\n\t\t\t\t},\n\t\t\t\tcheckbox: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'toggle-dynamic-size-mode',\n\t\t\t\tlabel: {\n\t\t\t\t\tdefault: 'action.toggle-dynamic-size-mode',\n\t\t\t\t\tmenu: 'action.toggle-dynamic-size-mode.menu',\n\t\t\t\t},\n\t\t\t\treadonlyOk: false,\n\t\t\t\tonSelect(source) {\n\t\t\t\t\ttrackEvent('toggle-dynamic-size-mode', { source })\n\t\t\t\t\teditor.user.updateUserPreferences({\n\t\t\t\t\t\tisDynamicSizeMode: !editor.user.getIsDynamicResizeMode(),\n\t\t\t\t\t})\n\t\t\t\t},\n\t\t\t\tcheckbox: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'toggle-paste-at-cursor',\n\t\t\t\tlabel: {\n\t\t\t\t\tdefault: 'action.toggle-paste-at-cursor',\n\t\t\t\t\tmenu: 'action.toggle-paste-at-cursor.menu',\n\t\t\t\t},\n\t\t\t\treadonlyOk: false,\n\t\t\t\tonSelect(source) {\n\t\t\t\t\ttrackEvent('toggle-paste-at-cursor', { source })\n\t\t\t\t\teditor.user.updateUserPreferences({\n\t\t\t\t\t\tisPasteAtCursorMode: !editor.user.getIsPasteAtCursorMode(),\n\t\t\t\t\t})\n\t\t\t\t},\n\t\t\t\tcheckbox: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'toggle-reduce-motion',\n\t\t\t\tlabel: {\n\t\t\t\t\tdefault: 'action.toggle-reduce-motion',\n\t\t\t\t\tmenu: 'action.toggle-reduce-motion.menu',\n\t\t\t\t},\n\t\t\t\treadonlyOk: true,\n\t\t\t\tonSelect(source) {\n\t\t\t\t\ttrackEvent('toggle-reduce-motion', { source })\n\t\t\t\t\teditor.user.updateUserPreferences({\n\t\t\t\t\t\tanimationSpeed: editor.user.getAnimationSpeed() === 0 ? 1 : 0,\n\t\t\t\t\t})\n\t\t\t\t},\n\t\t\t\tcheckbox: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'toggle-edge-scrolling',\n\t\t\t\tlabel: {\n\t\t\t\t\tdefault: 'action.toggle-edge-scrolling',\n\t\t\t\t\tmenu: 'action.toggle-edge-scrolling.menu',\n\t\t\t\t},\n\t\t\t\treadonlyOk: true,\n\t\t\t\tonSelect(source) {\n\t\t\t\t\ttrackEvent('toggle-edge-scrolling', { source })\n\t\t\t\t\teditor.user.updateUserPreferences({\n\t\t\t\t\t\tedgeScrollSpeed: editor.user.getEdgeScrollSpeed() === 0 ? 1 : 0,\n\t\t\t\t\t})\n\t\t\t\t},\n\t\t\t\tcheckbox: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'toggle-transparent',\n\t\t\t\tlabel: {\n\t\t\t\t\tdefault: 'action.toggle-transparent',\n\t\t\t\t\tmenu: 'action.toggle-transparent.menu',\n\t\t\t\t\t['context-menu']: 'action.toggle-transparent.context-menu',\n\t\t\t\t},\n\t\t\t\treadonlyOk: true,\n\t\t\t\tonSelect(source) {\n\t\t\t\t\ttrackEvent('toggle-transparent', { source })\n\t\t\t\t\teditor.updateInstanceState({\n\t\t\t\t\t\texportBackground: !editor.getInstanceState().exportBackground,\n\t\t\t\t\t})\n\t\t\t\t},\n\t\t\t\tcheckbox: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'toggle-tool-lock',\n\t\t\t\tlabel: {\n\t\t\t\t\tdefault: 'action.toggle-tool-lock',\n\t\t\t\t\tmenu: 'action.toggle-tool-lock.menu',\n\t\t\t\t},\n\t\t\t\tkbd: 'q',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\ttrackEvent('toggle-tool-lock', { source })\n\t\t\t\t\teditor.updateInstanceState({ isToolLocked: !editor.getInstanceState().isToolLocked })\n\t\t\t\t},\n\t\t\t\tcheckbox: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'unlock-all',\n\t\t\t\tlabel: 'action.unlock-all',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\ttrackEvent('unlock-all', { source })\n\t\t\t\t\tconst updates = [] as TLShapePartial[]\n\t\t\t\t\tfor (const shape of editor.getCurrentPageShapes()) {\n\t\t\t\t\t\tif (shape.isLocked) {\n\t\t\t\t\t\t\tupdates.push({ id: shape.id, type: shape.type, isLocked: false })\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif (updates.length > 0) {\n\t\t\t\t\t\teditor.updateShapes(updates)\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'toggle-focus-mode',\n\t\t\t\tlabel: {\n\t\t\t\t\tdefault: 'action.toggle-focus-mode',\n\t\t\t\t\tmenu: 'action.toggle-focus-mode.menu',\n\t\t\t\t},\n\t\t\t\treadonlyOk: true,\n\t\t\t\tkbd: '$.',\n\t\t\t\tcheckbox: true,\n\t\t\t\tonSelect(source) {\n\t\t\t\t\t// this needs to be deferred because it causes the menu\n\t\t\t\t\t// UI to unmount which puts us in a dodgy state\n\t\t\t\t\teditor.timers.requestAnimationFrame(() => {\n\t\t\t\t\t\teditor.run(() => {\n\t\t\t\t\t\t\ttrackEvent('toggle-focus-mode', { source })\n\t\t\t\t\t\t\thelpers.clearDialogs()\n\t\t\t\t\t\t\thelpers.clearToasts()\n\t\t\t\t\t\t\teditor.updateInstanceState({ isFocusMode: !editor.getInstanceState().isFocusMode })\n\t\t\t\t\t\t})\n\t\t\t\t\t})\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'toggle-grid',\n\t\t\t\tlabel: {\n\t\t\t\t\tdefault: 'action.toggle-grid',\n\t\t\t\t\tmenu: 'action.toggle-grid.menu',\n\t\t\t\t},\n\t\t\t\treadonlyOk: true,\n\t\t\t\tkbd: \"$'\",\n\t\t\t\tonSelect(source) {\n\t\t\t\t\ttrackEvent('toggle-grid-mode', { source })\n\t\t\t\t\teditor.updateInstanceState({ isGridMode: !editor.getInstanceState().isGridMode })\n\t\t\t\t},\n\t\t\t\tcheckbox: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'toggle-debug-mode',\n\t\t\t\tlabel: {\n\t\t\t\t\tdefault: 'action.toggle-debug-mode',\n\t\t\t\t\tmenu: 'action.toggle-debug-mode.menu',\n\t\t\t\t},\n\t\t\t\treadonlyOk: true,\n\t\t\t\tonSelect(source) {\n\t\t\t\t\ttrackEvent('toggle-debug-mode', { source })\n\t\t\t\t\teditor.updateInstanceState({\n\t\t\t\t\t\tisDebugMode: !editor.getInstanceState().isDebugMode,\n\t\t\t\t\t})\n\t\t\t\t},\n\t\t\t\tcheckbox: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'print',\n\t\t\t\tlabel: 'action.print',\n\t\t\t\tkbd: '$p',\n\t\t\t\treadonlyOk: true,\n\t\t\t\tonSelect(source) {\n\t\t\t\t\ttrackEvent('print', { source })\n\t\t\t\t\thelpers.printSelectionOrPages()\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'exit-pen-mode',\n\t\t\t\tlabel: 'action.exit-pen-mode',\n\t\t\t\ticon: 'cross-2',\n\t\t\t\treadonlyOk: true,\n\t\t\t\tonSelect(source) {\n\t\t\t\t\ttrackEvent('exit-pen-mode', { source })\n\t\t\t\t\teditor.updateInstanceState({ isPenMode: false })\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'stop-following',\n\t\t\t\tlabel: 'action.stop-following',\n\t\t\t\ticon: 'cross-2',\n\t\t\t\treadonlyOk: true,\n\t\t\t\tonSelect(source) {\n\t\t\t\t\ttrackEvent('stop-following', { source })\n\t\t\t\t\teditor.stopFollowingUser()\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'back-to-content',\n\t\t\t\tlabel: 'action.back-to-content',\n\t\t\t\ticon: 'arrow-left',\n\t\t\t\treadonlyOk: true,\n\t\t\t\tonSelect(source) {\n\t\t\t\t\ttrackEvent('zoom-to-content', { source })\n\t\t\t\t\tconst bounds = editor.getSelectionPageBounds() ?? editor.getCurrentPageBounds()\n\t\t\t\t\tif (!bounds) return\n\t\t\t\t\teditor.zoomToBounds(bounds, {\n\t\t\t\t\t\ttargetZoom: Math.min(1, editor.getZoomLevel()),\n\t\t\t\t\t\tanimation: { duration: 220 },\n\t\t\t\t\t})\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'toggle-lock',\n\t\t\t\tlabel: 'action.toggle-lock',\n\t\t\t\tkbd: '!l',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\teditor.markHistoryStoppingPoint('locking')\n\t\t\t\t\ttrackEvent('toggle-lock', { source })\n\t\t\t\t\teditor.toggleLock(editor.getSelectedShapeIds())\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'move-to-new-page',\n\t\t\t\tlabel: 'context.pages.new-page',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tconst newPageId = PageRecordType.createId()\n\t\t\t\t\tconst ids = editor.getSelectedShapeIds()\n\t\t\t\t\teditor.run(() => {\n\t\t\t\t\t\teditor.markHistoryStoppingPoint('move_shapes_to_page')\n\t\t\t\t\t\teditor.createPage({\n\t\t\t\t\t\t\tname: helpers.msg('page-menu.new-page-initial-name'),\n\t\t\t\t\t\t\tid: newPageId,\n\t\t\t\t\t\t})\n\t\t\t\t\t\teditor.moveShapesToPage(ids, newPageId)\n\t\t\t\t\t})\n\t\t\t\t\ttrackEvent('move-to-new-page', { source })\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'select-white-color',\n\t\t\t\tlabel: 'color-style.white',\n\t\t\t\tkbd: '?t',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tconst style = DefaultColorStyle\n\t\t\t\t\teditor.run(() => {\n\t\t\t\t\t\teditor.markHistoryStoppingPoint('change-color')\n\t\t\t\t\t\tif (editor.isIn('select')) {\n\t\t\t\t\t\t\teditor.setStyleForSelectedShapes(style, 'white')\n\t\t\t\t\t\t}\n\t\t\t\t\t\teditor.setStyleForNextShapes(style, 'white')\n\t\t\t\t\t})\n\t\t\t\t\ttrackEvent('set-style', { source, id: style.id, value: 'white' })\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'select-fill-fill',\n\t\t\t\tlabel: 'fill-style.fill',\n\t\t\t\tkbd: '?f',\n\t\t\t\tonSelect(source) {\n\t\t\t\t\tconst style = DefaultFillStyle\n\t\t\t\t\teditor.run(() => {\n\t\t\t\t\t\teditor.markHistoryStoppingPoint('change-fill')\n\t\t\t\t\t\tif (editor.isIn('select')) {\n\t\t\t\t\t\t\teditor.setStyleForSelectedShapes(style, 'fill')\n\t\t\t\t\t\t}\n\t\t\t\t\t\teditor.setStyleForNextShapes(style, 'fill')\n\t\t\t\t\t})\n\t\t\t\t\ttrackEvent('set-style', { source, id: style.id, value: 'fill' })\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 'flatten-to-image',\n\t\t\t\tlabel: 'action.flatten-to-image',\n\t\t\t\tkbd: '!f',\n\t\t\t\tonSelect: async (source) => {\n\t\t\t\t\tconst ids = editor.getSelectedShapeIds()\n\t\t\t\t\tif (ids.length === 0) return\n\n\t\t\t\t\teditor.markHistoryStoppingPoint('flattening to image')\n\t\t\t\t\ttrackEvent('flatten-to-image', { source })\n\n\t\t\t\t\tconst newShapeIds = await flattenShapesToImages(\n\t\t\t\t\t\teditor,\n\t\t\t\t\t\tids,\n\t\t\t\t\t\teditor.options.flattenImageBoundsExpand\n\t\t\t\t\t)\n\n\t\t\t\t\tif (newShapeIds?.length) {\n\t\t\t\t\t\teditor.setSelectedShapes(newShapeIds)\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t},\n\t\t]\n\n\t\tif (showCollaborationUi) {\n\t\t\tactionItems.push({\n\t\t\t\tid: 'open-cursor-chat',\n\t\t\t\tlabel: 'action.open-cursor-chat',\n\t\t\t\treadonlyOk: true,\n\t\t\t\tkbd: '/',\n\t\t\t\tonSelect(source: any) {\n\t\t\t\t\ttrackEvent('open-cursor-chat', { source })\n\n\t\t\t\t\t// Don't open cursor chat if we're on a touch device\n\t\t\t\t\tif (editor.getInstanceState().isCoarsePointer) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\t// wait a frame before opening as otherwise the open context menu will close it\n\t\t\t\t\teditor.timers.requestAnimationFrame(() => {\n\t\t\t\t\t\teditor.updateInstanceState({ isChatting: true })\n\t\t\t\t\t})\n\t\t\t\t},\n\t\t\t})\n\t\t}\n\n\t\tconst actions = makeActions(actionItems)\n\n\t\tif (overrides) {\n\t\t\treturn overrides(editor, actions, helpers)\n\t\t}\n\n\t\treturn actions\n\t}, [helpers, editor, trackEvent, overrides, defaultDocumentName, showCollaborationUi])\n\n\treturn <ActionsContext.Provider value={asActions(actions)}>{children}</ActionsContext.Provider>\n}\n\n/** @public */\nexport function useActions() {\n\tconst ctx = React.useContext(ActionsContext)\n\n\tif (!ctx) {\n\t\tthrow new Error('useTools must be used within a ToolProvider')\n\t}\n\n\treturn ctx\n}\n\nfunction asActions<T extends Record<string, TLUiActionItem>>(actions: T) {\n\treturn actions as Record<keyof typeof actions, TLUiActionItem>\n}\n\n/** @public */\nexport function unwrapLabel(label?: TLUiActionItem['label'], menuType?: string) {\n\treturn label\n\t\t? typeof label === 'string'\n\t\t\t? label\n\t\t\t: menuType\n\t\t\t\t? label[menuType] ?? label['default']\