[3/5] Automatically enable multiplayer UI when using demo sync (#4119)
Adds a new `multiplayerStatus` store prop. This can either be `null` (indicating this isn't a multiplayer store) or a signal containing `online` or `offline` indicating that it is. We move a bunch of previously dotcom specific UI into `tldraw` and use this new prop to turn it on or off by default. closes TLD-2611 ### Change type - [x] `improvement`
This commit is contained in:
parent
627c84c2af
commit
7273eb3101
37 changed files with 573 additions and 519 deletions
|
@ -1,15 +1,14 @@
|
||||||
import {
|
import {
|
||||||
ChangeEvent,
|
ChangeEvent,
|
||||||
KeyboardEvent,
|
KeyboardEvent,
|
||||||
ReactNode,
|
|
||||||
SetStateAction,
|
SetStateAction,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useLayoutEffect,
|
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import {
|
import {
|
||||||
|
CenteredTopPanelContainer,
|
||||||
OfflineIndicator,
|
OfflineIndicator,
|
||||||
TLUiTranslationKey,
|
TLUiTranslationKey,
|
||||||
TldrawUiButton,
|
TldrawUiButton,
|
||||||
|
@ -38,12 +37,7 @@ interface NameState {
|
||||||
readonly isEditing: boolean
|
readonly isEditing: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_TITLE_WIDTH_PX = 420
|
|
||||||
const BUTTON_WIDTH = 44
|
const BUTTON_WIDTH = 44
|
||||||
const STYLE_PANEL_WIDTH = 148
|
|
||||||
const MARGIN_BETWEEN_ZONES = 12
|
|
||||||
// the maximum amount the people menu will extend from the style panel
|
|
||||||
const SQUEEZE_FACTOR = 52
|
|
||||||
|
|
||||||
export const DocumentTopZone = track(function DocumentTopZone({
|
export const DocumentTopZone = track(function DocumentTopZone({
|
||||||
isOffline,
|
isOffline,
|
||||||
|
@ -53,10 +47,10 @@ export const DocumentTopZone = track(function DocumentTopZone({
|
||||||
const isDocumentNameVisible = useBreakpoint() >= 4
|
const isDocumentNameVisible = useBreakpoint() >= 4
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DocumentTopZoneContainer>
|
<CenteredTopPanelContainer ignoreRightWidth={BUTTON_WIDTH}>
|
||||||
{isDocumentNameVisible && <DocumentNameInner />}
|
{isDocumentNameVisible && <DocumentNameInner />}
|
||||||
{isOffline && <OfflineIndicator />}
|
{isOffline && <OfflineIndicator />}
|
||||||
</DocumentTopZoneContainer>
|
</CenteredTopPanelContainer>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -135,85 +129,6 @@ export const DocumentNameInner = track(function DocumentNameInner() {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
function DocumentTopZoneContainer({ children }: { children: ReactNode }) {
|
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
|
||||||
const breakpoint = useBreakpoint()
|
|
||||||
|
|
||||||
const updateLayout = useCallback(() => {
|
|
||||||
const element = ref.current
|
|
||||||
if (!element) return
|
|
||||||
|
|
||||||
const layoutTop = element.parentElement!.parentElement!
|
|
||||||
const leftPanel = layoutTop.querySelector('.tlui-layout__top__left')! as HTMLElement
|
|
||||||
const rightPanel = layoutTop.querySelector('.tlui-layout__top__right')! as HTMLElement
|
|
||||||
|
|
||||||
const totalWidth = layoutTop.offsetWidth
|
|
||||||
const leftWidth = leftPanel.offsetWidth
|
|
||||||
const rightWidth = rightPanel.offsetWidth
|
|
||||||
|
|
||||||
// Ignore button width
|
|
||||||
const selfWidth = element.offsetWidth - BUTTON_WIDTH
|
|
||||||
|
|
||||||
let xCoordIfCentered = (totalWidth - selfWidth) / 2
|
|
||||||
|
|
||||||
// Prevent subpixel bullsh
|
|
||||||
if (totalWidth % 2 !== 0) {
|
|
||||||
xCoordIfCentered -= 0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
const xCoordIfLeftAligned = leftWidth + MARGIN_BETWEEN_ZONES
|
|
||||||
|
|
||||||
const left = element.offsetLeft
|
|
||||||
const maxWidth = Math.min(
|
|
||||||
totalWidth - rightWidth - leftWidth - 2 * MARGIN_BETWEEN_ZONES,
|
|
||||||
MAX_TITLE_WIDTH_PX
|
|
||||||
)
|
|
||||||
const xCoord = Math.max(xCoordIfCentered, xCoordIfLeftAligned) - left
|
|
||||||
|
|
||||||
// Squeeze the title if the right panel is too wide on small screens
|
|
||||||
if (rightPanel.offsetWidth > STYLE_PANEL_WIDTH && breakpoint <= 6) {
|
|
||||||
element.style.setProperty('max-width', maxWidth - SQUEEZE_FACTOR + 'px')
|
|
||||||
} else {
|
|
||||||
element.style.setProperty('max-width', maxWidth + 'px')
|
|
||||||
}
|
|
||||||
element.style.setProperty('transform', `translate(${xCoord}px, 0px)`)
|
|
||||||
}, [breakpoint])
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
const element = ref.current
|
|
||||||
if (!element) return
|
|
||||||
|
|
||||||
const layoutTop = element.parentElement!.parentElement!
|
|
||||||
const leftPanel = layoutTop.querySelector('.tlui-layout__top__left')! as HTMLElement
|
|
||||||
const rightPanel = layoutTop.querySelector('.tlui-layout__top__right')! as HTMLElement
|
|
||||||
|
|
||||||
// Update layout when the things change
|
|
||||||
const observer = new ResizeObserver(updateLayout)
|
|
||||||
observer.observe(leftPanel)
|
|
||||||
observer.observe(rightPanel)
|
|
||||||
observer.observe(layoutTop)
|
|
||||||
observer.observe(element)
|
|
||||||
|
|
||||||
// Also update on first layout
|
|
||||||
updateLayout()
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
observer.disconnect()
|
|
||||||
}
|
|
||||||
}, [updateLayout])
|
|
||||||
|
|
||||||
// Update after every render, too
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
updateLayout()
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={ref} className="tlui-top-zone__container">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const DocumentNameEditor = track(function DocumentNameEditor({
|
const DocumentNameEditor = track(function DocumentNameEditor({
|
||||||
state,
|
state,
|
||||||
setState,
|
setState,
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import { ROOM_OPEN_MODE, RoomOpenModeToPath, type RoomOpenMode } from '@tldraw/dotcom-shared'
|
import { ROOM_OPEN_MODE, RoomOpenModeToPath, type RoomOpenMode } from '@tldraw/dotcom-shared'
|
||||||
import { useMultiplayerSync } from '@tldraw/sync-react'
|
import { useMultiplayerSync } from '@tldraw/sync-react'
|
||||||
import { useCallback, useEffect } from 'react'
|
import { useCallback } from 'react'
|
||||||
import {
|
import {
|
||||||
DefaultContextMenu,
|
|
||||||
DefaultContextMenuContent,
|
|
||||||
DefaultHelpMenu,
|
DefaultHelpMenu,
|
||||||
DefaultHelpMenuContent,
|
DefaultHelpMenuContent,
|
||||||
DefaultKeyboardShortcutsDialog,
|
DefaultKeyboardShortcutsDialog,
|
||||||
|
@ -13,48 +11,42 @@ import {
|
||||||
Editor,
|
Editor,
|
||||||
ExportFileContentSubMenu,
|
ExportFileContentSubMenu,
|
||||||
ExtrasGroup,
|
ExtrasGroup,
|
||||||
|
PeopleMenu,
|
||||||
PreferencesGroup,
|
PreferencesGroup,
|
||||||
TLComponents,
|
TLComponents,
|
||||||
Tldraw,
|
Tldraw,
|
||||||
|
TldrawUiButton,
|
||||||
|
TldrawUiButtonIcon,
|
||||||
|
TldrawUiButtonLabel,
|
||||||
TldrawUiMenuGroup,
|
TldrawUiMenuGroup,
|
||||||
TldrawUiMenuItem,
|
TldrawUiMenuItem,
|
||||||
ViewSubmenu,
|
ViewSubmenu,
|
||||||
atom,
|
assertExists,
|
||||||
useActions,
|
useActions,
|
||||||
|
useEditor,
|
||||||
|
useTranslation,
|
||||||
useValue,
|
useValue,
|
||||||
} from 'tldraw'
|
} from 'tldraw'
|
||||||
import { UrlStateParams, useUrlState } from '../hooks/useUrlState'
|
import { UrlStateParams, useUrlState } from '../hooks/useUrlState'
|
||||||
import { assetUrls } from '../utils/assetUrls'
|
import { assetUrls } from '../utils/assetUrls'
|
||||||
import { MULTIPLAYER_SERVER } from '../utils/config'
|
import { MULTIPLAYER_SERVER } from '../utils/config'
|
||||||
import { CursorChatMenuItem } from '../utils/context-menu/CursorChatMenuItem'
|
|
||||||
import { createAssetFromUrl } from '../utils/createAssetFromUrl'
|
import { createAssetFromUrl } from '../utils/createAssetFromUrl'
|
||||||
import { multiplayerAssetStore } from '../utils/multiplayerAssetStore'
|
import { multiplayerAssetStore } from '../utils/multiplayerAssetStore'
|
||||||
import { useSharing } from '../utils/sharing'
|
import { useSharing } from '../utils/sharing'
|
||||||
import { CURSOR_CHAT_ACTION, useCursorChat } from '../utils/useCursorChat'
|
|
||||||
import { OPEN_FILE_ACTION, SAVE_FILE_COPY_ACTION, useFileSystem } from '../utils/useFileSystem'
|
import { OPEN_FILE_ACTION, SAVE_FILE_COPY_ACTION, useFileSystem } from '../utils/useFileSystem'
|
||||||
import { useHandleUiEvents } from '../utils/useHandleUiEvent'
|
import { useHandleUiEvents } from '../utils/useHandleUiEvent'
|
||||||
import { CursorChatBubble } from './CursorChatBubble'
|
|
||||||
import { DocumentTopZone } from './DocumentName/DocumentName'
|
import { DocumentTopZone } from './DocumentName/DocumentName'
|
||||||
import { MultiplayerFileMenu } from './FileMenu'
|
import { MultiplayerFileMenu } from './FileMenu'
|
||||||
import { Links } from './Links'
|
import { Links } from './Links'
|
||||||
import { PeopleMenu } from './PeopleMenu/PeopleMenu'
|
|
||||||
import { ShareMenu } from './ShareMenu'
|
import { ShareMenu } from './ShareMenu'
|
||||||
import { SneakyOnDropOverride } from './SneakyOnDropOverride'
|
import { SneakyOnDropOverride } from './SneakyOnDropOverride'
|
||||||
import { StoreErrorScreen } from './StoreErrorScreen'
|
import { StoreErrorScreen } from './StoreErrorScreen'
|
||||||
import { ThemeUpdater } from './ThemeUpdater/ThemeUpdater'
|
import { ThemeUpdater } from './ThemeUpdater/ThemeUpdater'
|
||||||
|
|
||||||
const shittyOfflineAtom = atom('shitty offline atom', false)
|
|
||||||
|
|
||||||
const components: TLComponents = {
|
const components: TLComponents = {
|
||||||
ErrorFallback: ({ error }) => {
|
ErrorFallback: ({ error }) => {
|
||||||
throw error
|
throw error
|
||||||
},
|
},
|
||||||
ContextMenu: (props) => (
|
|
||||||
<DefaultContextMenu {...props}>
|
|
||||||
<CursorChatMenuItem />
|
|
||||||
<DefaultContextMenuContent />
|
|
||||||
</DefaultContextMenu>
|
|
||||||
),
|
|
||||||
HelpMenu: () => (
|
HelpMenu: () => (
|
||||||
<DefaultHelpMenu>
|
<DefaultHelpMenu>
|
||||||
<TldrawUiMenuGroup id="help">
|
<TldrawUiMenuGroup id="help">
|
||||||
|
@ -83,20 +75,41 @@ const components: TLComponents = {
|
||||||
<TldrawUiMenuItem {...actions[OPEN_FILE_ACTION]} />
|
<TldrawUiMenuItem {...actions[OPEN_FILE_ACTION]} />
|
||||||
</TldrawUiMenuGroup>
|
</TldrawUiMenuGroup>
|
||||||
<DefaultKeyboardShortcutsDialogContent />
|
<DefaultKeyboardShortcutsDialogContent />
|
||||||
<TldrawUiMenuGroup label="shortcuts-dialog.collaboration" id="collaboration">
|
|
||||||
<TldrawUiMenuItem {...actions[CURSOR_CHAT_ACTION]} />
|
|
||||||
</TldrawUiMenuGroup>
|
|
||||||
</DefaultKeyboardShortcutsDialog>
|
</DefaultKeyboardShortcutsDialog>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
TopPanel: () => {
|
TopPanel: () => {
|
||||||
const isOffline = useValue('offline', () => shittyOfflineAtom.get(), [])
|
const editor = useEditor()
|
||||||
|
const isOffline = useValue(
|
||||||
|
'offline',
|
||||||
|
() => {
|
||||||
|
const status = assertExists(
|
||||||
|
editor.store.props.multiplayerStatus,
|
||||||
|
'should be used with multiplayer store'
|
||||||
|
)
|
||||||
|
return status.get() === 'offline'
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
return <DocumentTopZone isOffline={isOffline} />
|
return <DocumentTopZone isOffline={isOffline} />
|
||||||
},
|
},
|
||||||
SharePanel: () => {
|
SharePanel: () => {
|
||||||
|
const editor = useEditor()
|
||||||
|
const msg = useTranslation()
|
||||||
return (
|
return (
|
||||||
<div className="tlui-share-zone" draggable={false}>
|
<div className="tlui-share-zone" draggable={false}>
|
||||||
<PeopleMenu />
|
<PeopleMenu>
|
||||||
|
<div className="tlui-people-menu__section">
|
||||||
|
<TldrawUiButton
|
||||||
|
type="menu"
|
||||||
|
data-testid="people-menu.invite"
|
||||||
|
onClick={() => editor.addOpenMenu('share menu')}
|
||||||
|
>
|
||||||
|
<TldrawUiButtonLabel>{msg('people-menu.invite')}</TldrawUiButtonLabel>
|
||||||
|
<TldrawUiButtonIcon icon="plus" />
|
||||||
|
</TldrawUiButton>
|
||||||
|
</div>
|
||||||
|
</PeopleMenu>
|
||||||
<ShareMenu />
|
<ShareMenu />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -118,15 +131,8 @@ export function MultiplayerEditor({
|
||||||
assets: multiplayerAssetStore,
|
assets: multiplayerAssetStore,
|
||||||
})
|
})
|
||||||
|
|
||||||
const isOffline =
|
|
||||||
storeWithStatus.status === 'synced-remote' && storeWithStatus.connectionStatus === 'offline'
|
|
||||||
useEffect(() => {
|
|
||||||
shittyOfflineAtom.set(isOffline)
|
|
||||||
}, [isOffline])
|
|
||||||
|
|
||||||
const sharingUiOverrides = useSharing()
|
const sharingUiOverrides = useSharing()
|
||||||
const fileSystemUiOverrides = useFileSystem({ isMultiplayer: true })
|
const fileSystemUiOverrides = useFileSystem({ isMultiplayer: true })
|
||||||
const cursorChatOverrides = useCursorChat()
|
|
||||||
const isReadonly =
|
const isReadonly =
|
||||||
roomOpenMode === ROOM_OPEN_MODE.READ_ONLY || roomOpenMode === ROOM_OPEN_MODE.READ_ONLY_LEGACY
|
roomOpenMode === ROOM_OPEN_MODE.READ_ONLY || roomOpenMode === ROOM_OPEN_MODE.READ_ONLY_LEGACY
|
||||||
|
|
||||||
|
@ -154,14 +160,13 @@ export function MultiplayerEditor({
|
||||||
store={storeWithStatus}
|
store={storeWithStatus}
|
||||||
assetUrls={assetUrls}
|
assetUrls={assetUrls}
|
||||||
onMount={handleMount}
|
onMount={handleMount}
|
||||||
overrides={[sharingUiOverrides, fileSystemUiOverrides, cursorChatOverrides]}
|
overrides={[sharingUiOverrides, fileSystemUiOverrides]}
|
||||||
initialState={isReadonly ? 'hand' : 'select'}
|
initialState={isReadonly ? 'hand' : 'select'}
|
||||||
onUiEvent={handleUiEvent}
|
onUiEvent={handleUiEvent}
|
||||||
components={components}
|
components={components}
|
||||||
inferDarkMode
|
inferDarkMode
|
||||||
>
|
>
|
||||||
<UrlStateSync />
|
<UrlStateSync />
|
||||||
<CursorChatBubble />
|
|
||||||
<SneakyOnDropOverride isMultiplayer />
|
<SneakyOnDropOverride isMultiplayer />
|
||||||
<ThemeUpdater />
|
<ThemeUpdater />
|
||||||
</Tldraw>
|
</Tldraw>
|
||||||
|
|
|
@ -1,86 +1,13 @@
|
||||||
import { useMultiplayerDemo } from '@tldraw/sync-react'
|
import { useMultiplayerDemo } from '@tldraw/sync-react'
|
||||||
import { useCallback, useEffect } from 'react'
|
import { Tldraw } from 'tldraw'
|
||||||
import { DefaultContextMenu, DefaultContextMenuContent, TLComponents, Tldraw, atom } from 'tldraw'
|
|
||||||
import { UrlStateParams, useUrlState } from '../hooks/useUrlState'
|
|
||||||
import { assetUrls } from '../utils/assetUrls'
|
import { assetUrls } from '../utils/assetUrls'
|
||||||
import { CursorChatMenuItem } from '../utils/context-menu/CursorChatMenuItem'
|
|
||||||
import { useCursorChat } from '../utils/useCursorChat'
|
|
||||||
import { useFileSystem } from '../utils/useFileSystem'
|
|
||||||
import { useHandleUiEvents } from '../utils/useHandleUiEvent'
|
|
||||||
import { CursorChatBubble } from './CursorChatBubble'
|
|
||||||
import { PeopleMenu } from './PeopleMenu/PeopleMenu'
|
|
||||||
import { SneakyOnDropOverride } from './SneakyOnDropOverride'
|
|
||||||
import { StoreErrorScreen } from './StoreErrorScreen'
|
|
||||||
import { ThemeUpdater } from './ThemeUpdater/ThemeUpdater'
|
|
||||||
|
|
||||||
const shittyOfflineAtom = atom('shitty offline atom', false)
|
|
||||||
|
|
||||||
const components: TLComponents = {
|
|
||||||
ErrorFallback: ({ error }) => {
|
|
||||||
throw error
|
|
||||||
},
|
|
||||||
ContextMenu: (props) => (
|
|
||||||
<DefaultContextMenu {...props}>
|
|
||||||
<CursorChatMenuItem />
|
|
||||||
<DefaultContextMenuContent />
|
|
||||||
</DefaultContextMenu>
|
|
||||||
),
|
|
||||||
SharePanel: () => {
|
|
||||||
return (
|
|
||||||
<div className="tlui-share-zone" draggable={false}>
|
|
||||||
<PeopleMenu />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TemporaryBemoDevEditor({ slug }: { slug: string }) {
|
export function TemporaryBemoDevEditor({ slug }: { slug: string }) {
|
||||||
const handleUiEvent = useHandleUiEvents()
|
const store = useMultiplayerDemo({ host: 'http://127.0.0.1:8989', roomId: slug })
|
||||||
|
|
||||||
const storeWithStatus = useMultiplayerDemo({ host: 'http://127.0.0.1:8989', roomId: slug })
|
|
||||||
|
|
||||||
const isOffline =
|
|
||||||
storeWithStatus.status === 'synced-remote' && storeWithStatus.connectionStatus === 'offline'
|
|
||||||
useEffect(() => {
|
|
||||||
shittyOfflineAtom.set(isOffline)
|
|
||||||
}, [isOffline])
|
|
||||||
|
|
||||||
const fileSystemUiOverrides = useFileSystem({ isMultiplayer: true })
|
|
||||||
const cursorChatOverrides = useCursorChat()
|
|
||||||
|
|
||||||
if (storeWithStatus.error) {
|
|
||||||
return <StoreErrorScreen error={storeWithStatus.error} />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tldraw__editor">
|
<div className="tldraw__editor">
|
||||||
<Tldraw
|
<Tldraw store={store} assetUrls={assetUrls} inferDarkMode />
|
||||||
store={storeWithStatus}
|
|
||||||
assetUrls={assetUrls}
|
|
||||||
overrides={[fileSystemUiOverrides, cursorChatOverrides]}
|
|
||||||
onUiEvent={handleUiEvent}
|
|
||||||
components={components}
|
|
||||||
inferDarkMode
|
|
||||||
>
|
|
||||||
<UrlStateSync />
|
|
||||||
<CursorChatBubble />
|
|
||||||
<SneakyOnDropOverride isMultiplayer />
|
|
||||||
<ThemeUpdater />
|
|
||||||
</Tldraw>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UrlStateSync() {
|
|
||||||
const syncViewport = useCallback((params: UrlStateParams) => {
|
|
||||||
window.history.replaceState(
|
|
||||||
{},
|
|
||||||
document.title,
|
|
||||||
window.location.pathname + `?v=${params.v}&p=${params.p}`
|
|
||||||
)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useUrlState(syncViewport)
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
import { TldrawUiMenuItem, useActions, useEditor, useValue } from 'tldraw'
|
|
||||||
import { CURSOR_CHAT_ACTION } from '../useCursorChat'
|
|
||||||
|
|
||||||
export function CursorChatMenuItem() {
|
|
||||||
const editor = useEditor()
|
|
||||||
const actions = useActions()
|
|
||||||
const shouldShow = useValue(
|
|
||||||
'show cursor chat',
|
|
||||||
() => editor.getCurrentToolId() === 'select' && !editor.getInstanceState().isCoarsePointer,
|
|
||||||
[editor]
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!shouldShow) return null
|
|
||||||
|
|
||||||
return <TldrawUiMenuItem {...actions[CURSOR_CHAT_ACTION]} />
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
import { useMemo } from 'react'
|
|
||||||
import { TLUiOverrides } from 'tldraw'
|
|
||||||
import { useHandleUiEvents } from './useHandleUiEvent'
|
|
||||||
|
|
||||||
export const CURSOR_CHAT_ACTION = 'open-cursor-chat' as const
|
|
||||||
|
|
||||||
export function useCursorChat(): TLUiOverrides {
|
|
||||||
const handleUiEvent = useHandleUiEvents()
|
|
||||||
return useMemo(
|
|
||||||
(): TLUiOverrides => ({
|
|
||||||
actions(editor, actions) {
|
|
||||||
actions[CURSOR_CHAT_ACTION] = {
|
|
||||||
id: 'open-cursor-chat',
|
|
||||||
label: 'action.open-cursor-chat',
|
|
||||||
readonlyOk: true,
|
|
||||||
kbd: '/',
|
|
||||||
onSelect(source: any) {
|
|
||||||
handleUiEvent('open-cursor-chat', { source })
|
|
||||||
|
|
||||||
// Don't open cursor chat if we're on a touch device
|
|
||||||
if (editor.getInstanceState().isCoarsePointer) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
editor.updateInstanceState({ isChatting: true })
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return actions
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
[handleUiEvent]
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -4,17 +4,6 @@
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tlui-share-zone {
|
|
||||||
padding: 0px 0px 0px 0px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-end;
|
|
||||||
z-index: var(--layer-panels);
|
|
||||||
align-items: center;
|
|
||||||
padding-top: 2px;
|
|
||||||
padding-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tlui-share-zone__connection-status {
|
.tlui-share-zone__connection-status {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -134,183 +123,6 @@
|
||||||
|
|
||||||
/* ------------------- People Menu ------------------- */
|
/* ------------------- People Menu ------------------- */
|
||||||
|
|
||||||
.tlui-people-menu__avatars-button {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
pointer-events: all;
|
|
||||||
border-radius: var(--radius-1);
|
|
||||||
padding-right: 1px;
|
|
||||||
height: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tlui-people-menu__avatars {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tlui-people-menu__avatar {
|
|
||||||
height: 28px;
|
|
||||||
width: 28px;
|
|
||||||
border: 2px solid var(--color-background);
|
|
||||||
background-color: var(--color-low);
|
|
||||||
border-radius: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
position: relative;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--color-selected-contrast);
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tlui-people-menu__avatar:nth-of-type(n + 2) {
|
|
||||||
margin-left: -12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tlui-people-menu__avatars-button[data-state='open'] {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (hover: hover) {
|
|
||||||
.tlui-people-menu__avatars-button:hover .tlui-people-menu__avatar {
|
|
||||||
border-color: var(--color-low);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tlui-people-menu__more {
|
|
||||||
min-width: 0px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--color-text-1);
|
|
||||||
font-family: inherit;
|
|
||||||
padding: 0px 4px;
|
|
||||||
letter-spacing: 1.5;
|
|
||||||
}
|
|
||||||
.tlui-people-menu__more::after {
|
|
||||||
border-radius: var(--radius-2);
|
|
||||||
inset: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tlui-people-menu__wrapper {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 220px;
|
|
||||||
height: fit-content;
|
|
||||||
max-height: 50vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tlui-people-menu__section {
|
|
||||||
position: relative;
|
|
||||||
touch-action: auto;
|
|
||||||
flex-direction: column;
|
|
||||||
max-height: 100%;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
touch-action: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tlui-people-menu__section:not(:last-child) {
|
|
||||||
border-bottom: 1px solid var(--color-divider);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tlui-people-menu__user {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tlui-people-menu__user__color {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tlui-people-menu__user__name {
|
|
||||||
text-align: left;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--color-text-1);
|
|
||||||
max-width: 100%;
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-shrink: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tlui-people-menu__user__label {
|
|
||||||
text-align: left;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--color-text-3);
|
|
||||||
flex-grow: 100;
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin-left: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tlui-people-menu__user__input {
|
|
||||||
flex-grow: 2;
|
|
||||||
height: 100%;
|
|
||||||
padding: 0px;
|
|
||||||
margin: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tlui-people-menu__user > .tlui-input__wrapper {
|
|
||||||
width: auto;
|
|
||||||
display: flex;
|
|
||||||
align-items: auto;
|
|
||||||
flex-grow: 2;
|
|
||||||
gap: 8px;
|
|
||||||
height: 100%;
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tlui-people-menu__item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tlui-people-menu__item__button {
|
|
||||||
padding: 0 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tlui-people-menu__item > .tlui-button__menu {
|
|
||||||
width: auto;
|
|
||||||
display: flex;
|
|
||||||
align-items: auto;
|
|
||||||
justify-content: flex-start;
|
|
||||||
flex-grow: 2;
|
|
||||||
gap: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tlui-people-menu__item__follow {
|
|
||||||
min-width: 44px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tlui-people-menu__item__follow[data-active='true'] .tlui-icon {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tlui-people-menu__item__follow:focus-visible .tlui-icon {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (hover: hover) {
|
|
||||||
.tlui-people-menu__item__follow .tlui-icon {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tlui-people-menu__item__follow:hover .tlui-icon {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tlui-layout[data-breakpoint='0'] .tlui-offline-indicator {
|
.tlui-layout[data-breakpoint='0'] .tlui-offline-indicator {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ const components: Required<TLUiComponents> = {
|
||||||
SharePanel: null,
|
SharePanel: null,
|
||||||
MenuPanel: null,
|
MenuPanel: null,
|
||||||
TopPanel: null,
|
TopPanel: null,
|
||||||
|
CursorChatBubble: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UiComponentsHiddenExample() {
|
export default function UiComponentsHiddenExample() {
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
"refresh-assets": "lazy refresh-assets",
|
"refresh-assets": "lazy refresh-assets",
|
||||||
"dev": "LAZYREPO_PRETTY_OUTPUT=0 lazy run dev --filter='apps/examples' --filter='packages/tldraw' --filter='apps/{bemo-worker,images.tldraw.xyz}'",
|
"dev": "LAZYREPO_PRETTY_OUTPUT=0 lazy run dev --filter='apps/examples' --filter='packages/tldraw' --filter='apps/{bemo-worker,images.tldraw.xyz}'",
|
||||||
"dev-vscode": "code ./apps/vscode/extension && lazy run dev --filter='apps/vscode/{extension,editor}'",
|
"dev-vscode": "code ./apps/vscode/extension && lazy run dev --filter='apps/vscode/{extension,editor}'",
|
||||||
"dev-app": "LAZYREPO_PRETTY_OUTPUT=0 lazy run dev --filter='apps/{dotcom,dotcom-asset-upload,dotcom-worker,images.tldraw.xyz}' --filter='packages/tldraw'",
|
"dev-app": "LAZYREPO_PRETTY_OUTPUT=0 lazy run dev --filter='apps/{dotcom,dotcom-asset-upload,dotcom-worker,images.tldraw.xyz,bemo-worker}' --filter='packages/tldraw'",
|
||||||
"dev-docs": "LAZYREPO_PRETTY_OUTPUT=0 lazy run dev --filter='apps/docs'",
|
"dev-docs": "LAZYREPO_PRETTY_OUTPUT=0 lazy run dev --filter='apps/docs'",
|
||||||
"dev-huppy": "LAZYREPO_PRETTY_OUTPUT=0 lazy run dev --filter 'apps/huppy'",
|
"dev-huppy": "LAZYREPO_PRETTY_OUTPUT=0 lazy run dev --filter 'apps/huppy'",
|
||||||
"build": "lazy build",
|
"build": "lazy build",
|
||||||
|
|
|
@ -487,7 +487,7 @@ export function counterClockwiseAngleDist(a0: number, a1: number): number;
|
||||||
export function createSessionStateSnapshotSignal(store: TLStore): Signal<null | TLSessionStateSnapshot>;
|
export function createSessionStateSnapshotSignal(store: TLStore): Signal<null | TLSessionStateSnapshot>;
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export function createTLStore({ initialData, defaultName, id, assets, onEditorMount, ...rest }?: TLStoreOptions): TLStore;
|
export function createTLStore({ initialData, defaultName, id, assets, onEditorMount, multiplayerStatus, ...rest }?: TLStoreOptions): TLStore;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function createTLUser(opts?: {
|
export function createTLUser(opts?: {
|
||||||
|
@ -3246,6 +3246,7 @@ export interface TLStoreBaseOptions {
|
||||||
assets?: Partial<TLAssetStore>;
|
assets?: Partial<TLAssetStore>;
|
||||||
defaultName?: string;
|
defaultName?: string;
|
||||||
initialData?: SerializedStore<TLRecord>;
|
initialData?: SerializedStore<TLRecord>;
|
||||||
|
multiplayerStatus?: null | Signal<'offline' | 'online'>;
|
||||||
onEditorMount?: (editor: Editor) => (() => void) | void;
|
onEditorMount?: (editor: Editor) => (() => void) | void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Signal } from '@tldraw/state'
|
||||||
import { HistoryEntry, MigrationSequence, SerializedStore, Store, StoreSchema } from '@tldraw/store'
|
import { HistoryEntry, MigrationSequence, SerializedStore, Store, StoreSchema } from '@tldraw/store'
|
||||||
import {
|
import {
|
||||||
SchemaPropsInfo,
|
SchemaPropsInfo,
|
||||||
|
@ -25,6 +26,9 @@ export interface TLStoreBaseOptions {
|
||||||
|
|
||||||
/** Called when the store is connected to an {@link Editor}. */
|
/** Called when the store is connected to an {@link Editor}. */
|
||||||
onEditorMount?: (editor: Editor) => void | (() => void)
|
onEditorMount?: (editor: Editor) => void | (() => void)
|
||||||
|
|
||||||
|
/** Is this store connected to a multiplayer sync server? */
|
||||||
|
multiplayerStatus?: Signal<'online' | 'offline'> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
@ -63,6 +67,7 @@ export function createTLStore({
|
||||||
id,
|
id,
|
||||||
assets,
|
assets,
|
||||||
onEditorMount,
|
onEditorMount,
|
||||||
|
multiplayerStatus,
|
||||||
...rest
|
...rest
|
||||||
}: TLStoreOptions = {}): TLStore {
|
}: TLStoreOptions = {}): TLStore {
|
||||||
const schema =
|
const schema =
|
||||||
|
@ -96,6 +101,7 @@ export function createTLStore({
|
||||||
assert(editor instanceof Editor)
|
assert(editor instanceof Editor)
|
||||||
onEditorMount?.(editor)
|
onEditorMount?.(editor)
|
||||||
},
|
},
|
||||||
|
multiplayerStatus: multiplayerStatus ?? null,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -163,7 +163,7 @@ export interface Signal<Value, Diff = unknown> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export function track<T extends FunctionComponent<any>>(baseComponent: T): T extends React_2.MemoExoticComponent<any> ? T : React_2.MemoExoticComponent<T>;
|
export function track<T extends FunctionComponent<any>>(baseComponent: T): React_2.NamedExoticComponent<React_2.ComponentProps<T>>;
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export function transact<T>(fn: () => T): T;
|
export function transact<T>(fn: () => T): T;
|
||||||
|
|
|
@ -45,7 +45,7 @@ export const ReactForwardRefSymbol = Symbol.for('react.forward_ref')
|
||||||
*/
|
*/
|
||||||
export function track<T extends FunctionComponent<any>>(
|
export function track<T extends FunctionComponent<any>>(
|
||||||
baseComponent: T
|
baseComponent: T
|
||||||
): T extends React.MemoExoticComponent<any> ? T : React.MemoExoticComponent<T> {
|
): React.NamedExoticComponent<React.ComponentProps<T>> {
|
||||||
let compare = null
|
let compare = null
|
||||||
const $$typeof = baseComponent['$$typeof' as keyof typeof baseComponent]
|
const $$typeof = baseComponent['$$typeof' as keyof typeof baseComponent]
|
||||||
if ($$typeof === ReactMemoSymbol) {
|
if ($$typeof === ReactMemoSymbol) {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
TLSyncClient,
|
TLSyncClient,
|
||||||
schema,
|
schema,
|
||||||
} from '@tldraw/sync'
|
} from '@tldraw/sync'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect } from 'react'
|
||||||
import {
|
import {
|
||||||
Editor,
|
Editor,
|
||||||
Signal,
|
Signal,
|
||||||
|
@ -19,9 +19,11 @@ import {
|
||||||
TLUserPreferences,
|
TLUserPreferences,
|
||||||
computed,
|
computed,
|
||||||
createPresenceStateDerivation,
|
createPresenceStateDerivation,
|
||||||
|
createTLStore,
|
||||||
defaultUserPreferences,
|
defaultUserPreferences,
|
||||||
getUserPreferences,
|
getUserPreferences,
|
||||||
useTLStore,
|
uniqueId,
|
||||||
|
useRefState,
|
||||||
useValue,
|
useValue,
|
||||||
} from 'tldraw'
|
} from 'tldraw'
|
||||||
|
|
||||||
|
@ -35,20 +37,26 @@ export type RemoteTLStoreWithStatus = Exclude<
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function useMultiplayerSync(opts: UseMultiplayerSyncOptions): RemoteTLStoreWithStatus {
|
export function useMultiplayerSync(opts: UseMultiplayerSyncOptions): RemoteTLStoreWithStatus {
|
||||||
const [state, setState] = useState<{
|
const [state, setState] = useRefState<{
|
||||||
readyClient?: TLSyncClient<TLRecord, TLStore>
|
readyClient?: TLSyncClient<TLRecord, TLStore>
|
||||||
error?: Error
|
error?: Error
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
const { uri, roomId = 'default', userPreferences: prefs, assets, onEditorMount } = opts
|
const {
|
||||||
|
uri,
|
||||||
const store = useTLStore({ schema, assets, onEditorMount })
|
roomId = 'default',
|
||||||
|
userPreferences: prefs,
|
||||||
|
assets,
|
||||||
|
onEditorMount,
|
||||||
|
trackAnalyticsEvent: track,
|
||||||
|
} = opts
|
||||||
|
|
||||||
const error: NonNullable<typeof state>['error'] = state?.error ?? undefined
|
const error: NonNullable<typeof state>['error'] = state?.error ?? undefined
|
||||||
const track = opts.trackAnalyticsEvent
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (error) return
|
if (error) return
|
||||||
|
|
||||||
|
const storeId = uniqueId()
|
||||||
|
|
||||||
const userPreferences = computed<{ id: string; color: string; name: string }>(
|
const userPreferences = computed<{ id: string; color: string; name: string }>(
|
||||||
'userPreferences',
|
'userPreferences',
|
||||||
() => {
|
() => {
|
||||||
|
@ -65,7 +73,7 @@ export function useMultiplayerSync(opts: UseMultiplayerSyncOptions): RemoteTLSto
|
||||||
// set sessionKey as a query param on the uri
|
// set sessionKey as a query param on the uri
|
||||||
const withParams = new URL(uri)
|
const withParams = new URL(uri)
|
||||||
withParams.searchParams.set('sessionKey', TAB_ID)
|
withParams.searchParams.set('sessionKey', TAB_ID)
|
||||||
withParams.searchParams.set('storeId', store.id)
|
withParams.searchParams.set('storeId', storeId)
|
||||||
return withParams.toString()
|
return withParams.toString()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -81,6 +89,16 @@ export function useMultiplayerSync(opts: UseMultiplayerSyncOptions): RemoteTLSto
|
||||||
|
|
||||||
let didCancel = false
|
let didCancel = false
|
||||||
|
|
||||||
|
const store = createTLStore({
|
||||||
|
id: storeId,
|
||||||
|
schema,
|
||||||
|
assets,
|
||||||
|
onEditorMount,
|
||||||
|
multiplayerStatus: computed('multiplayer status', () =>
|
||||||
|
socket.connectionStatus === 'error' ? 'offline' : socket.connectionStatus
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
const client = new TLSyncClient({
|
const client = new TLSyncClient({
|
||||||
store,
|
store,
|
||||||
socket,
|
socket,
|
||||||
|
@ -115,7 +133,7 @@ export function useMultiplayerSync(opts: UseMultiplayerSyncOptions): RemoteTLSto
|
||||||
client.close()
|
client.close()
|
||||||
socket.close()
|
socket.close()
|
||||||
}
|
}
|
||||||
}, [prefs, roomId, store, uri, error, track])
|
}, [assets, error, onEditorMount, prefs, roomId, setState, track, uri])
|
||||||
|
|
||||||
return useValue<RemoteTLStoreWithStatus>(
|
return useValue<RemoteTLStoreWithStatus>(
|
||||||
'remote synced store',
|
'remote synced store',
|
||||||
|
|
|
@ -30,7 +30,6 @@ import { IndexKey } from '@tldraw/editor';
|
||||||
import { JsonObject } from '@tldraw/editor';
|
import { JsonObject } from '@tldraw/editor';
|
||||||
import { JSX as JSX_2 } from 'react/jsx-runtime';
|
import { JSX as JSX_2 } from 'react/jsx-runtime';
|
||||||
import { LANGUAGES } from '@tldraw/editor';
|
import { LANGUAGES } from '@tldraw/editor';
|
||||||
import { MemoExoticComponent } from 'react';
|
|
||||||
import { MigrationFailureReason } from '@tldraw/editor';
|
import { MigrationFailureReason } from '@tldraw/editor';
|
||||||
import { MigrationSequence } from '@tldraw/editor';
|
import { MigrationSequence } from '@tldraw/editor';
|
||||||
import { NamedExoticComponent } from 'react';
|
import { NamedExoticComponent } from 'react';
|
||||||
|
@ -333,6 +332,25 @@ export interface BreakPointProviderProps {
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
export function buildFromV1Document(editor: Editor, _document: unknown): void;
|
export function buildFromV1Document(editor: Editor, _document: unknown): void;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export function CenteredTopPanelContainer({ maxWidth, ignoreRightWidth, stylePanelWidth, marginBetweenZones, squeezeAmount, children, }: CenteredTopPanelContainerProps): JSX_2.Element;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export interface CenteredTopPanelContainerProps {
|
||||||
|
// (undocumented)
|
||||||
|
children: ReactNode;
|
||||||
|
// (undocumented)
|
||||||
|
ignoreRightWidth?: number;
|
||||||
|
// (undocumented)
|
||||||
|
marginBetweenZones?: number;
|
||||||
|
// (undocumented)
|
||||||
|
maxWidth?: number;
|
||||||
|
// (undocumented)
|
||||||
|
squeezeAmount?: number;
|
||||||
|
// (undocumented)
|
||||||
|
stylePanelWidth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function CheckBoxToolbarItem(): JSX_2.Element;
|
export function CheckBoxToolbarItem(): JSX_2.Element;
|
||||||
|
|
||||||
|
@ -443,6 +461,9 @@ export const defaultShapeTools: (typeof ArrowShapeTool)[];
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const defaultShapeUtils: TLAnyShapeUtilConstructor[];
|
export const defaultShapeUtils: TLAnyShapeUtilConstructor[];
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export function DefaultSharePanel(): JSX_2.Element;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const DefaultStylePanel: NamedExoticComponent<TLUiStylePanelProps>;
|
export const DefaultStylePanel: NamedExoticComponent<TLUiStylePanelProps>;
|
||||||
|
|
||||||
|
@ -464,6 +485,9 @@ export interface DefaultToolbarProps {
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const defaultTools: (typeof EraserTool | typeof HandTool | typeof ZoomTool)[];
|
export const defaultTools: (typeof EraserTool | typeof HandTool | typeof ZoomTool)[];
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export function DefaultTopPanel(): JSX_2.Element;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const DefaultZoomMenu: NamedExoticComponent<TLUiZoomMenuProps>;
|
export const DefaultZoomMenu: NamedExoticComponent<TLUiZoomMenuProps>;
|
||||||
|
|
||||||
|
@ -1264,7 +1288,7 @@ export interface PageItemInputProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const PageItemSubmenu: MemoExoticComponent<({ index, listSize, item, onRename, }: PageItemSubmenuProps) => JSX_2.Element>;
|
export const PageItemSubmenu: NamedExoticComponent<PageItemSubmenuProps>;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export interface PageItemSubmenuProps {
|
export interface PageItemSubmenuProps {
|
||||||
|
@ -1293,6 +1317,15 @@ export function parseTldrawJsonFile({ json, schema, }: {
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function PasteMenuItem(): JSX_2.Element;
|
export function PasteMenuItem(): JSX_2.Element;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export const PeopleMenu: NamedExoticComponent<PeopleMenuProps>;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export interface PeopleMenuProps {
|
||||||
|
// (undocumented)
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export enum PORTRAIT_BREAKPOINT {
|
export enum PORTRAIT_BREAKPOINT {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -1675,7 +1708,7 @@ export function TldrawScribble({ scribble, zoom, color, opacity, className }: TL
|
||||||
export const TldrawSelectionBackground: ({ bounds, rotation }: TLSelectionBackgroundProps) => JSX_2.Element | null;
|
export const TldrawSelectionBackground: ({ bounds, rotation }: TLSelectionBackgroundProps) => JSX_2.Element | null;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const TldrawSelectionForeground: MemoExoticComponent<({ bounds, rotation, }: TLSelectionForegroundProps) => JSX_2.Element | null>;
|
export const TldrawSelectionForeground: NamedExoticComponent<TLSelectionForegroundProps>;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function TldrawShapeIndicators(): JSX_2.Element | null;
|
export function TldrawShapeIndicators(): JSX_2.Element | null;
|
||||||
|
@ -1949,6 +1982,8 @@ export interface TLUiComponents {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
ContextMenu?: ComponentType<TLUiContextMenuProps> | null;
|
ContextMenu?: ComponentType<TLUiContextMenuProps> | null;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
CursorChatBubble?: ComponentType | null;
|
||||||
|
// (undocumented)
|
||||||
DebugMenu?: ComponentType | null;
|
DebugMenu?: ComponentType | null;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
DebugPanel?: ComponentType | null;
|
DebugPanel?: ComponentType | null;
|
||||||
|
@ -2174,6 +2209,8 @@ export interface TLUiEventMap {
|
||||||
locale: string;
|
locale: string;
|
||||||
};
|
};
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
'change-user-name': null;
|
||||||
|
// (undocumented)
|
||||||
'close-menu': {
|
'close-menu': {
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
@ -2264,6 +2301,8 @@ export interface TLUiEventMap {
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
'set-color': null;
|
||||||
|
// (undocumented)
|
||||||
'set-style': {
|
'set-style': {
|
||||||
id: string;
|
id: string;
|
||||||
value: number | string;
|
value: number | string;
|
||||||
|
@ -2273,6 +2312,8 @@ export interface TLUiEventMap {
|
||||||
operation: 'horizontal' | 'vertical';
|
operation: 'horizontal' | 'vertical';
|
||||||
};
|
};
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
'start-following': null;
|
||||||
|
// (undocumented)
|
||||||
'stop-following': null;
|
'stop-following': null;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
'stretch-shapes': {
|
'stretch-shapes': {
|
||||||
|
|
|
@ -141,6 +141,8 @@ export {
|
||||||
type TLUiQuickActionsProps,
|
type TLUiQuickActionsProps,
|
||||||
} from './lib/ui/components/QuickActions/DefaultQuickActions'
|
} from './lib/ui/components/QuickActions/DefaultQuickActions'
|
||||||
export { DefaultQuickActionsContent } from './lib/ui/components/QuickActions/DefaultQuickActionsContent'
|
export { DefaultQuickActionsContent } from './lib/ui/components/QuickActions/DefaultQuickActionsContent'
|
||||||
|
export { DefaultSharePanel } from './lib/ui/components/SharePanel/DefaultSharePanel'
|
||||||
|
export { PeopleMenu, type PeopleMenuProps } from './lib/ui/components/SharePanel/PeopleMenu'
|
||||||
export { Spinner } from './lib/ui/components/Spinner'
|
export { Spinner } from './lib/ui/components/Spinner'
|
||||||
export {
|
export {
|
||||||
DefaultStylePanel,
|
DefaultStylePanel,
|
||||||
|
@ -196,6 +198,11 @@ export {
|
||||||
useIsToolSelected,
|
useIsToolSelected,
|
||||||
type ToolbarItemProps,
|
type ToolbarItemProps,
|
||||||
} from './lib/ui/components/Toolbar/DefaultToolbarContent'
|
} from './lib/ui/components/Toolbar/DefaultToolbarContent'
|
||||||
|
export {
|
||||||
|
CenteredTopPanelContainer,
|
||||||
|
type CenteredTopPanelContainerProps,
|
||||||
|
} from './lib/ui/components/TopPanel/CenteredTopPanelContainer'
|
||||||
|
export { DefaultTopPanel } from './lib/ui/components/TopPanel/DefaultTopPanel'
|
||||||
export {
|
export {
|
||||||
DefaultZoomMenu,
|
DefaultZoomMenu,
|
||||||
type TLUiZoomMenuProps,
|
type TLUiZoomMenuProps,
|
||||||
|
|
|
@ -615,12 +615,17 @@
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
margin: 8px;
|
margin: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
touch-action: auto;
|
touch-action: auto;
|
||||||
overscroll-behavior: none;
|
overscroll-behavior: none;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
/* if the style panel is the only child (ie no share menu), increase the margin */
|
||||||
|
.tlui-style-panel__wrapper:only-child {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.tlui-style-panel {
|
.tlui-style-panel {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -1579,3 +1584,193 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ------------------- Da share zone ------------------ */
|
||||||
|
.tlui-share-zone {
|
||||||
|
padding: 0px 0px 0px 0px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
z-index: var(--layer-panels);
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 2px;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------- People Menu ------------------- */
|
||||||
|
.tlui-people-menu__avatars-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: all;
|
||||||
|
border-radius: var(--radius-1);
|
||||||
|
padding-right: 1px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tlui-people-menu__avatars {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tlui-people-menu__avatar {
|
||||||
|
height: 28px;
|
||||||
|
width: 28px;
|
||||||
|
border: 2px solid var(--color-background);
|
||||||
|
background-color: var(--color-low);
|
||||||
|
border-radius: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--color-selected-contrast);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tlui-people-menu__avatar:nth-of-type(n + 2) {
|
||||||
|
margin-left: -12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tlui-people-menu__avatars-button[data-state='open'] {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
.tlui-people-menu__avatars-button:hover .tlui-people-menu__avatar {
|
||||||
|
border-color: var(--color-low);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tlui-people-menu__more {
|
||||||
|
min-width: 0px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
font-family: inherit;
|
||||||
|
padding: 0px 4px;
|
||||||
|
letter-spacing: 1.5;
|
||||||
|
}
|
||||||
|
.tlui-people-menu__more::after {
|
||||||
|
border-radius: var(--radius-2);
|
||||||
|
inset: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tlui-people-menu__wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 220px;
|
||||||
|
height: fit-content;
|
||||||
|
max-height: 50vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tlui-people-menu__section {
|
||||||
|
position: relative;
|
||||||
|
touch-action: auto;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
touch-action: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tlui-people-menu__section:not(:last-child) {
|
||||||
|
border-bottom: 1px solid var(--color-divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tlui-people-menu__user {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tlui-people-menu__user__color {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tlui-people-menu__user__name {
|
||||||
|
text-align: left;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
max-width: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tlui-people-menu__user__label {
|
||||||
|
text-align: left;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
flex-grow: 100;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tlui-people-menu__user__input {
|
||||||
|
flex-grow: 2;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0px;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tlui-people-menu__user > .tlui-input__wrapper {
|
||||||
|
width: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: auto;
|
||||||
|
flex-grow: 2;
|
||||||
|
gap: 8px;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tlui-people-menu__item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tlui-people-menu__item__button {
|
||||||
|
padding: 0 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tlui-people-menu__item > .tlui-button__menu {
|
||||||
|
width: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: auto;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-grow: 2;
|
||||||
|
gap: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tlui-people-menu__item__follow {
|
||||||
|
min-width: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tlui-people-menu__item__follow[data-active='true'] .tlui-icon {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tlui-people-menu__item__follow:focus-visible .tlui-icon {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
.tlui-people-menu__item__follow .tlui-icon {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tlui-people-menu__item__follow:hover .tlui-icon {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -109,6 +109,7 @@ const TldrawUiContent = React.memo(function TldrawUI() {
|
||||||
NavigationPanel,
|
NavigationPanel,
|
||||||
HelperButtons,
|
HelperButtons,
|
||||||
DebugPanel,
|
DebugPanel,
|
||||||
|
CursorChatBubble,
|
||||||
} = useTldrawUiComponents()
|
} = useTldrawUiComponents()
|
||||||
|
|
||||||
useKeyboardShortcuts()
|
useKeyboardShortcuts()
|
||||||
|
@ -164,6 +165,7 @@ const TldrawUiContent = React.memo(function TldrawUI() {
|
||||||
<Dialogs />
|
<Dialogs />
|
||||||
<ToastViewport />
|
<ToastViewport />
|
||||||
<FollowingIndicator />
|
<FollowingIndicator />
|
||||||
|
{CursorChatBubble && <CursorChatBubble />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import { useEditor, useValue } from '@tldraw/editor'
|
import { useEditor, useValue } from '@tldraw/editor'
|
||||||
|
import { useIsMultiplayer } from '../../hooks/useIsMultiplayer'
|
||||||
import {
|
import {
|
||||||
ArrangeMenuSubmenu,
|
ArrangeMenuSubmenu,
|
||||||
ClipboardMenuGroup,
|
ClipboardMenuGroup,
|
||||||
ConversionsMenuGroup,
|
ConversionsMenuGroup,
|
||||||
|
CursorChatItem,
|
||||||
EditMenuSubmenu,
|
EditMenuSubmenu,
|
||||||
MoveToPageMenu,
|
MoveToPageMenu,
|
||||||
ReorderMenuSubmenu,
|
ReorderMenuSubmenu,
|
||||||
|
@ -13,6 +15,7 @@ import { TldrawUiMenuGroup } from '../primitives/menus/TldrawUiMenuGroup'
|
||||||
/** @public @react */
|
/** @public @react */
|
||||||
export function DefaultContextMenuContent() {
|
export function DefaultContextMenuContent() {
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
|
const isMultiplayer = useIsMultiplayer()
|
||||||
|
|
||||||
const selectToolActive = useValue(
|
const selectToolActive = useValue(
|
||||||
'isSelectToolActive',
|
'isSelectToolActive',
|
||||||
|
@ -24,6 +27,7 @@ export function DefaultContextMenuContent() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{isMultiplayer && <CursorChatItem />}
|
||||||
<TldrawUiMenuGroup id="modify">
|
<TldrawUiMenuGroup id="modify">
|
||||||
<EditMenuSubmenu />
|
<EditMenuSubmenu />
|
||||||
<ArrangeMenuSubmenu />
|
<ArrangeMenuSubmenu />
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { preventDefault, track, useEditor } from '@tldraw/editor'
|
||||||
import {
|
import {
|
||||||
ChangeEvent,
|
ChangeEvent,
|
||||||
ClipboardEvent,
|
ClipboardEvent,
|
||||||
|
@ -9,7 +10,7 @@ import {
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { preventDefault, track, useEditor, useTranslation } from 'tldraw'
|
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
||||||
|
|
||||||
// todo:
|
// todo:
|
||||||
// - not cleaning up
|
// - not cleaning up
|
||||||
|
@ -27,7 +28,7 @@ export const CursorChatBubble = track(function CursorChatBubble() {
|
||||||
const closingUp = !isChatting && chatMessage
|
const closingUp = !isChatting && chatMessage
|
||||||
if (closingUp || isChatting) {
|
if (closingUp || isChatting) {
|
||||||
const duration = isChatting ? CHAT_MESSAGE_TIMEOUT_CHATTING : CHAT_MESSAGE_TIMEOUT_CLOSING
|
const duration = isChatting ? CHAT_MESSAGE_TIMEOUT_CHATTING : CHAT_MESSAGE_TIMEOUT_CLOSING
|
||||||
rTimeout.current = setTimeout(() => {
|
rTimeout.current = editor.timers.setTimeout(() => {
|
||||||
editor.updateInstanceState({ chatMessage: '', isChatting: false })
|
editor.updateInstanceState({ chatMessage: '', isChatting: false })
|
||||||
setValue('')
|
setValue('')
|
||||||
editor.focus()
|
editor.focus()
|
||||||
|
@ -125,7 +126,7 @@ const CursorChatInput = track(function CursorChatInput({
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
// Focus the input
|
// Focus the input
|
||||||
const raf = requestAnimationFrame(() => {
|
const raf = editor.timers.requestAnimationFrame(() => {
|
||||||
ref.current?.focus()
|
ref.current?.focus()
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { useActions } from '../../context/actions'
|
import { useActions } from '../../context/actions'
|
||||||
|
import { useIsMultiplayer } from '../../hooks/useIsMultiplayer'
|
||||||
import { useTools } from '../../hooks/useTools'
|
import { useTools } from '../../hooks/useTools'
|
||||||
import { TldrawUiMenuGroup } from '../primitives/menus/TldrawUiMenuGroup'
|
import { TldrawUiMenuGroup } from '../primitives/menus/TldrawUiMenuGroup'
|
||||||
import { TldrawUiMenuItem } from '../primitives/menus/TldrawUiMenuItem'
|
import { TldrawUiMenuItem } from '../primitives/menus/TldrawUiMenuItem'
|
||||||
|
@ -7,6 +8,7 @@ import { TldrawUiMenuItem } from '../primitives/menus/TldrawUiMenuItem'
|
||||||
export function DefaultKeyboardShortcutsDialogContent() {
|
export function DefaultKeyboardShortcutsDialogContent() {
|
||||||
const actions = useActions()
|
const actions = useActions()
|
||||||
const tools = useTools()
|
const tools = useTools()
|
||||||
|
const isMultiplayer = useIsMultiplayer()
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TldrawUiMenuGroup label="shortcuts-dialog.tools" id="tools">
|
<TldrawUiMenuGroup label="shortcuts-dialog.tools" id="tools">
|
||||||
|
@ -63,6 +65,11 @@ export function DefaultKeyboardShortcutsDialogContent() {
|
||||||
<TldrawUiMenuItem {...actions['align-center-horizontal']} />
|
<TldrawUiMenuItem {...actions['align-center-horizontal']} />
|
||||||
<TldrawUiMenuItem {...actions['align-right']} />
|
<TldrawUiMenuItem {...actions['align-right']} />
|
||||||
</TldrawUiMenuGroup>
|
</TldrawUiMenuGroup>
|
||||||
|
{isMultiplayer && (
|
||||||
|
<TldrawUiMenuGroup label="shortcuts-dialog.collaboration" id="collaboration">
|
||||||
|
<TldrawUiMenuItem {...actions['open-cursor-chat']} />
|
||||||
|
</TldrawUiMenuGroup>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { useRef } from 'react'
|
|
||||||
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
|
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
|
||||||
import { TldrawUiIcon } from '../primitives/TldrawUiIcon'
|
import { TldrawUiIcon } from '../primitives/TldrawUiIcon'
|
||||||
|
|
||||||
/** @public @react */
|
/** @public @react */
|
||||||
export function OfflineIndicator() {
|
export function OfflineIndicator() {
|
||||||
const msg = useTranslation()
|
const msg = useTranslation()
|
||||||
const rContainer = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames('tlui-offline-indicator')} ref={rContainer}>
|
<div className={classNames('tlui-offline-indicator')}>
|
||||||
{msg('status.offline')}
|
{msg('status.offline')}
|
||||||
<TldrawUiIcon aria-label="offline" icon="status-offline" small />
|
<TldrawUiIcon aria-label="offline" icon="status-offline" small />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { PeopleMenu } from './PeopleMenu'
|
||||||
|
|
||||||
|
/** @public @react */
|
||||||
|
export function DefaultSharePanel() {
|
||||||
|
return (
|
||||||
|
<div className="tlui-share-zone" draggable={false}>
|
||||||
|
<PeopleMenu />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,27 +1,20 @@
|
||||||
import * as Popover from '@radix-ui/react-popover'
|
import * as Popover from '@radix-ui/react-popover'
|
||||||
import {
|
import { track, useContainer, useEditor, usePeerIds, useValue } from '@tldraw/editor'
|
||||||
TldrawUiButton,
|
import { ReactNode } from 'react'
|
||||||
TldrawUiButtonIcon,
|
import { useMenuIsOpen } from '../../hooks/useMenuIsOpen'
|
||||||
TldrawUiButtonLabel,
|
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
|
||||||
track,
|
|
||||||
useContainer,
|
|
||||||
useEditor,
|
|
||||||
useMenuIsOpen,
|
|
||||||
usePeerIds,
|
|
||||||
useTranslation,
|
|
||||||
useValue,
|
|
||||||
} from 'tldraw'
|
|
||||||
import { PeopleMenuAvatar } from './PeopleMenuAvatar'
|
import { PeopleMenuAvatar } from './PeopleMenuAvatar'
|
||||||
import { PeopleMenuItem } from './PeopleMenuItem'
|
import { PeopleMenuItem } from './PeopleMenuItem'
|
||||||
import { PeopleMenuMore } from './PeopleMenuMore'
|
import { PeopleMenuMore } from './PeopleMenuMore'
|
||||||
import { UserPresenceEditor } from './UserPresenceEditor'
|
import { UserPresenceEditor } from './UserPresenceEditor'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export const PeopleMenu = track(function PeopleMenu({
|
export interface PeopleMenuProps {
|
||||||
hideShareMenu,
|
children?: ReactNode
|
||||||
}: {
|
}
|
||||||
hideShareMenu?: boolean
|
|
||||||
}) {
|
/** @public @react */
|
||||||
|
export const PeopleMenu = track(function PeopleMenu({ children }: PeopleMenuProps) {
|
||||||
const msg = useTranslation()
|
const msg = useTranslation()
|
||||||
|
|
||||||
const container = useContainer()
|
const container = useContainer()
|
||||||
|
@ -73,18 +66,7 @@ export const PeopleMenu = track(function PeopleMenu({
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!hideShareMenu && (
|
{children}
|
||||||
<div className="tlui-people-menu__section">
|
|
||||||
<TldrawUiButton
|
|
||||||
type="menu"
|
|
||||||
data-testid="people-menu.invite"
|
|
||||||
onClick={() => editor.addOpenMenu('share menu')}
|
|
||||||
>
|
|
||||||
<TldrawUiButtonLabel>{msg('people-menu.invite')}</TldrawUiButtonLabel>
|
|
||||||
<TldrawUiButtonIcon icon="plus" />
|
|
||||||
</TldrawUiButton>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</Popover.Content>
|
</Popover.Content>
|
||||||
</Popover.Portal>
|
</Popover.Portal>
|
|
@ -1,4 +1,4 @@
|
||||||
import { usePresence } from 'tldraw'
|
import { usePresence } from '@tldraw/editor'
|
||||||
|
|
||||||
export function PeopleMenuAvatar({ userId }: { userId: string }) {
|
export function PeopleMenuAvatar({ userId }: { userId: string }) {
|
||||||
const presence = usePresence(userId)
|
const presence = usePresence(userId)
|
|
@ -1,15 +1,10 @@
|
||||||
|
import { track, useEditor, usePresence } from '@tldraw/editor'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import {
|
import { useUiEvents } from '../../context/events'
|
||||||
TldrawUiButton,
|
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
|
||||||
TldrawUiButtonIcon,
|
import { TldrawUiButton } from '../primitives/Button/TldrawUiButton'
|
||||||
TldrawUiIcon,
|
import { TldrawUiButtonIcon } from '../primitives/Button/TldrawUiButtonIcon'
|
||||||
track,
|
import { TldrawUiIcon } from '../primitives/TldrawUiIcon'
|
||||||
useEditor,
|
|
||||||
usePresence,
|
|
||||||
useTranslation,
|
|
||||||
useUiEvents,
|
|
||||||
} from 'tldraw'
|
|
||||||
import { UI_OVERRIDE_TODO_EVENT } from '../../utils/useHandleUiEvent'
|
|
||||||
|
|
||||||
export const PeopleMenuItem = track(function PeopleMenuItem({ userId }: { userId: string }) {
|
export const PeopleMenuItem = track(function PeopleMenuItem({ userId }: { userId: string }) {
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
|
@ -24,7 +19,7 @@ export const PeopleMenuItem = track(function PeopleMenuItem({ userId }: { userId
|
||||||
trackEvent('stop-following', { source: 'people-menu' })
|
trackEvent('stop-following', { source: 'people-menu' })
|
||||||
} else {
|
} else {
|
||||||
editor.startFollowingUser(userId)
|
editor.startFollowingUser(userId)
|
||||||
trackEvent('start-following' as UI_OVERRIDE_TODO_EVENT, { source: 'people-menu' })
|
trackEvent('start-following', { source: 'people-menu' })
|
||||||
}
|
}
|
||||||
}, [editor, userId, trackEvent])
|
}, [editor, userId, trackEvent])
|
||||||
|
|
|
@ -1,16 +1,10 @@
|
||||||
import * as Popover from '@radix-ui/react-popover'
|
import * as Popover from '@radix-ui/react-popover'
|
||||||
|
import { USER_COLORS, track, useContainer, useEditor } from '@tldraw/editor'
|
||||||
import React, { useCallback, useRef, useState } from 'react'
|
import React, { useCallback, useRef, useState } from 'react'
|
||||||
import {
|
import { useUiEvents } from '../../context/events'
|
||||||
TldrawUiButton,
|
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
|
||||||
TldrawUiButtonIcon,
|
import { TldrawUiButton } from '../primitives/Button/TldrawUiButton'
|
||||||
USER_COLORS,
|
import { TldrawUiButtonIcon } from '../primitives/Button/TldrawUiButtonIcon'
|
||||||
track,
|
|
||||||
useContainer,
|
|
||||||
useEditor,
|
|
||||||
useTranslation,
|
|
||||||
useUiEvents,
|
|
||||||
} from 'tldraw'
|
|
||||||
import { UI_OVERRIDE_TODO_EVENT } from '../../utils/useHandleUiEvent'
|
|
||||||
|
|
||||||
export const UserPresenceColorPicker = track(function UserPresenceColorPicker() {
|
export const UserPresenceColorPicker = track(function UserPresenceColorPicker() {
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
|
@ -30,7 +24,7 @@ export const UserPresenceColorPicker = track(function UserPresenceColorPicker()
|
||||||
const onValueChange = useCallback(
|
const onValueChange = useCallback(
|
||||||
(item: string) => {
|
(item: string) => {
|
||||||
editor.user.updateUserPreferences({ color: item })
|
editor.user.updateUserPreferences({ color: item })
|
||||||
trackEvent('set-color' as UI_OVERRIDE_TODO_EVENT, { source: 'people-menu' })
|
trackEvent('set-color', { source: 'people-menu' })
|
||||||
},
|
},
|
||||||
[editor, trackEvent]
|
[editor, trackEvent]
|
||||||
)
|
)
|
|
@ -1,14 +1,10 @@
|
||||||
|
import { useEditor, useValue } from '@tldraw/editor'
|
||||||
import { useCallback, useRef, useState } from 'react'
|
import { useCallback, useRef, useState } from 'react'
|
||||||
import {
|
import { useUiEvents } from '../../context/events'
|
||||||
TldrawUiButton,
|
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
|
||||||
TldrawUiButtonIcon,
|
import { TldrawUiButton } from '../primitives/Button/TldrawUiButton'
|
||||||
TldrawUiInput,
|
import { TldrawUiButtonIcon } from '../primitives/Button/TldrawUiButtonIcon'
|
||||||
useEditor,
|
import { TldrawUiInput } from '../primitives/TldrawUiInput'
|
||||||
useTranslation,
|
|
||||||
useUiEvents,
|
|
||||||
useValue,
|
|
||||||
} from 'tldraw'
|
|
||||||
import { UI_OVERRIDE_TODO_EVENT } from '../../utils/useHandleUiEvent'
|
|
||||||
import { UserPresenceColorPicker } from './UserPresenceColorPicker'
|
import { UserPresenceColorPicker } from './UserPresenceColorPicker'
|
||||||
|
|
||||||
export function UserPresenceEditor() {
|
export function UserPresenceEditor() {
|
||||||
|
@ -36,7 +32,7 @@ export function UserPresenceEditor() {
|
||||||
|
|
||||||
const handleBlur = useCallback(() => {
|
const handleBlur = useCallback(() => {
|
||||||
if (rOriginalName.current === rCurrentName.current) return
|
if (rOriginalName.current === rCurrentName.current) return
|
||||||
trackEvent('change-user-name' as UI_OVERRIDE_TODO_EVENT, { source: 'people-menu' })
|
trackEvent('change-user-name', { source: 'people-menu' })
|
||||||
rOriginalName.current = rCurrentName.current
|
rOriginalName.current = rCurrentName.current
|
||||||
}, [trackEvent])
|
}, [trackEvent])
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
import { ReactNode, useCallback, useLayoutEffect, useRef } from 'react'
|
||||||
|
import { useBreakpoint } from '../../context/breakpoints'
|
||||||
|
|
||||||
|
/** @public */
|
||||||
|
export interface CenteredTopPanelContainerProps {
|
||||||
|
children: ReactNode
|
||||||
|
maxWidth?: number
|
||||||
|
ignoreRightWidth?: number
|
||||||
|
stylePanelWidth?: number
|
||||||
|
marginBetweenZones?: number
|
||||||
|
squeezeAmount?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @public @react */
|
||||||
|
export function CenteredTopPanelContainer({
|
||||||
|
maxWidth = 420,
|
||||||
|
ignoreRightWidth = 0,
|
||||||
|
stylePanelWidth = 148,
|
||||||
|
marginBetweenZones = 12,
|
||||||
|
squeezeAmount = 52,
|
||||||
|
children,
|
||||||
|
}: CenteredTopPanelContainerProps) {
|
||||||
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
const breakpoint = useBreakpoint()
|
||||||
|
|
||||||
|
const updateLayout = useCallback(() => {
|
||||||
|
const element = ref.current
|
||||||
|
if (!element) return
|
||||||
|
|
||||||
|
const layoutTop = element.parentElement!.parentElement!
|
||||||
|
const leftPanel = layoutTop.querySelector('.tlui-layout__top__left')! as HTMLElement
|
||||||
|
const rightPanel = layoutTop.querySelector('.tlui-layout__top__right')! as HTMLElement
|
||||||
|
|
||||||
|
const totalWidth = layoutTop.offsetWidth
|
||||||
|
const leftWidth = leftPanel.offsetWidth
|
||||||
|
const rightWidth = rightPanel.offsetWidth
|
||||||
|
|
||||||
|
// Ignore button width
|
||||||
|
const selfWidth = element.offsetWidth - ignoreRightWidth
|
||||||
|
|
||||||
|
let xCoordIfCentered = (totalWidth - selfWidth) / 2
|
||||||
|
|
||||||
|
// Prevent subpixel bullsh
|
||||||
|
if (totalWidth % 2 !== 0) {
|
||||||
|
xCoordIfCentered -= 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
const xCoordIfLeftAligned = leftWidth + marginBetweenZones
|
||||||
|
|
||||||
|
const left = element.offsetLeft
|
||||||
|
const maxWidthProperty = Math.min(
|
||||||
|
totalWidth - rightWidth - leftWidth - 2 * marginBetweenZones,
|
||||||
|
maxWidth
|
||||||
|
)
|
||||||
|
const xCoord = Math.max(xCoordIfCentered, xCoordIfLeftAligned) - left
|
||||||
|
|
||||||
|
// Squeeze the title if the right panel is too wide on small screens
|
||||||
|
if (rightPanel.offsetWidth > stylePanelWidth && breakpoint <= 6) {
|
||||||
|
element.style.setProperty('max-width', maxWidthProperty - squeezeAmount + 'px')
|
||||||
|
} else {
|
||||||
|
element.style.setProperty('max-width', maxWidthProperty + 'px')
|
||||||
|
}
|
||||||
|
element.style.setProperty('transform', `translate(${xCoord}px, 0px)`)
|
||||||
|
}, [breakpoint, ignoreRightWidth, marginBetweenZones, maxWidth, squeezeAmount, stylePanelWidth])
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const element = ref.current
|
||||||
|
if (!element) return
|
||||||
|
|
||||||
|
const layoutTop = element.parentElement!.parentElement!
|
||||||
|
const leftPanel = layoutTop.querySelector('.tlui-layout__top__left')! as HTMLElement
|
||||||
|
const rightPanel = layoutTop.querySelector('.tlui-layout__top__right')! as HTMLElement
|
||||||
|
|
||||||
|
// Update layout when the things change
|
||||||
|
const observer = new ResizeObserver(updateLayout)
|
||||||
|
observer.observe(leftPanel)
|
||||||
|
observer.observe(rightPanel)
|
||||||
|
observer.observe(layoutTop)
|
||||||
|
observer.observe(element)
|
||||||
|
|
||||||
|
// Also update on first layout
|
||||||
|
updateLayout()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
}, [updateLayout])
|
||||||
|
|
||||||
|
// Update after every render, too
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
updateLayout()
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} className="tlui-top-panel__container">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { useMultiplayerStatus } from '../../hooks/useIsMultiplayer'
|
||||||
|
import { OfflineIndicator } from '../OfflineIndicator/OfflineIndicator'
|
||||||
|
import { CenteredTopPanelContainer } from './CenteredTopPanelContainer'
|
||||||
|
|
||||||
|
/** @public @react */
|
||||||
|
export function DefaultTopPanel() {
|
||||||
|
const isOffline = useMultiplayerStatus() === 'offline'
|
||||||
|
|
||||||
|
return <CenteredTopPanelContainer>{isOffline && <OfflineIndicator />}</CenteredTopPanelContainer>
|
||||||
|
}
|
|
@ -646,3 +646,19 @@ export function PrintItem() {
|
||||||
])
|
])
|
||||||
return <TldrawUiMenuItem {...actions['print']} disabled={emptyPage} />
|
return <TldrawUiMenuItem {...actions['print']} disabled={emptyPage} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------------------- Multiplayer --------------------- */
|
||||||
|
/** @public @react */
|
||||||
|
export function CursorChatItem() {
|
||||||
|
const editor = useEditor()
|
||||||
|
const actions = useActions()
|
||||||
|
const shouldShow = useValue(
|
||||||
|
'show cursor chat',
|
||||||
|
() => editor.getCurrentToolId() === 'select' && !editor.getInstanceState().isCoarsePointer,
|
||||||
|
[editor]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!shouldShow) return null
|
||||||
|
|
||||||
|
return <TldrawUiMenuItem {...actions['open-cursor-chat']} />
|
||||||
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ import { useCopyAs } from '../hooks/useCopyAs'
|
||||||
import { useExportAs } from '../hooks/useExportAs'
|
import { useExportAs } from '../hooks/useExportAs'
|
||||||
import { flattenShapesToImages } from '../hooks/useFlatten'
|
import { flattenShapesToImages } from '../hooks/useFlatten'
|
||||||
import { useInsertMedia } from '../hooks/useInsertMedia'
|
import { useInsertMedia } from '../hooks/useInsertMedia'
|
||||||
|
import { useIsMultiplayer } from '../hooks/useIsMultiplayer'
|
||||||
import { usePrint } from '../hooks/usePrint'
|
import { usePrint } from '../hooks/usePrint'
|
||||||
import { TLUiTranslationKey } from '../hooks/useTranslation/TLUiTranslationKey'
|
import { TLUiTranslationKey } from '../hooks/useTranslation/TLUiTranslationKey'
|
||||||
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
import { useTranslation } from '../hooks/useTranslation/useTranslation'
|
||||||
|
@ -84,6 +85,7 @@ function getExportName(editor: Editor, defaultName: string) {
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
|
const isMultiplayer = useIsMultiplayer()
|
||||||
|
|
||||||
const { addDialog, clearDialogs } = useDialogs()
|
const { addDialog, clearDialogs } = useDialogs()
|
||||||
const { clearToasts, addToast } = useToasts()
|
const { clearToasts, addToast } = useToasts()
|
||||||
|
@ -1424,6 +1426,28 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if (isMultiplayer) {
|
||||||
|
actionItems.push({
|
||||||
|
id: 'open-cursor-chat',
|
||||||
|
label: 'action.open-cursor-chat',
|
||||||
|
readonlyOk: true,
|
||||||
|
kbd: '/',
|
||||||
|
onSelect(source: any) {
|
||||||
|
trackEvent('open-cursor-chat', { source })
|
||||||
|
|
||||||
|
// Don't open cursor chat if we're on a touch device
|
||||||
|
if (editor.getInstanceState().isCoarsePointer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait a frame before opening as otherwise the open context menu will close it
|
||||||
|
editor.timers.requestAnimationFrame(() => {
|
||||||
|
editor.updateInstanceState({ isChatting: true })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const actions = makeActions(actionItems)
|
const actions = makeActions(actionItems)
|
||||||
|
|
||||||
if (overrides) {
|
if (overrides) {
|
||||||
|
@ -1448,6 +1472,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
printSelectionOrPages,
|
printSelectionOrPages,
|
||||||
msg,
|
msg,
|
||||||
defaultDocumentName,
|
defaultDocumentName,
|
||||||
|
isMultiplayer,
|
||||||
])
|
])
|
||||||
|
|
||||||
return <ActionsContext.Provider value={asActions(actions)}>{children}</ActionsContext.Provider>
|
return <ActionsContext.Provider value={asActions(actions)}>{children}</ActionsContext.Provider>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
DefaultContextMenu,
|
DefaultContextMenu,
|
||||||
TLUiContextMenuProps,
|
TLUiContextMenuProps,
|
||||||
} from '../components/ContextMenu/DefaultContextMenu'
|
} from '../components/ContextMenu/DefaultContextMenu'
|
||||||
|
import { CursorChatBubble } from '../components/CursorChatBubble'
|
||||||
import { DefaultDebugMenu } from '../components/DebugMenu/DefaultDebugMenu'
|
import { DefaultDebugMenu } from '../components/DebugMenu/DefaultDebugMenu'
|
||||||
import { DefaultDebugPanel } from '../components/DefaultDebugPanel'
|
import { DefaultDebugPanel } from '../components/DefaultDebugPanel'
|
||||||
import { DefaultHelpMenu, TLUiHelpMenuProps } from '../components/HelpMenu/DefaultHelpMenu'
|
import { DefaultHelpMenu, TLUiHelpMenuProps } from '../components/HelpMenu/DefaultHelpMenu'
|
||||||
|
@ -28,9 +29,12 @@ import {
|
||||||
DefaultQuickActions,
|
DefaultQuickActions,
|
||||||
TLUiQuickActionsProps,
|
TLUiQuickActionsProps,
|
||||||
} from '../components/QuickActions/DefaultQuickActions'
|
} from '../components/QuickActions/DefaultQuickActions'
|
||||||
|
import { DefaultSharePanel } from '../components/SharePanel/DefaultSharePanel'
|
||||||
import { DefaultStylePanel, TLUiStylePanelProps } from '../components/StylePanel/DefaultStylePanel'
|
import { DefaultStylePanel, TLUiStylePanelProps } from '../components/StylePanel/DefaultStylePanel'
|
||||||
import { DefaultToolbar } from '../components/Toolbar/DefaultToolbar'
|
import { DefaultToolbar } from '../components/Toolbar/DefaultToolbar'
|
||||||
|
import { DefaultTopPanel } from '../components/TopPanel/DefaultTopPanel'
|
||||||
import { DefaultZoomMenu, TLUiZoomMenuProps } from '../components/ZoomMenu/DefaultZoomMenu'
|
import { DefaultZoomMenu, TLUiZoomMenuProps } from '../components/ZoomMenu/DefaultZoomMenu'
|
||||||
|
import { useIsMultiplayer } from '../hooks/useIsMultiplayer'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export interface TLUiComponents {
|
export interface TLUiComponents {
|
||||||
|
@ -52,6 +56,7 @@ export interface TLUiComponents {
|
||||||
MenuPanel?: ComponentType | null
|
MenuPanel?: ComponentType | null
|
||||||
TopPanel?: ComponentType | null
|
TopPanel?: ComponentType | null
|
||||||
SharePanel?: ComponentType | null
|
SharePanel?: ComponentType | null
|
||||||
|
CursorChatBubble?: ComponentType | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const TldrawUiComponentsContext = createContext<TLUiComponents | null>(null)
|
const TldrawUiComponentsContext = createContext<TLUiComponents | null>(null)
|
||||||
|
@ -68,6 +73,7 @@ export function TldrawUiComponentsProvider({
|
||||||
children,
|
children,
|
||||||
}: TLUiComponentsProviderProps) {
|
}: TLUiComponentsProviderProps) {
|
||||||
const _overrides = useShallowObjectIdentity(overrides)
|
const _overrides = useShallowObjectIdentity(overrides)
|
||||||
|
const isMultiplayer = useIsMultiplayer()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TldrawUiComponentsContext.Provider
|
<TldrawUiComponentsContext.Provider
|
||||||
|
@ -89,9 +95,12 @@ export function TldrawUiComponentsProvider({
|
||||||
DebugPanel: DefaultDebugPanel,
|
DebugPanel: DefaultDebugPanel,
|
||||||
DebugMenu: DefaultDebugMenu,
|
DebugMenu: DefaultDebugMenu,
|
||||||
MenuPanel: DefaultMenuPanel,
|
MenuPanel: DefaultMenuPanel,
|
||||||
|
SharePanel: isMultiplayer ? DefaultSharePanel : null,
|
||||||
|
CursorChatBubble: isMultiplayer ? CursorChatBubble : null,
|
||||||
|
TopPanel: isMultiplayer ? DefaultTopPanel : null,
|
||||||
..._overrides,
|
..._overrides,
|
||||||
}),
|
}),
|
||||||
[_overrides]
|
[_overrides, isMultiplayer]
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -94,7 +94,10 @@ export interface TLUiEventMap {
|
||||||
'toggle-edge-scrolling': null
|
'toggle-edge-scrolling': null
|
||||||
'color-scheme': { value: string }
|
'color-scheme': { value: string }
|
||||||
'exit-pen-mode': null
|
'exit-pen-mode': null
|
||||||
|
'start-following': null
|
||||||
'stop-following': null
|
'stop-following': null
|
||||||
|
'set-color': null
|
||||||
|
'change-user-name': null
|
||||||
'open-cursor-chat': null
|
'open-cursor-chat': null
|
||||||
'zoom-tool': null
|
'zoom-tool': null
|
||||||
'unlock-all': null
|
'unlock-all': null
|
||||||
|
|
20
packages/tldraw/src/lib/ui/hooks/useIsMultiplayer.ts
Normal file
20
packages/tldraw/src/lib/ui/hooks/useIsMultiplayer.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { useEditor, useValue } from '@tldraw/editor'
|
||||||
|
|
||||||
|
export function useIsMultiplayer() {
|
||||||
|
const editor = useEditor()
|
||||||
|
return !!editor.store.props.multiplayerStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMultiplayerStatus() {
|
||||||
|
const editor = useEditor()
|
||||||
|
return useValue(
|
||||||
|
'multiplayerStatus',
|
||||||
|
() => {
|
||||||
|
if (!editor.store.props.multiplayerStatus) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return editor.store.props.multiplayerStatus.get()
|
||||||
|
},
|
||||||
|
[editor]
|
||||||
|
)
|
||||||
|
}
|
|
@ -1499,6 +1499,8 @@ export interface TLStoreProps {
|
||||||
assets: TLAssetStore;
|
assets: TLAssetStore;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
defaultName: string;
|
defaultName: string;
|
||||||
|
// (undocumented)
|
||||||
|
multiplayerStatus: null | Signal<'offline' | 'online'>;
|
||||||
onEditorMount: (editor: unknown) => (() => void) | void;
|
onEditorMount: (editor: unknown) => (() => void) | void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Signal } from '@tldraw/state'
|
||||||
import {
|
import {
|
||||||
SerializedStore,
|
SerializedStore,
|
||||||
Store,
|
Store,
|
||||||
|
@ -98,6 +99,7 @@ export interface TLStoreProps {
|
||||||
* Called an {@link @tldraw/editor#Editor} connected to this store is mounted.
|
* Called an {@link @tldraw/editor#Editor} connected to this store is mounted.
|
||||||
*/
|
*/
|
||||||
onEditorMount: (editor: unknown) => void | (() => void)
|
onEditorMount: (editor: unknown) => void | (() => void)
|
||||||
|
multiplayerStatus: Signal<'online' | 'offline'> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
|
Loading…
Reference in a new issue