UI components round two (#2847)

This PR:
- replaces the `shareZone` prop with `SharePanel` component
- replaces the `topZone` prop with `TopPanel` components
- replaces the `Button` component with `TldrawUiButton` and
subcomponents
- adds `TldrawUi` prefix to our primitives
- fixes a couple of bugs with the components

### Change Type

- [x] `major` — Breaking change
This commit is contained in:
Steve Ruiz 2024-02-16 09:13:04 +00:00 committed by GitHub
parent 5d87804a76
commit 7ece89a357
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
83 changed files with 3122 additions and 2826 deletions

View file

@ -67,6 +67,13 @@ const components: TLComponents = {
</DefaultDebugMenu> </DefaultDebugMenu>
) )
}, },
SharePanel: () => {
return (
<div className="tlui-share-zone" draggable={false}>
<ShareMenu />
</div>
)
},
} }
export function LocalEditor() { export function LocalEditor() {
@ -88,11 +95,6 @@ export function LocalEditor() {
overrides={[sharingUiOverrides, fileSystemUiOverrides]} overrides={[sharingUiOverrides, fileSystemUiOverrides]}
onUiEvent={handleUiEvent} onUiEvent={handleUiEvent}
components={components} components={components}
shareZone={
<div className="tlui-share-zone" draggable={false}>
<ShareMenu />
</div>
}
inferDarkMode inferDarkMode
> >
<LocalMigration /> <LocalMigration />

View file

@ -13,8 +13,10 @@ import {
Tldraw, Tldraw,
TldrawUiMenuGroup, TldrawUiMenuGroup,
TldrawUiMenuItem, TldrawUiMenuItem,
atom,
lns, lns,
useActions, useActions,
useValue,
} from '@tldraw/tldraw' } from '@tldraw/tldraw'
import { useCallback, useEffect } from 'react' import { useCallback, useEffect } from 'react'
import { useRemoteSyncClient } from '../hooks/useRemoteSyncClient' import { useRemoteSyncClient } from '../hooks/useRemoteSyncClient'
@ -39,6 +41,8 @@ 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
@ -78,6 +82,19 @@ const components: TLComponents = {
</DefaultKeyboardShortcutsDialog> </DefaultKeyboardShortcutsDialog>
) )
}, },
TopPanel: () => {
const isOffline = useValue('offline', () => shittyOfflineAtom.get(), [])
if (!isOffline) return null
return <OfflineIndicator />
},
SharePanel: () => {
return (
<div className="tlui-share-zone" draggable={false}>
<PeopleMenu />
<ShareMenu />
</div>
)
},
} }
export function MultiplayerEditor({ export function MultiplayerEditor({
@ -96,6 +113,12 @@ export function MultiplayerEditor({
roomId, roomId,
}) })
const isOffline =
storeWithStatus.status === 'synced-remote' && storeWithStatus.connectionStatus === 'offline'
useEffect(() => {
shittyOfflineAtom.set(isOffline)
}, [isOffline])
const isEmbedded = useIsEmbedded(roomSlug) const isEmbedded = useIsEmbedded(roomSlug)
const sharingUiOverrides = useSharing() const sharingUiOverrides = useSharing()
const fileSystemUiOverrides = useFileSystem({ isMultiplayer: true }) const fileSystemUiOverrides = useFileSystem({ isMultiplayer: true })
@ -118,9 +141,6 @@ export function MultiplayerEditor({
return <EmbeddedInIFrameWarning /> return <EmbeddedInIFrameWarning />
} }
const isOffline =
storeWithStatus.status === 'synced-remote' && storeWithStatus.connectionStatus === 'offline'
return ( return (
<div className="tldraw__editor"> <div className="tldraw__editor">
<Tldraw <Tldraw
@ -131,13 +151,6 @@ export function MultiplayerEditor({
initialState={isReadOnly ? 'hand' : 'select'} initialState={isReadOnly ? 'hand' : 'select'}
onUiEvent={handleUiEvent} onUiEvent={handleUiEvent}
components={components} components={components}
topZone={isOffline && <OfflineIndicator />}
shareZone={
<div className="tlui-share-zone" draggable={false}>
<PeopleMenu />
<ShareMenu />
</div>
}
autoFocus autoFocus
inferDarkMode inferDarkMode
> >

View file

@ -1,6 +1,8 @@
import * as Popover from '@radix-ui/react-popover' import * as Popover from '@radix-ui/react-popover'
import { import {
Button, TldrawUiButton,
TldrawUiButtonIcon,
TldrawUiButtonLabel,
track, track,
useContainer, useContainer,
useEditor, useEditor,
@ -73,13 +75,16 @@ export const PeopleMenu = track(function PeopleMenu({
)} )}
{!hideShareMenu && ( {!hideShareMenu && (
<div className="tlui-people-menu__section"> <div className="tlui-people-menu__section">
<Button <TldrawUiButton
type="menu" type="menu"
data-wd="people-menu.invite" data-testid="people-menu.invite"
label={'people-menu.invite'}
icon="plus"
onClick={() => editor.addOpenMenu('share menu')} onClick={() => editor.addOpenMenu('share menu')}
/> >
<TldrawUiButtonLabel>
{msg('people-menu.invite')}
<TldrawUiButtonIcon icon="plus" />
</TldrawUiButtonLabel>
</TldrawUiButton>
</div> </div>
)} )}
</div> </div>

View file

@ -1,6 +1,7 @@
import { import {
Button, TldrawUiButton,
Icon, TldrawUiButtonIcon,
TldrawUiIcon,
track, track,
useEditor, useEditor,
usePresence, usePresence,
@ -34,16 +35,16 @@ export const PeopleMenuItem = track(function PeopleMenuItem({ userId }: { userId
return ( return (
<div className="tlui-people-menu__item tlui-buttons__horizontal"> <div className="tlui-people-menu__item tlui-buttons__horizontal">
<Button <TldrawUiButton
type="menu" type="menu"
className="tlui-people-menu__item__button" className="tlui-people-menu__item__button"
onClick={() => editor.animateToUser(userId)} onClick={() => editor.animateToUser(userId)}
onDoubleClick={handleFollowClick} onDoubleClick={handleFollowClick}
> >
<Icon icon="color" color={presence.color} /> <TldrawUiIcon icon="color" color={presence.color} />
<div className="tlui-people-menu__name">{presence.userName ?? 'New User'}</div> <div className="tlui-people-menu__name">{presence.userName ?? 'New User'}</div>
</Button> </TldrawUiButton>
<Button <TldrawUiButton
type="icon" type="icon"
className="tlui-people-menu__item__follow" className="tlui-people-menu__item__follow"
title={ title={
@ -53,11 +54,14 @@ export const PeopleMenuItem = track(function PeopleMenuItem({ userId }: { userId
? msg('people-menu.following') ? msg('people-menu.following')
: msg('people-menu.follow') : msg('people-menu.follow')
} }
icon={theyAreFollowingYou ? 'leading' : youAreFollowingThem ? 'following' : 'follow'}
onClick={handleFollowClick} onClick={handleFollowClick}
disabled={theyAreFollowingYou} disabled={theyAreFollowingYou}
data-active={youAreFollowingThem || theyAreFollowingYou} data-active={youAreFollowingThem || theyAreFollowingYou}
>
<TldrawUiButtonIcon
icon={theyAreFollowingYou ? 'leading' : youAreFollowingThem ? 'following' : 'follow'}
/> />
</TldrawUiButton>
</div> </div>
) )
}) })

View file

@ -1,6 +1,7 @@
import * as Popover from '@radix-ui/react-popover' import * as Popover from '@radix-ui/react-popover'
import { import {
Button, TldrawUiButton,
TldrawUiButtonIcon,
USER_COLORS, USER_COLORS,
track, track,
useContainer, useContainer,
@ -88,13 +89,14 @@ export const UserPresenceColorPicker = track(function UserPresenceColorPicker()
return ( return (
<Popover.Root onOpenChange={handleOpenChange} open={isOpen}> <Popover.Root onOpenChange={handleOpenChange} open={isOpen}>
<Popover.Trigger dir="ltr" asChild> <Popover.Trigger dir="ltr" asChild>
<Button <TldrawUiButton
type="icon" type="icon"
className="tlui-people-menu__user__color" className="tlui-people-menu__user__color"
icon="color"
style={{ color: editor.user.getColor() }} style={{ color: editor.user.getColor() }}
title={msg('people-menu.change-color')} title={msg('people-menu.change-color')}
/> >
<TldrawUiButtonIcon icon="color" />
</TldrawUiButton>
</Popover.Trigger> </Popover.Trigger>
<Popover.Portal container={container}> <Popover.Portal container={container}>
<Popover.Content <Popover.Content
@ -106,11 +108,11 @@ export const UserPresenceColorPicker = track(function UserPresenceColorPicker()
> >
<div className={'tlui-buttons__grid'}> <div className={'tlui-buttons__grid'}>
{USER_COLORS.map((item: string) => ( {USER_COLORS.map((item: string) => (
<Button <TldrawUiButton
type="icon" type="icon"
key={item} key={item}
data-id={item} data-id={item}
data-wd={item} data-testid={item}
aria-label={item} aria-label={item}
data-state={value === item ? 'hinted' : undefined} data-state={value === item ? 'hinted' : undefined}
title={item} title={item}
@ -120,8 +122,9 @@ export const UserPresenceColorPicker = track(function UserPresenceColorPicker()
onPointerDown={handleButtonPointerDown} onPointerDown={handleButtonPointerDown}
onPointerUp={handleButtonPointerUp} onPointerUp={handleButtonPointerUp}
onClick={handleButtonClick} onClick={handleButtonClick}
icon={'color'} >
/> <TldrawUiButtonIcon icon="color" />
</TldrawUiButton>
))} ))}
</div> </div>
</Popover.Content> </Popover.Content>

View file

@ -1,4 +1,12 @@
import { Button, Input, useEditor, useTranslation, useUiEvents, useValue } from '@tldraw/tldraw' import {
TldrawUiButton,
TldrawUiButtonIcon,
TldrawUiInput,
useEditor,
useTranslation,
useUiEvents,
useValue,
} from '@tldraw/tldraw'
import { useCallback, useRef, useState } from 'react' import { useCallback, useRef, useState } from 'react'
import { UI_OVERRIDE_TODO_EVENT } from '../../utils/useHandleUiEvent' import { UI_OVERRIDE_TODO_EVENT } from '../../utils/useHandleUiEvent'
import { UserPresenceColorPicker } from './UserPresenceColorPicker' import { UserPresenceColorPicker } from './UserPresenceColorPicker'
@ -36,7 +44,7 @@ export function UserPresenceEditor() {
<div className="tlui-people-menu__user"> <div className="tlui-people-menu__user">
<UserPresenceColorPicker /> <UserPresenceColorPicker />
{isEditingName ? ( {isEditingName ? (
<Input <TldrawUiInput
className="tlui-people-menu__user__input" className="tlui-people-menu__user__input"
defaultValue={userName} defaultValue={userName}
onValueChange={handleValueChange} onValueChange={handleValueChange}
@ -62,14 +70,15 @@ export function UserPresenceEditor() {
) : null} ) : null}
</> </>
)} )}
<Button <TldrawUiButton
type="icon" type="icon"
className="tlui-people-menu__user__edit" className="tlui-people-menu__user__edit"
data-wd="people-menu.change-name" data-testid="people-menu.change-name"
title={msg('people-menu.change-name')} title={msg('people-menu.change-name')}
icon={isEditingName ? 'check' : 'edit'}
onClick={toggleEditingName} onClick={toggleEditingName}
/> >
<TldrawUiButtonIcon icon={isEditingName ? 'check' : 'edit'} />
</TldrawUiButton>
</div> </div>
) )
} }

View file

@ -54,6 +54,13 @@ const components: TLComponents = {
</DefaultKeyboardShortcutsDialog> </DefaultKeyboardShortcutsDialog>
) )
}, },
SharePanel: () => {
return (
<div className="tlui-share-zone" draggable={false}>
<ExportMenu />
</div>
)
},
} }
type SnapshotEditorProps = { type SnapshotEditorProps = {
@ -79,11 +86,6 @@ export function SnapshotsEditor(props: SnapshotEditorProps) {
editor.updateInstanceState({ isReadonly: true }) editor.updateInstanceState({ isReadonly: true })
}} }}
components={components} components={components}
shareZone={
<div className="tlui-share-zone" draggable={false}>
<ExportMenu />
</div>
}
renderDebugMenuItems={() => <DebugMenuItems />} renderDebugMenuItems={() => <DebugMenuItems />}
autoFocus autoFocus
inferDarkMode inferDarkMode

View file

@ -1,4 +1,10 @@
import { Button, LegacyTldrawDocument, useEditor, useValue } from '@tldraw/tldraw' import {
LegacyTldrawDocument,
TldrawUiButton,
TldrawUiButtonLabel,
useEditor,
useValue,
} from '@tldraw/tldraw'
export function MigrationAnnouncement({ export function MigrationAnnouncement({
onClose, onClose,
@ -107,16 +113,16 @@ export function MigrationAnnouncement({
marginTop: 8, marginTop: 8,
}} }}
> >
<Button <TldrawUiButton
type="normal" type="normal"
style={{ fontSize: 14, marginRight: 'auto' }} style={{ fontSize: 14, marginRight: 'auto' }}
onClick={downloadFile} onClick={downloadFile}
> >
Download original <TldrawUiButtonLabel>Download original</TldrawUiButtonLabel>
</Button> </TldrawUiButton>
<Button style={{ fontSize: 14 }} type="primary" onClick={onClose}> <TldrawUiButton style={{ fontSize: 14 }} type="primary" onClick={onClose}>
Continue <TldrawUiButtonLabel>Continue</TldrawUiButtonLabel>
</Button> </TldrawUiButton>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,11 +1,13 @@
import { import {
Button,
DialogBody,
DialogCloseButton,
DialogFooter,
DialogHeader,
DialogTitle,
TLUiDialogsContextType, TLUiDialogsContextType,
TldrawUiButton,
TldrawUiButtonCheck,
TldrawUiButtonLabel,
TldrawUiDialogBody,
TldrawUiDialogCloseButton,
TldrawUiDialogFooter,
TldrawUiDialogHeader,
TldrawUiDialogTitle,
useTranslation, useTranslation,
} from '@tldraw/tldraw' } from '@tldraw/tldraw'
import { useState } from 'react' import { useState } from 'react'
@ -49,26 +51,28 @@ function ConfirmClearDialog({
const [dontShowAgain, setDontShowAgain] = useState(false) const [dontShowAgain, setDontShowAgain] = useState(false)
return ( return (
<> <>
<DialogHeader> <TldrawUiDialogHeader>
<DialogTitle>{msg('file-system.confirm-clear.title')}</DialogTitle> <TldrawUiDialogTitle>{msg('file-system.confirm-clear.title')}</TldrawUiDialogTitle>
<DialogCloseButton /> <TldrawUiDialogCloseButton />
</DialogHeader> </TldrawUiDialogHeader>
<DialogBody style={{ maxWidth: 350 }}> <TldrawUiDialogBody style={{ maxWidth: 350 }}>
{msg('file-system.confirm-clear.description')} {msg('file-system.confirm-clear.description')}
</DialogBody> </TldrawUiDialogBody>
<DialogFooter className="tlui-dialog__footer__actions"> <TldrawUiDialogFooter className="tlui-dialog__footer__actions">
<Button <TldrawUiButton
type="normal" type="normal"
onClick={() => setDontShowAgain(!dontShowAgain)} onClick={() => setDontShowAgain(!dontShowAgain)}
iconLeft={dontShowAgain ? 'checkbox-checked' : 'checkbox-empty'}
style={{ marginRight: 'auto' }} style={{ marginRight: 'auto' }}
> >
<TldrawUiButtonCheck checked={dontShowAgain} />
<TldrawUiButtonLabel>
{msg('file-system.confirm-clear.dont-show-again')} {msg('file-system.confirm-clear.dont-show-again')}
</Button> </TldrawUiButtonLabel>
<Button type="normal" onClick={onCancel}> </TldrawUiButton>
{msg('file-system.confirm-clear.cancel')} <TldrawUiButton type="normal" onClick={onCancel}>
</Button> <TldrawUiButtonLabel>{msg('file-system.confirm-clear.cancel')}</TldrawUiButtonLabel>
<Button </TldrawUiButton>
<TldrawUiButton
type="primary" type="primary"
onClick={async () => { onClick={async () => {
if (dontShowAgain) { if (dontShowAgain) {
@ -77,9 +81,9 @@ function ConfirmClearDialog({
onContinue() onContinue()
}} }}
> >
{msg('file-system.confirm-clear.continue')} <TldrawUiButtonLabel>{msg('file-system.confirm-clear.continue')}</TldrawUiButtonLabel>
</Button> </TldrawUiButton>
</DialogFooter> </TldrawUiDialogFooter>
</> </>
) )
} }

View file

@ -1,11 +1,13 @@
import { import {
Button,
DialogBody,
DialogCloseButton,
DialogFooter,
DialogHeader,
DialogTitle,
TLUiDialogsContextType, TLUiDialogsContextType,
TldrawUiButton,
TldrawUiButtonCheck,
TldrawUiButtonLabel,
TldrawUiDialogBody,
TldrawUiDialogCloseButton,
TldrawUiDialogFooter,
TldrawUiDialogHeader,
TldrawUiDialogTitle,
useLocalStorageState, useLocalStorageState,
useTranslation, useTranslation,
} from '@tldraw/tldraw' } from '@tldraw/tldraw'
@ -50,24 +52,26 @@ function ConfirmLeaveDialog({
return ( return (
<> <>
<DialogHeader> <TldrawUiDialogHeader>
<DialogTitle>{msg('sharing.confirm-leave.title')}</DialogTitle> <TldrawUiDialogTitle>{msg('sharing.confirm-leave.title')}</TldrawUiDialogTitle>
<DialogCloseButton /> <TldrawUiDialogCloseButton />
</DialogHeader> </TldrawUiDialogHeader>
<DialogBody style={{ maxWidth: 350 }}>{msg('sharing.confirm-leave.description')}</DialogBody> <TldrawUiDialogBody style={{ maxWidth: 350 }}>
<DialogFooter className="tlui-dialog__footer__actions"> {msg('sharing.confirm-leave.description')}
<Button </TldrawUiDialogBody>
<TldrawUiDialogFooter className="tlui-dialog__footer__actions">
<TldrawUiButton
type="normal" type="normal"
onClick={() => setDontShowAgain(!dontShowAgain)}
iconLeft={dontShowAgain ? 'checkbox-checked' : 'checkbox-empty'}
style={{ marginRight: 'auto' }} style={{ marginRight: 'auto' }}
onClick={() => setDontShowAgain(!dontShowAgain)}
> >
{msg('sharing.confirm-leave.dont-show-again')} <TldrawUiButtonCheck checked={dontShowAgain} />
</Button> <TldrawUiButtonLabel>{msg('sharing.confirm-leave.dont-show-again')}</TldrawUiButtonLabel>
<Button type="normal" onClick={onCancel}> </TldrawUiButton>
{msg('sharing.confirm-leave.cancel')} <TldrawUiButton type="normal" onClick={onCancel}>
</Button> <TldrawUiButtonLabel>{msg('sharing.confirm-leave.cancel')}</TldrawUiButtonLabel>
<Button </TldrawUiButton>
<TldrawUiButton
type="primary" type="primary"
onClick={async () => { onClick={async () => {
if (dontShowAgain) { if (dontShowAgain) {
@ -76,9 +80,9 @@ function ConfirmLeaveDialog({
onContinue() onContinue()
}} }}
> >
{msg('sharing.confirm-leave.leave')} <TldrawUiButtonLabel>{msg('sharing.confirm-leave.leave')}</TldrawUiButtonLabel>
</Button> </TldrawUiButton>
</DialogFooter> </TldrawUiDialogFooter>
</> </>
) )
} }

View file

@ -1,11 +1,13 @@
import { import {
Button,
DialogBody,
DialogCloseButton,
DialogFooter,
DialogHeader,
DialogTitle,
TLUiDialogsContextType, TLUiDialogsContextType,
TldrawUiButton,
TldrawUiButtonCheck,
TldrawUiButtonLabel,
TldrawUiDialogBody,
TldrawUiDialogCloseButton,
TldrawUiDialogFooter,
TldrawUiDialogHeader,
TldrawUiDialogTitle,
useTranslation, useTranslation,
} from '@tldraw/tldraw' } from '@tldraw/tldraw'
import { useState } from 'react' import { useState } from 'react'
@ -49,26 +51,28 @@ function ConfirmOpenDialog({
const [dontShowAgain, setDontShowAgain] = useState(false) const [dontShowAgain, setDontShowAgain] = useState(false)
return ( return (
<> <>
<DialogHeader> <TldrawUiDialogHeader>
<DialogTitle>{msg('file-system.confirm-open.title')}</DialogTitle> <TldrawUiDialogTitle>{msg('file-system.confirm-open.title')}</TldrawUiDialogTitle>
<DialogCloseButton /> <TldrawUiDialogCloseButton />
</DialogHeader> </TldrawUiDialogHeader>
<DialogBody style={{ maxWidth: 350 }}> <TldrawUiDialogBody style={{ maxWidth: 350 }}>
{msg('file-system.confirm-open.description')} {msg('file-system.confirm-open.description')}
</DialogBody> </TldrawUiDialogBody>
<DialogFooter className="tlui-dialog__footer__actions"> <TldrawUiDialogFooter className="tlui-dialog__footer__actions">
<Button <TldrawUiButton
type="normal" type="normal"
onClick={() => setDontShowAgain(!dontShowAgain)} onClick={() => setDontShowAgain(!dontShowAgain)}
iconLeft={dontShowAgain ? 'checkbox-checked' : 'checkbox-empty'}
style={{ marginRight: 'auto' }} style={{ marginRight: 'auto' }}
> >
<TldrawUiButtonCheck checked={dontShowAgain} />
<TldrawUiButtonLabel>
{msg('file-system.confirm-open.dont-show-again')} {msg('file-system.confirm-open.dont-show-again')}
</Button> </TldrawUiButtonLabel>
<Button type="normal" onClick={onCancel}> </TldrawUiButton>
{msg('file-system.confirm-open.cancel')} <TldrawUiButton type="normal" onClick={onCancel}>
</Button> <TldrawUiButtonLabel>{msg('file-system.confirm-open.cancel')}</TldrawUiButtonLabel>
<Button </TldrawUiButton>
<TldrawUiButton
type="primary" type="primary"
onClick={async () => { onClick={async () => {
if (dontShowAgain) { if (dontShowAgain) {
@ -77,9 +81,9 @@ function ConfirmOpenDialog({
onContinue() onContinue()
}} }}
> >
{msg('file-system.confirm-open.open')} <TldrawUiButtonLabel>{msg('file-system.confirm-open.open')}</TldrawUiButtonLabel>
</Button> </TldrawUiButton>
</DialogFooter> </TldrawUiDialogFooter>
</> </>
) )
} }

View file

@ -1,8 +1,8 @@
import { import {
DefaultSizeStyle, DefaultSizeStyle,
Icon,
SharedStyleMap, SharedStyleMap,
Tldraw, Tldraw,
TldrawUiIcon,
TLEditorComponents, TLEditorComponents,
track, track,
useEditor, useEditor,
@ -85,7 +85,7 @@ const ContextToolbarComponent = track(() => {
editor.setStyleForSelectedShapes(DefaultSizeStyle, value, { squashing: false }) editor.setStyleForSelectedShapes(DefaultSizeStyle, value, { squashing: false })
} }
> >
<Icon icon={icon} /> <TldrawUiIcon icon={icon} />
</div> </div>
) )
})} })}

View file

@ -1,9 +1,9 @@
import { import {
Button,
DefaultQuickActions, DefaultQuickActions,
DefaultQuickActionsContent, DefaultQuickActionsContent,
TLComponents, TLComponents,
Tldraw, Tldraw,
TldrawUiMenuItem,
} from '@tldraw/tldraw' } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css' import '@tldraw/tldraw/tldraw.css'
@ -11,7 +11,7 @@ function CustomQuickActions() {
return ( return (
<DefaultQuickActions> <DefaultQuickActions>
<DefaultQuickActionsContent /> <DefaultQuickActionsContent />
<Button type="icon" icon="code" smallIcon /> <TldrawUiMenuItem id="code" icon="code" onSelect={() => window.alert('code')} />
</DefaultQuickActions> </DefaultQuickActions>
) )
} }

View file

@ -1,11 +1,12 @@
import { import {
Button,
DefaultColorStyle, DefaultColorStyle,
DefaultStylePanel, DefaultStylePanel,
DefaultStylePanelContent, DefaultStylePanelContent,
TLComponents, TLComponents,
TLUiStylePanelProps, TLUiStylePanelProps,
Tldraw, Tldraw,
TldrawUiButton,
TldrawUiButtonLabel,
useEditor, useEditor,
} from '@tldraw/tldraw' } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css' import '@tldraw/tldraw/tldraw.css'
@ -17,22 +18,22 @@ function CustomStylePanel(props: TLUiStylePanelProps) {
return ( return (
<DefaultStylePanel {...props}> <DefaultStylePanel {...props}>
<Button <TldrawUiButton
type="menu" type="menu"
onClick={() => { onClick={() => {
editor.setStyleForSelectedShapes(DefaultColorStyle, 'red', { squashing: true }) editor.setStyleForSelectedShapes(DefaultColorStyle, 'red', { squashing: true })
}} }}
> >
Red <TldrawUiButtonLabel>Red</TldrawUiButtonLabel>
</Button> </TldrawUiButton>
<Button <TldrawUiButton
type="menu" type="menu"
onClick={() => { onClick={() => {
editor.setStyleForSelectedShapes(DefaultColorStyle, 'green', { squashing: true }) editor.setStyleForSelectedShapes(DefaultColorStyle, 'green', { squashing: true })
}} }}
> >
Green <TldrawUiButtonLabel>Green</TldrawUiButtonLabel>
</Button> </TldrawUiButton>
<DefaultStylePanelContent relevantStyles={props.relevantStyles} /> <DefaultStylePanelContent relevantStyles={props.relevantStyles} />
</DefaultStylePanel> </DefaultStylePanel>
) )

View file

@ -17,6 +17,9 @@ const components: Required<TLUiComponents> = {
QuickActions: null, QuickActions: null,
HelperButtons: null, HelperButtons: null,
DebugMenu: null, DebugMenu: null,
SharePanel: null,
MenuPanel: null,
TopPanel: null,
} }
export default function UiComponentsHiddenExample() { export default function UiComponentsHiddenExample() {

View file

@ -1,13 +1,18 @@
import { Tldraw } from '@tldraw/tldraw' import { TLComponents, Tldraw } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css' import '@tldraw/tldraw/tldraw.css'
// There's a guide at the bottom of this file! // There's a guide at the bottom of this file!
const components: TLComponents = {
SharePanel: CustomShareZone,
TopPanel: CustomTopZone,
}
// [1] // [1]
export default function Example() { export default function Example() {
return ( return (
<div className="tldraw__editor"> <div className="tldraw__editor">
<Tldraw topZone={<CustomTopZone />} shareZone={<CustomShareZone />} /> <Tldraw components={components} />
</div> </div>
) )
} }
@ -46,8 +51,8 @@ function CustomShareZone() {
} }
/* /*
This example shows how to pass in a custom component to the share zone and top zone. This example shows how to pass in a custom component to the share panel and top panel.
The share zone is in the top right corner above the style menu, the top zone is in The share panel is in the top right corner above the style menu, the top panel is in
the top center. the top center.
[1] [1]

View file

@ -28,6 +28,7 @@ import { JSX as JSX_2 } from 'react/jsx-runtime';
import { LANGUAGES } from '@tldraw/editor'; import { LANGUAGES } from '@tldraw/editor';
import { Mat } from '@tldraw/editor'; import { Mat } from '@tldraw/editor';
import { MatModel } from '@tldraw/editor'; import { MatModel } from '@tldraw/editor';
import { MemoExoticComponent } from 'react';
import { MigrationFailureReason } from '@tldraw/editor'; import { MigrationFailureReason } from '@tldraw/editor';
import { Migrations } from '@tldraw/editor'; import { Migrations } from '@tldraw/editor';
import { NamedExoticComponent } from 'react'; import { NamedExoticComponent } from 'react';
@ -49,6 +50,7 @@ import { ShapeUtil } from '@tldraw/editor';
import { SharedStyle } from '@tldraw/editor'; import { SharedStyle } from '@tldraw/editor';
import { StateNode } from '@tldraw/editor'; import { StateNode } from '@tldraw/editor';
import { StoreSnapshot } from '@tldraw/editor'; import { StoreSnapshot } from '@tldraw/editor';
import { StyleProp } from '@tldraw/editor';
import { SvgExportContext } from '@tldraw/editor'; import { SvgExportContext } from '@tldraw/editor';
import { T } from '@tldraw/editor'; import { T } from '@tldraw/editor';
import { TLAnyShapeUtilConstructor } from '@tldraw/editor'; import { TLAnyShapeUtilConstructor } from '@tldraw/editor';
@ -265,9 +267,6 @@ export function BreakPointProvider({ forceMobile, children, }: {
// @internal (undocumented) // @internal (undocumented)
export function buildFromV1Document(editor: Editor, document: LegacyTldrawDocument): void; export function buildFromV1Document(editor: Editor, document: LegacyTldrawDocument): void;
// @public (undocumented)
export const Button: React_3.ForwardRefExoticComponent<TLUiButtonProps & React_3.RefAttributes<HTMLButtonElement>>;
// @public // @public
export function containBoxSize(originalSize: BoxWidthHeight, containBoxSize: BoxWidthHeight): BoxWidthHeight; export function containBoxSize(originalSize: BoxWidthHeight, containBoxSize: BoxWidthHeight): BoxWidthHeight;
@ -360,21 +359,6 @@ export const DefaultZoomMenu: NamedExoticComponent<TLUiZoomMenuProps>;
// @public (undocumented) // @public (undocumented)
export function DefaultZoomMenuContent(): JSX_2.Element; export function DefaultZoomMenuContent(): JSX_2.Element;
// @public (undocumented)
export function DialogBody({ className, children, style }: TLUiDialogBodyProps): JSX_2.Element;
// @public (undocumented)
export function DialogCloseButton(): JSX_2.Element;
// @public (undocumented)
export function DialogFooter({ className, children }: TLUiDialogFooterProps): JSX_2.Element;
// @public (undocumented)
export function DialogHeader({ className, children }: TLUiDialogHeaderProps): JSX_2.Element;
// @public (undocumented)
export function DialogTitle({ className, children }: TLUiDialogTitleProps): JSX_2.Element;
// @public // @public
export function downsizeImage(blob: Blob, width: number, height: number, opts?: { export function downsizeImage(blob: Blob, width: number, height: number, opts?: {
type?: string | undefined; type?: string | undefined;
@ -439,36 +423,6 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
static type: "draw"; static type: "draw";
} }
// @public (undocumented)
export function DropdownMenuCheckboxItem({ children, onSelect, ...rest }: TLUiDropdownMenuCheckboxItemProps): JSX_2.Element;
// @public (undocumented)
export function DropdownMenuContent({ side, align, sideOffset, alignOffset, children, }: TLUiDropdownMenuContentProps): JSX_2.Element;
// @public (undocumented)
export function DropdownMenuGroup({ children, size }: TLUiDropdownMenuGroupProps): JSX_2.Element;
// @public (undocumented)
export function DropdownMenuIndicator(): JSX_2.Element;
// @public (undocumented)
export function DropdownMenuItem({ noClose, ...props }: TLUiDropdownMenuItemProps): JSX_2.Element;
// @public (undocumented)
export function DropdownMenuRadioItem({ children, ...rest }: TLUiDropdownMenuRadioItemProps): JSX_2.Element;
// @public (undocumented)
export function DropdownMenuRoot({ id, children, modal, debugOpen, }: TLUiDropdownMenuRootProps): JSX_2.Element;
// @public (undocumented)
export function DropdownMenuSub({ id, children }: TLUiDropdownMenuSubProps): JSX_2.Element;
// @public (undocumented)
export function DropdownMenuSubTrigger({ label, title, disabled, }: TLUiDropdownMenuSubTriggerProps): JSX_2.Element;
// @public (undocumented)
export function DropdownMenuTrigger({ children, ...rest }: TLUiDropdownMenuTriggerProps): JSX_2.Element;
// @public (undocumented) // @public (undocumented)
export class EmbedShapeUtil extends BaseBoxShapeUtil<TLEmbedShape> { export class EmbedShapeUtil extends BaseBoxShapeUtil<TLEmbedShape> {
// (undocumented) // (undocumented)
@ -815,9 +769,6 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
static type: "highlight"; static type: "highlight";
} }
// @public (undocumented)
export const Icon: NamedExoticComponent<TLUiIconProps>;
// @public (undocumented) // @public (undocumented)
export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> { export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
// (undocumented) // (undocumented)
@ -856,9 +807,6 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
static type: "image"; static type: "image";
} }
// @public (undocumented)
export const Input: React_3.ForwardRefExoticComponent<TLUiInputProps & React_3.RefAttributes<HTMLInputElement>>;
// @public (undocumented) // @public (undocumented)
export function isGifAnimated(file: Blob): Promise<boolean>; export function isGifAnimated(file: Blob): Promise<boolean>;
@ -1059,15 +1007,6 @@ export function parseTldrawJsonFile({ json, schema, }: {
json: string; json: string;
}): Result<TLStore, TldrawFileParseError>; }): Result<TLStore, TldrawFileParseError>;
// @public (undocumented)
export function Popover({ id, children, onOpenChange, open }: TLUiPopoverProps): JSX_2.Element;
// @public (undocumented)
export function PopoverContent({ side, children, align, sideOffset, alignOffset, }: TLUiPopoverContentProps): JSX_2.Element;
// @public (undocumented)
export function PopoverTrigger({ children, ...rest }: TLUiPopoverTriggerProps): JSX_2.Element;
// @public // @public
export function removeFrame(editor: Editor, ids: TLShapeId[]): void; export function removeFrame(editor: Editor, ids: TLShapeId[]): void;
@ -1292,13 +1231,25 @@ export interface TldrawUiBaseProps {
components?: TLUiComponents; components?: TLUiComponents;
hideUi?: boolean; hideUi?: boolean;
renderDebugMenuItems?: () => React_2.ReactNode; renderDebugMenuItems?: () => React_2.ReactNode;
shareZone?: ReactNode;
// @internal
topZone?: ReactNode;
} }
// @public (undocumented) // @public (undocumented)
export function TldrawUiComponentsProvider({ overrides, children, }: ComponentsContextProviderProps): JSX_2.Element; export const TldrawUiButton: React_3.ForwardRefExoticComponent<TLUiButtonProps & React_3.RefAttributes<HTMLButtonElement>>;
// @public (undocumented)
export function TldrawUiButtonCheck({ checked }: TLUiButtonCheckProps): JSX_2.Element;
// @public (undocumented)
export function TldrawUiButtonIcon({ icon, small, invertIcon }: TLUiButtonIconProps): JSX_2.Element;
// @public (undocumented)
export function TldrawUiButtonLabel({ children }: TLUiButtonLabelProps): JSX_2.Element;
// @public (undocumented)
export const TldrawUiButtonPicker: MemoExoticComponent<(<T extends string>(props: TLUiButtonPickerProps<T>) => JSX_2.Element)>;
// @public (undocumented)
export function TldrawUiComponentsProvider({ overrides, children, }: TLUiComponentsProviderProps): JSX_2.Element;
// @public (undocumented) // @public (undocumented)
export function TldrawUiContextProvider({ overrides, components, assetUrls, onUiEvent, forceMobile, children, }: TldrawUiContextProviderProps): JSX_2.Element; export function TldrawUiContextProvider({ overrides, components, assetUrls, onUiEvent, forceMobile, children, }: TldrawUiContextProviderProps): JSX_2.Element;
@ -1313,6 +1264,57 @@ export interface TldrawUiContextProviderProps {
overrides?: TLUiOverrides | TLUiOverrides[]; overrides?: TLUiOverrides | TLUiOverrides[];
} }
// @public (undocumented)
export function TldrawUiDialogBody({ className, children, style }: TLUiDialogBodyProps): JSX_2.Element;
// @public (undocumented)
export function TldrawUiDialogCloseButton(): JSX_2.Element;
// @public (undocumented)
export function TldrawUiDialogFooter({ className, children }: TLUiDialogFooterProps): JSX_2.Element;
// @public (undocumented)
export function TldrawUiDialogHeader({ className, children }: TLUiDialogHeaderProps): JSX_2.Element;
// @public (undocumented)
export function TldrawUiDialogTitle({ className, children }: TLUiDialogTitleProps): JSX_2.Element;
// @public (undocumented)
export function TldrawUiDropdownMenuCheckboxItem({ children, onSelect, ...rest }: TLUiDropdownMenuCheckboxItemProps): JSX_2.Element;
// @public (undocumented)
export function TldrawUiDropdownMenuContent({ side, align, sideOffset, alignOffset, children, }: TLUiDropdownMenuContentProps): JSX_2.Element;
// @public (undocumented)
export function TldrawUiDropdownMenuGroup({ children, size, }: TLUiDropdownMenuGroupProps): JSX_2.Element;
// @public (undocumented)
export function TldrawUiDropdownMenuIndicator(): JSX_2.Element;
// @public (undocumented)
export function TldrawUiDropdownMenuItem({ noClose, children }: TLUiDropdownMenuItemProps): JSX_2.Element;
// @public (undocumented)
export function TldrawUiDropdownMenuRoot({ id, children, modal, debugOpen, }: TLUiDropdownMenuRootProps): JSX_2.Element;
// @public (undocumented)
export function TldrawUiDropdownMenuSub({ id, children }: TLUiDropdownMenuSubProps): JSX_2.Element;
// @public (undocumented)
export function TldrawUiDropdownMenuSubTrigger({ label, title, disabled, }: TLUiDropdownMenuSubTriggerProps): JSX_2.Element;
// @public (undocumented)
export function TldrawUiDropdownMenuTrigger({ children, ...rest }: TLUiDropdownMenuTriggerProps): JSX_2.Element;
// @public (undocumented)
export const TldrawUiIcon: NamedExoticComponent<TLUiIconProps>;
// @public (undocumented)
export const TldrawUiInput: React_3.ForwardRefExoticComponent<TLUiInputProps & React_3.RefAttributes<HTMLInputElement>>;
// @public (undocumented)
export function TldrawUiKbd({ children }: TLUiKbdProps): JSX_2.Element | null;
// @public (undocumented) // @public (undocumented)
export function TldrawUiMenuCheckboxItem<TranslationKey extends string = string, IconType extends string = string>({ id, kbd, label, readonlyOk, onSelect, disabled, checked, }: TLUiMenuCheckboxItemProps<TranslationKey, IconType>): JSX_2.Element | null; export function TldrawUiMenuCheckboxItem<TranslationKey extends string = string, IconType extends string = string>({ id, kbd, label, readonlyOk, onSelect, disabled, checked, }: TLUiMenuCheckboxItemProps<TranslationKey, IconType>): JSX_2.Element | null;
@ -1328,9 +1330,21 @@ export function TldrawUiMenuItem<TranslationKey extends string = string, IconTyp
// @public (undocumented) // @public (undocumented)
export function TldrawUiMenuSubmenu<Translation extends string = string>({ id, disabled, label, size, children, }: TLUiMenuSubmenuProps<Translation>): any; export function TldrawUiMenuSubmenu<Translation extends string = string>({ id, disabled, label, size, children, }: TLUiMenuSubmenuProps<Translation>): any;
// @public (undocumented)
export function TldrawUiPopover({ id, children, onOpenChange, open }: TLUiPopoverProps): JSX_2.Element;
// @public (undocumented)
export function TldrawUiPopoverContent({ side, children, align, sideOffset, alignOffset, }: TLUiPopoverContentProps): JSX_2.Element;
// @public (undocumented)
export function TldrawUiPopoverTrigger({ children }: TLUiPopoverTriggerProps): JSX_2.Element;
// @public // @public
export type TldrawUiProps = TldrawUiBaseProps & TldrawUiContextProviderProps; export type TldrawUiProps = TldrawUiBaseProps & TldrawUiContextProviderProps;
// @internal (undocumented)
export const TldrawUiSlider: NamedExoticComponent<TLUiSliderProps>;
// @public (undocumented) // @public (undocumented)
export interface TLUiActionItem<TransationKey extends string = string, IconType extends string = string> { export interface TLUiActionItem<TransationKey extends string = string, IconType extends string = string> {
// (undocumented) // (undocumented)
@ -1364,29 +1378,44 @@ export type TLUiActionsMenuProps = {
// @public (undocumented) // @public (undocumented)
export type TLUiAssetUrlOverrides = RecursivePartial<TLUiAssetUrls>; export type TLUiAssetUrlOverrides = RecursivePartial<TLUiAssetUrls>;
// @public (undocumented)
export type TLUiButtonCheckProps = {
checked: boolean;
};
// @public (undocumented)
export type TLUiButtonIconProps = {
icon: string;
small?: boolean;
invertIcon?: boolean;
};
// @public (undocumented)
export type TLUiButtonLabelProps = {
children?: any;
};
// @public (undocumented)
export interface TLUiButtonPickerProps<T extends string> {
// (undocumented)
items: StyleValuesForUi<T>;
// (undocumented)
onValueChange: (style: StyleProp<T>, value: T, squashing: boolean) => void;
// (undocumented)
style: StyleProp<T>;
// (undocumented)
title: string;
// (undocumented)
uiType: string;
// (undocumented)
value: SharedStyle<T>;
}
// @public (undocumented) // @public (undocumented)
export interface TLUiButtonProps extends React_3.HTMLAttributes<HTMLButtonElement> { export interface TLUiButtonProps extends React_3.HTMLAttributes<HTMLButtonElement> {
// (undocumented) // (undocumented)
disabled?: boolean; disabled?: boolean;
// (undocumented) // (undocumented)
icon?: Exclude<string, TLUiIconType> | TLUiIconType;
// (undocumented)
iconLeft?: Exclude<string, TLUiIconType> | TLUiIconType;
// (undocumented)
invertIcon?: boolean;
// (undocumented)
isChecked?: boolean;
// (undocumented)
kbd?: string;
// (undocumented)
label?: Exclude<string, TLUiTranslationKey> | TLUiTranslationKey;
// (undocumented)
loading?: boolean;
// (undocumented)
smallIcon?: boolean;
// (undocumented)
spinner?: boolean;
// (undocumented)
type: 'danger' | 'help' | 'icon' | 'low' | 'menu' | 'normal' | 'primary' | 'tool'; type: 'danger' | 'help' | 'icon' | 'low' | 'menu' | 'normal' | 'primary' | 'tool';
} }
@ -1395,6 +1424,12 @@ export type TLUiComponents = Partial<{
[K in keyof BaseTLUiComponents]: BaseTLUiComponents[K] | null; [K in keyof BaseTLUiComponents]: BaseTLUiComponents[K] | null;
}>; }>;
// @public (undocumented)
export type TLUiComponentsProviderProps = {
overrides?: TLUiComponents;
children: any;
};
// @public (undocumented) // @public (undocumented)
export interface TLUiContextMenuProps { export interface TLUiContextMenuProps {
// (undocumented) // (undocumented)
@ -1491,23 +1526,11 @@ export type TLUiDropdownMenuGroupProps = {
}; };
// @public (undocumented) // @public (undocumented)
export interface TLUiDropdownMenuItemProps extends TLUiButtonProps { export interface TLUiDropdownMenuItemProps {
// (undocumented)
noClose?: boolean;
}
// @public (undocumented)
export interface TLUiDropdownMenuRadioItemProps {
// (undocumented)
checked?: boolean;
// (undocumented) // (undocumented)
children: any; children: any;
// (undocumented) // (undocumented)
disabled?: boolean; noClose?: boolean;
// (undocumented)
onSelect?: (e: Event) => void;
// (undocumented)
title: string;
} }
// @public (undocumented) // @public (undocumented)
@ -1533,7 +1556,7 @@ export type TLUiDropdownMenuSubTriggerProps = {
}; };
// @public (undocumented) // @public (undocumented)
export interface TLUiDropdownMenuTriggerProps extends TLUiButtonProps { export interface TLUiDropdownMenuTriggerProps {
// (undocumented) // (undocumented)
children?: any; children?: any;
} }
@ -1772,6 +1795,12 @@ export interface TLUiInputProps {
value?: string; value?: string;
} }
// @public (undocumented)
export interface TLUiKbdProps {
// (undocumented)
children: string;
}
// @public (undocumented) // @public (undocumented)
export type TLUiKeyboardShortcutsDialogProps = TLUiDialogProps & { export type TLUiKeyboardShortcutsDialogProps = TLUiDialogProps & {
children?: any; children?: any;
@ -1867,7 +1896,7 @@ export type TLUiPopoverProps = {
}; };
// @public (undocumented) // @public (undocumented)
export interface TLUiPopoverTriggerProps extends TLUiButtonProps { export interface TLUiPopoverTriggerProps {
// (undocumented) // (undocumented)
children?: React_2.ReactNode; children?: React_2.ReactNode;
} }
@ -1877,6 +1906,22 @@ export type TLUiQuickActionsProps = {
children?: any; children?: any;
}; };
// @internal (undocumented)
export interface TLUiSliderProps {
// (undocumented)
'data-testid'?: string;
// (undocumented)
label: string;
// (undocumented)
onValueChange: (value: number, emphemeral: boolean) => void;
// (undocumented)
steps: number;
// (undocumented)
title: string;
// (undocumented)
value: null | number;
}
// @public (undocumented) // @public (undocumented)
export type TLUiStylePanelContentProps = { export type TLUiStylePanelContentProps = {
relevantStyles: ReturnType<typeof useRelevantStyles>; relevantStyles: ReturnType<typeof useRelevantStyles>;
@ -2080,6 +2125,9 @@ export function useTldrawUiComponents(): Partial<{
QuickActions: ComponentType<TLUiQuickActionsProps> | null; QuickActions: ComponentType<TLUiQuickActionsProps> | null;
HelperButtons: ComponentType<TLUiHelperButtonsProps> | null; HelperButtons: ComponentType<TLUiHelperButtonsProps> | null;
DebugMenu: ComponentType | null; DebugMenu: ComponentType | null;
MenuPanel: ComponentType | null;
TopPanel: ComponentType | null;
SharePanel: ComponentType | null;
}>; }>;
// @public (undocumented) // @public (undocumented)

File diff suppressed because it is too large Load diff

View file

@ -42,9 +42,6 @@ export { TldrawUi, type TldrawUiBaseProps, type TldrawUiProps } from './lib/ui/T
export { setDefaultUiAssetUrls, type TLUiAssetUrlOverrides } from './lib/ui/assetUrls' export { setDefaultUiAssetUrls, type TLUiAssetUrlOverrides } from './lib/ui/assetUrls'
export { OfflineIndicator } from './lib/ui/components/OfflineIndicator/OfflineIndicator' export { OfflineIndicator } from './lib/ui/components/OfflineIndicator/OfflineIndicator'
export { Spinner } from './lib/ui/components/Spinner' export { Spinner } from './lib/ui/components/Spinner'
export { Button, type TLUiButtonProps } from './lib/ui/components/primitives/Button'
export { Icon, type TLUiIconProps } from './lib/ui/components/primitives/Icon'
export { Input, type TLUiInputProps } from './lib/ui/components/primitives/Input'
export { export {
TldrawUiContextProvider, TldrawUiContextProvider,
type TldrawUiContextProviderProps, type TldrawUiContextProviderProps,
@ -99,7 +96,7 @@ export {
export { type TLUiTranslationKey } from './lib/ui/hooks/useTranslation/TLUiTranslationKey' export { type TLUiTranslationKey } from './lib/ui/hooks/useTranslation/TLUiTranslationKey'
export { type TLUiTranslation } from './lib/ui/hooks/useTranslation/translations' export { type TLUiTranslation } from './lib/ui/hooks/useTranslation/translations'
export { export {
useTranslation as useTranslation, useTranslation,
type TLUiTranslationContextType, type TLUiTranslationContextType,
} from './lib/ui/hooks/useTranslation/useTranslation' } from './lib/ui/hooks/useTranslation/useTranslation'
export { type TLUiIconType } from './lib/ui/icon-types' export { type TLUiIconType } from './lib/ui/icon-types'
@ -137,35 +134,13 @@ export { DefaultMinimap } from './lib/ui/components/Minimap/DefaultMinimap'
// Helper to unwrap label from action items // Helper to unwrap label from action items
export { unwrapLabel } from './lib/ui/context/actions' export { unwrapLabel } from './lib/ui/context/actions'
// General UI components for building menus
export {
TldrawUiMenuCheckboxItem,
type TLUiMenuCheckboxItemProps,
} from './lib/ui/components/menus/TldrawUiMenuCheckboxItem'
export {
TldrawUiMenuContextProvider,
type TLUiMenuContextProviderProps,
} from './lib/ui/components/menus/TldrawUiMenuContext'
export {
TldrawUiMenuGroup,
type TLUiMenuGroupProps,
} from './lib/ui/components/menus/TldrawUiMenuGroup'
export {
TldrawUiMenuItem,
type TLUiMenuItemProps,
} from './lib/ui/components/menus/TldrawUiMenuItem'
export {
TldrawUiMenuSubmenu,
type TLUiMenuSubmenuProps,
} from './lib/ui/components/menus/TldrawUiMenuSubmenu'
export { export {
TldrawUiComponentsProvider, TldrawUiComponentsProvider,
useTldrawUiComponents, useTldrawUiComponents,
type TLUiComponents, type TLUiComponents,
type TLUiComponentsProviderProps,
} from './lib/ui/context/components' } from './lib/ui/context/components'
// Menus / UI elements that can be customized
export { DefaultPageMenu } from './lib/ui/components/PageMenu/DefaultPageMenu' export { DefaultPageMenu } from './lib/ui/components/PageMenu/DefaultPageMenu'
export { export {
@ -236,45 +211,108 @@ export { DefaultToolbar } from './lib/ui/components/Toolbar/DefaultToolbar'
export { type TLComponents } from './lib/Tldraw' export { type TLComponents } from './lib/Tldraw'
/* ------------------- Primitives ------------------- */
// Button
export { export {
DialogBody, TldrawUiButton,
DialogCloseButton, type TLUiButtonProps,
DialogFooter, } from './lib/ui/components/primitives/Button/TldrawUiButton'
DialogHeader, export {
DialogTitle, TldrawUiButtonCheck,
type TLUiButtonCheckProps,
} from './lib/ui/components/primitives/Button/TldrawUiButtonCheck'
export {
TldrawUiButtonIcon,
type TLUiButtonIconProps,
} from './lib/ui/components/primitives/Button/TldrawUiButtonIcon'
export {
TldrawUiButtonLabel,
type TLUiButtonLabelProps,
} from './lib/ui/components/primitives/Button/TldrawUiButtonLabel'
// Button picker
export {
TldrawUiButtonPicker,
type TLUiButtonPickerProps,
} from './lib/ui/components/primitives/TldrawUiButtonPicker'
// Dialog
export {
TldrawUiDialogBody,
TldrawUiDialogCloseButton,
TldrawUiDialogFooter,
TldrawUiDialogHeader,
TldrawUiDialogTitle,
type TLUiDialogBodyProps, type TLUiDialogBodyProps,
type TLUiDialogFooterProps, type TLUiDialogFooterProps,
type TLUiDialogHeaderProps, type TLUiDialogHeaderProps,
type TLUiDialogTitleProps, type TLUiDialogTitleProps,
} from './lib/ui/components/primitives/Dialog' } from './lib/ui/components/primitives/TldrawUiDialog'
// Dropdown Menu
export { export {
DropdownMenuCheckboxItem, TldrawUiDropdownMenuCheckboxItem,
DropdownMenuContent, TldrawUiDropdownMenuContent,
DropdownMenuGroup, TldrawUiDropdownMenuGroup,
DropdownMenuIndicator, TldrawUiDropdownMenuIndicator,
DropdownMenuItem, TldrawUiDropdownMenuItem,
DropdownMenuRadioItem, TldrawUiDropdownMenuRoot,
DropdownMenuRoot, TldrawUiDropdownMenuSub,
DropdownMenuSub, TldrawUiDropdownMenuSubTrigger,
DropdownMenuSubTrigger, TldrawUiDropdownMenuTrigger,
DropdownMenuTrigger,
type TLUiDropdownMenuCheckboxItemProps, type TLUiDropdownMenuCheckboxItemProps,
type TLUiDropdownMenuContentProps, type TLUiDropdownMenuContentProps,
type TLUiDropdownMenuGroupProps, type TLUiDropdownMenuGroupProps,
type TLUiDropdownMenuItemProps, type TLUiDropdownMenuItemProps,
type TLUiDropdownMenuRadioItemProps,
type TLUiDropdownMenuRootProps, type TLUiDropdownMenuRootProps,
type TLUiDropdownMenuSubProps, type TLUiDropdownMenuSubProps,
type TLUiDropdownMenuSubTriggerProps, type TLUiDropdownMenuSubTriggerProps,
type TLUiDropdownMenuTriggerProps, type TLUiDropdownMenuTriggerProps,
} from './lib/ui/components/primitives/DropdownMenu' } from './lib/ui/components/primitives/TldrawUiDropdownMenu'
// Icon
export { TldrawUiIcon, type TLUiIconProps } from './lib/ui/components/primitives/TldrawUiIcon'
// Input
export { TldrawUiInput, type TLUiInputProps } from './lib/ui/components/primitives/TldrawUiInput'
// Kbd
export { TldrawUiKbd, type TLUiKbdProps } from './lib/ui/components/primitives/TldrawUiKbd'
// Popover
export { export {
Popover, TldrawUiPopover,
PopoverContent, TldrawUiPopoverContent,
PopoverTrigger, TldrawUiPopoverTrigger,
type TLUiPopoverContentProps, type TLUiPopoverContentProps,
type TLUiPopoverProps, type TLUiPopoverProps,
type TLUiPopoverTriggerProps, type TLUiPopoverTriggerProps,
} from './lib/ui/components/primitives/Popover' } from './lib/ui/components/primitives/TldrawUiPopover'
// Slider
export { TldrawUiSlider, type TLUiSliderProps } from './lib/ui/components/primitives/TldrawUiSlider'
/* ----------------- Menu Primitives ---------------- */
// General UI components for building menus
export {
TldrawUiMenuCheckboxItem,
type TLUiMenuCheckboxItemProps,
} from './lib/ui/components/primitives/menus/TldrawUiMenuCheckboxItem'
export {
TldrawUiMenuContextProvider,
type TLUiMenuContextProviderProps,
} from './lib/ui/components/primitives/menus/TldrawUiMenuContext'
export {
TldrawUiMenuGroup,
type TLUiMenuGroupProps,
} from './lib/ui/components/primitives/menus/TldrawUiMenuGroup'
export {
TldrawUiMenuItem,
type TLUiMenuItemProps,
} from './lib/ui/components/primitives/menus/TldrawUiMenuItem'
export {
TldrawUiMenuSubmenu,
type TLUiMenuSubmenuProps,
} from './lib/ui/components/primitives/menus/TldrawUiMenuSubmenu'

View file

@ -3,6 +3,7 @@ export type StyleValuesForUi<T> = readonly {
readonly icon: string readonly icon: string
}[] }[]
// todo: default styles prop?
export const STYLES = { export const STYLES = {
color: [ color: [
{ value: 'black', icon: 'color' }, { value: 'black', icon: 'color' },

View file

@ -75,6 +75,10 @@
opacity: 1; opacity: 1;
} }
.tlui-button__icon + .tlui-button__label {
margin-left: var(--space-2);
}
@media (hover: hover) { @media (hover: hover) {
.tlui-button::after { .tlui-button::after {
background-color: var(--color-muted-2); background-color: var(--color-muted-2);
@ -127,46 +131,6 @@
position: relative; position: relative;
} }
/* Icon button */
.tlui-button__icon {
height: 40px;
width: 40px;
min-height: 40px;
min-width: 40px;
padding: 0px;
}
.tlui-button__icon-left {
margin-right: var(--space-2);
}
/* Hinted */
.tlui-button__icon[data-state='hinted']::after {
background: var(--color-hint);
opacity: 1;
/* box-shadow: inset 0 0 0 1px var(--color-muted-1); */
}
.tlui-button__icon[data-state='hinted']:not(:disabled, :focus-visible):active::after {
background: var(--color-hint);
opacity: 1;
/* box-shadow: inset 0 0 0 1px var(--color-text-3); */
}
@media (hover: hover) {
.tlui-button__icon::after {
inset: 4px;
border-radius: var(--radius-2);
}
.tlui-button__icon[data-state='hinted']:not(:disabled, :focus-visible):hover::after {
background: var(--color-hint);
/* box-shadow: inset 0 0 0 1px var(--color-text-3); */
}
}
/* Menu button */ /* Menu button */
.tlui-button__menu { .tlui-button__menu {
@ -1225,11 +1189,6 @@
width: 128px; width: 128px;
} }
.tlui-page-menu__trigger > span {
flex-grow: 2;
margin-right: var(--space-4);
}
.tlui-page-menu__header { .tlui-page-menu__header {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -1373,17 +1332,29 @@
.tlui-page_menu__item__submenu[data-isediting='true'] { .tlui-page_menu__item__submenu[data-isediting='true'] {
display: block; display: block;
opacity: 1;
}
.tlui-page_menu__item__submenu > .tlui-button {
opacity: 0;
}
.tlui-page-menu__item__button .tlui-button__icon {
margin-right: 4px;
} }
@media (hover: hover) { @media (hover: hover) {
.tlui-page_menu__item__submenu { .tlui-page_menu__item__submenu {
opacity: 0;
display: block; display: block;
} }
.tlui-page_menu__item__submenu:hover, .tlui-page_menu__item__submenu[data-isediting='true'] > .tlui-button {
.tlui-page-menu__item:focus-within > .tlui-page_menu__item__submenu, opacity: 0;
.tlui-page_menu__item__sortable:focus-within > .tlui-page_menu__item__submenu { }
.tlui-page_menu__item__submenu > .tlui-button[data-state='open'],
.tlui-page_menu__item__submenu:hover > .tlui-button,
.tlui-page_menu__item__sortable:focus-within > .tlui-page_menu__item__submenu > .tlui-button {
opacity: 1; opacity: 1;
} }
} }

View file

@ -6,9 +6,9 @@ import { TLUiAssetUrlOverrides } from './assetUrls'
import { DebugPanel } from './components/DebugPanel' import { DebugPanel } from './components/DebugPanel'
import { Dialogs } from './components/Dialogs' import { Dialogs } from './components/Dialogs'
import { FollowingIndicator } from './components/FollowingIndicator' import { FollowingIndicator } from './components/FollowingIndicator'
import { MenuZone } from './components/MenuZone'
import { ToastViewport, Toasts } from './components/Toasts' import { ToastViewport, Toasts } from './components/Toasts'
import { Button } from './components/primitives/Button' import { TldrawUiButton } from './components/primitives/Button/TldrawUiButton'
import { TldrawUiButtonIcon } from './components/primitives/Button/TldrawUiButtonIcon'
import { PORTRAIT_BREAKPOINT } from './constants' import { PORTRAIT_BREAKPOINT } from './constants'
import { import {
TldrawUiContextProvider, TldrawUiContextProvider,
@ -20,6 +20,7 @@ import { TLUiComponents, useTldrawUiComponents } from './context/components'
import { useNativeClipboardEvents } from './hooks/useClipboardEvents' import { useNativeClipboardEvents } from './hooks/useClipboardEvents'
import { useEditorEvents } from './hooks/useEditorEvents' import { useEditorEvents } from './hooks/useEditorEvents'
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts' import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'
import { useReadonly } from './hooks/useReadonly'
import { useRelevantStyles } from './hooks/useRevelantStyles' import { useRelevantStyles } from './hooks/useRevelantStyles'
import { useTranslation } from './hooks/useTranslation/useTranslation' import { useTranslation } from './hooks/useTranslation/useTranslation'
@ -51,17 +52,6 @@ export interface TldrawUiBaseProps {
*/ */
components?: TLUiComponents components?: TLUiComponents
/**
* A component to use for the share zone (will be deprecated)
*/
shareZone?: ReactNode
/**
* A component to use for the top zone (will be deprecated)
* @internal
*/
topZone?: ReactNode
/** /**
* Additional items to add to the debug menu (will be deprecated) * Additional items to add to the debug menu (will be deprecated)
*/ */
@ -75,8 +65,6 @@ export interface TldrawUiBaseProps {
* @public * @public
*/ */
export const TldrawUi = React.memo(function TldrawUi({ export const TldrawUi = React.memo(function TldrawUi({
shareZone,
topZone,
renderDebugMenuItems, renderDebugMenuItems,
children, children,
hideUi, hideUi,
@ -85,12 +73,7 @@ export const TldrawUi = React.memo(function TldrawUi({
}: TldrawUiProps) { }: TldrawUiProps) {
return ( return (
<TldrawUiContextProvider {...rest} components={components}> <TldrawUiContextProvider {...rest} components={components}>
<TldrawUiInner <TldrawUiInner hideUi={hideUi} renderDebugMenuItems={renderDebugMenuItems}>
hideUi={hideUi}
shareZone={shareZone}
topZone={topZone}
renderDebugMenuItems={renderDebugMenuItems}
>
{children} {children}
</TldrawUiInner> </TldrawUiInner>
</TldrawUiContextProvider> </TldrawUiContextProvider>
@ -121,17 +104,24 @@ const TldrawUiInner = React.memo(function TldrawUiInner({
) )
}) })
const TldrawUiContent = React.memo(function TldrawUI({ shareZone, topZone }: TldrawUiContentProps) { const TldrawUiContent = React.memo(function TldrawUI() {
const editor = useEditor() const editor = useEditor()
const msg = useTranslation() const msg = useTranslation()
const breakpoint = useBreakpoint() const breakpoint = useBreakpoint()
const isReadonlyMode = useValue('isReadonlyMode', () => editor.getInstanceState().isReadonly, [ const isReadonlyMode = useReadonly()
editor,
])
const isFocusMode = useValue('focus', () => editor.getInstanceState().isFocusMode, [editor]) const isFocusMode = useValue('focus', () => editor.getInstanceState().isFocusMode, [editor])
const isDebugMode = useValue('debug', () => editor.getInstanceState().isDebugMode, [editor]) const isDebugMode = useValue('debug', () => editor.getInstanceState().isDebugMode, [editor])
const { StylePanel, Toolbar, HelpMenu, NavigationPanel, HelperButtons } = useTldrawUiComponents() const {
SharePanel,
TopPanel,
MenuPanel,
StylePanel,
Toolbar,
HelpMenu,
NavigationPanel,
HelperButtons,
} = useTldrawUiComponents()
useKeyboardShortcuts() useKeyboardShortcuts()
useNativeClipboardEvents() useNativeClipboardEvents()
@ -149,24 +139,25 @@ const TldrawUiContent = React.memo(function TldrawUI({ shareZone, topZone }: Tld
> >
{isFocusMode ? ( {isFocusMode ? (
<div className="tlui-layout__top"> <div className="tlui-layout__top">
<Button <TldrawUiButton
type="icon" type="icon"
className="tlui-focus-button" className="tlui-focus-button"
title={`${msg('focus-mode.toggle-focus-mode')}`} title={msg('focus-mode.toggle-focus-mode')}
icon="dot"
onClick={() => toggleFocus.onSelect('menu')} onClick={() => toggleFocus.onSelect('menu')}
/> >
<TldrawUiButtonIcon icon="dot" />
</TldrawUiButton>
</div> </div>
) : ( ) : (
<> <>
<div className="tlui-layout__top"> <div className="tlui-layout__top">
<div className="tlui-layout__top__left"> <div className="tlui-layout__top__left">
<MenuZone /> {MenuPanel && <MenuPanel />}
{HelperButtons && <HelperButtons />} {HelperButtons && <HelperButtons />}
</div> </div>
<div className="tlui-layout__top__center">{topZone}</div> <div className="tlui-layout__top__center">{TopPanel && <TopPanel />}</div>
<div className="tlui-layout__top__right"> <div className="tlui-layout__top__right">
{shareZone} {SharePanel && <SharePanel />}
{StylePanel && breakpoint >= PORTRAIT_BREAKPOINT.TABLET_SM && !isReadonlyMode && ( {StylePanel && breakpoint >= PORTRAIT_BREAKPOINT.TABLET_SM && !isReadonlyMode && (
<_StylePanel /> <_StylePanel />
)} )}

View file

@ -4,8 +4,14 @@ import { PORTRAIT_BREAKPOINT } from '../../constants'
import { useBreakpoint } from '../../context/breakpoints' import { useBreakpoint } from '../../context/breakpoints'
import { useReadonly } from '../../hooks/useReadonly' import { useReadonly } from '../../hooks/useReadonly'
import { useTranslation } from '../../hooks/useTranslation/useTranslation' import { useTranslation } from '../../hooks/useTranslation/useTranslation'
import { TldrawUiMenuContextProvider } from '../menus/TldrawUiMenuContext' import { TldrawUiButton } from '../primitives/Button/TldrawUiButton'
import { Popover, PopoverContent, PopoverTrigger } from '../primitives/Popover' import { TldrawUiButtonIcon } from '../primitives/Button/TldrawUiButtonIcon'
import {
TldrawUiPopover,
TldrawUiPopoverContent,
TldrawUiPopoverTrigger,
} from '../primitives/TldrawUiPopover'
import { TldrawUiMenuContextProvider } from '../primitives/menus/TldrawUiMenuContext'
import { DefaultActionsMenuContent } from './DefaultActionsMenuContent' import { DefaultActionsMenuContent } from './DefaultActionsMenuContent'
/** @public */ /** @public */
@ -37,16 +43,17 @@ export const DefaultActionsMenu = memo(function DefaultActionsMenu({
if (isReadonlyMode && !isInAcceptableReadonlyState) return if (isReadonlyMode && !isInAcceptableReadonlyState) return
return ( return (
<Popover id="actions-menu"> <TldrawUiPopover id="actions-menu">
<PopoverTrigger <TldrawUiPopoverTrigger>
className="tlui-menu__trigger" <TldrawUiButton
type="icon"
data-testid="main.action-menu" data-testid="main.action-menu"
icon="dots-vertical"
title={msg('actions-menu.title')} title={msg('actions-menu.title')}
type="icon" // needs to be here because the trigger also passes down type="button" >
smallIcon <TldrawUiButtonIcon icon="dots-vertical" small />
/> </TldrawUiButton>
<PopoverContent </TldrawUiPopoverTrigger>
<TldrawUiPopoverContent
side={breakpoint >= PORTRAIT_BREAKPOINT.TABLET ? 'bottom' : 'top'} side={breakpoint >= PORTRAIT_BREAKPOINT.TABLET ? 'bottom' : 'top'}
sideOffset={6} sideOffset={6}
> >
@ -55,7 +62,7 @@ export const DefaultActionsMenu = memo(function DefaultActionsMenu({
{content} {content}
</TldrawUiMenuContextProvider> </TldrawUiMenuContextProvider>
</div> </div>
</PopoverContent> </TldrawUiPopoverContent>
</Popover> </TldrawUiPopover>
) )
}) })

View file

@ -9,7 +9,7 @@ import {
useThreeStackableItems, useThreeStackableItems,
useUnlockedSelectedShapesCount, useUnlockedSelectedShapesCount,
} from '../../hooks/menu-hooks' } from '../../hooks/menu-hooks'
import { TldrawUiMenuItem } from '../menus/TldrawUiMenuItem' import { TldrawUiMenuItem } from '../primitives/menus/TldrawUiMenuItem'
/** @public */ /** @public */
export function DefaultActionsMenuContent() { export function DefaultActionsMenuContent() {

View file

@ -2,7 +2,7 @@ import * as _ContextMenu from '@radix-ui/react-context-menu'
import { preventDefault, useContainer, useEditor } from '@tldraw/editor' import { preventDefault, useContainer, useEditor } from '@tldraw/editor'
import { memo, useCallback } from 'react' import { memo, useCallback } from 'react'
import { useMenuIsOpen } from '../../hooks/useMenuIsOpen' import { useMenuIsOpen } from '../../hooks/useMenuIsOpen'
import { TldrawUiMenuContextProvider } from '../menus/TldrawUiMenuContext' import { TldrawUiMenuContextProvider } from '../primitives/menus/TldrawUiMenuContext'
import { DefaultContextMenuContent } from './DefaultContextMenuContent' import { DefaultContextMenuContent } from './DefaultContextMenuContent'
/** @public */ /** @public */

View file

@ -17,7 +17,7 @@ import {
ToggleLockMenuItem, ToggleLockMenuItem,
UngroupMenuItem, UngroupMenuItem,
} from '../menu-items' } from '../menu-items'
import { TldrawUiMenuGroup } from '../menus/TldrawUiMenuGroup' import { TldrawUiMenuGroup } from '../primitives/menus/TldrawUiMenuGroup'
/** @public */ /** @public */
export function DefaultContextMenuContent() { export function DefaultContextMenuContent() {

View file

@ -1,10 +1,12 @@
import { useTranslation } from '../../hooks/useTranslation/useTranslation' import { useTranslation } from '../../hooks/useTranslation/useTranslation'
import { TldrawUiMenuContextProvider } from '../menus/TldrawUiMenuContext' import { TldrawUiButton } from '../primitives/Button/TldrawUiButton'
import { TldrawUiButtonIcon } from '../primitives/Button/TldrawUiButtonIcon'
import { import {
DropdownMenuContent, TldrawUiDropdownMenuContent,
DropdownMenuRoot, TldrawUiDropdownMenuRoot,
DropdownMenuTrigger, TldrawUiDropdownMenuTrigger,
} from '../primitives/DropdownMenu' } from '../primitives/TldrawUiDropdownMenu'
import { TldrawUiMenuContextProvider } from '../primitives/menus/TldrawUiMenuContext'
import { DefaultDebugMenuContent } from './DefaultDebugMenuContent' import { DefaultDebugMenuContent } from './DefaultDebugMenuContent'
/** @public */ /** @public */
@ -18,13 +20,17 @@ export function DefaultDebugMenu({ children }: TLUiDebugMenuProps) {
const content = children ?? <DefaultDebugMenuContent /> const content = children ?? <DefaultDebugMenuContent />
return ( return (
<DropdownMenuRoot id="debug"> <TldrawUiDropdownMenuRoot id="debug">
<DropdownMenuTrigger type="icon" icon="dots-horizontal" title={msg('debug-panel.more')} /> <TldrawUiDropdownMenuTrigger>
<DropdownMenuContent side="top" align="end" alignOffset={0}> <TldrawUiButton type="icon" title={msg('debug-menu.title')}>
<TldrawUiButtonIcon icon="dots-horizontal" />
</TldrawUiButton>
</TldrawUiDropdownMenuTrigger>
<TldrawUiDropdownMenuContent side="top" align="end" alignOffset={0}>
<TldrawUiMenuContextProvider type="menu" sourceId="debug-panel"> <TldrawUiMenuContextProvider type="menu" sourceId="debug-panel">
{content} {content}
</TldrawUiMenuContextProvider> </TldrawUiMenuContextProvider>
</DropdownMenuContent> </TldrawUiDropdownMenuContent>
</DropdownMenuRoot> </TldrawUiDropdownMenuRoot>
) )
} }

View file

@ -1,4 +1,3 @@
import { DialogTitle } from '@radix-ui/react-dialog'
import { import {
DebugFlag, DebugFlag,
Editor, Editor,
@ -15,12 +14,20 @@ import React from 'react'
import { useDialogs } from '../../context/dialogs' import { useDialogs } from '../../context/dialogs'
import { useToasts } from '../../context/toasts' import { useToasts } from '../../context/toasts'
import { untranslated } from '../../hooks/useTranslation/useTranslation' import { untranslated } from '../../hooks/useTranslation/useTranslation'
import { TldrawUiMenuCheckboxItem } from '../menus/TldrawUiMenuCheckboxItem' import { TldrawUiButton } from '../primitives/Button/TldrawUiButton'
import { TldrawUiMenuGroup } from '../menus/TldrawUiMenuGroup' import { TldrawUiButtonCheck } from '../primitives/Button/TldrawUiButtonCheck'
import { TldrawUiMenuItem } from '../menus/TldrawUiMenuItem' import { TldrawUiButtonLabel } from '../primitives/Button/TldrawUiButtonLabel'
import { TldrawUiMenuSubmenu } from '../menus/TldrawUiMenuSubmenu' import {
import { Button } from '../primitives/Button' TldrawUiDialogBody,
import { DialogBody, DialogCloseButton, DialogFooter, DialogHeader } from '../primitives/Dialog' TldrawUiDialogCloseButton,
TldrawUiDialogFooter,
TldrawUiDialogHeader,
TldrawUiDialogTitle,
} from '../primitives/TldrawUiDialog'
import { TldrawUiMenuCheckboxItem } from '../primitives/menus/TldrawUiMenuCheckboxItem'
import { TldrawUiMenuGroup } from '../primitives/menus/TldrawUiMenuGroup'
import { TldrawUiMenuItem } from '../primitives/menus/TldrawUiMenuItem'
import { TldrawUiMenuSubmenu } from '../primitives/menus/TldrawUiMenuSubmenu'
/** @public */ /** @public */
export function DefaultDebugMenuContent() { export function DefaultDebugMenuContent() {
@ -225,29 +232,29 @@ function ExampleDialog({
return ( return (
<> <>
<DialogHeader> <TldrawUiDialogHeader>
<DialogTitle>{title}</DialogTitle> <TldrawUiDialogTitle>{title}</TldrawUiDialogTitle>
<DialogCloseButton /> <TldrawUiDialogCloseButton />
</DialogHeader> </TldrawUiDialogHeader>
<DialogBody style={{ maxWidth: 350 }}>{body}</DialogBody> <TldrawUiDialogBody style={{ maxWidth: 350 }}>{body}</TldrawUiDialogBody>
<DialogFooter className="tlui-dialog__footer__actions"> <TldrawUiDialogFooter className="tlui-dialog__footer__actions">
{displayDontShowAgain && ( {displayDontShowAgain && (
<Button <TldrawUiButton
type="normal" type="normal"
onClick={() => setDontShowAgain(!dontShowAgain)} onClick={() => setDontShowAgain(!dontShowAgain)}
iconLeft={dontShowAgain ? 'check' : 'checkbox-empty'}
style={{ marginRight: 'auto' }} style={{ marginRight: 'auto' }}
> >
{`Don't show again`} <TldrawUiButtonCheck checked={dontShowAgain} />
</Button> <TldrawUiButtonLabel>Don't show again</TldrawUiButtonLabel>
</TldrawUiButton>
)} )}
<Button type="normal" onClick={onCancel}> <TldrawUiButton type="normal" onClick={onCancel}>
{cancel} <TldrawUiButtonLabel>{cancel}</TldrawUiButtonLabel>
</Button> </TldrawUiButton>
<Button type="primary" onClick={async () => onContinue()}> <TldrawUiButton type="primary" onClick={async () => onContinue()}>
{confirm} <TldrawUiButtonLabel>{confirm}</TldrawUiButtonLabel>
</Button> </TldrawUiButton>
</DialogFooter> </TldrawUiDialogFooter>
</> </>
) )
} }

View file

@ -3,9 +3,15 @@ import { T, TLBaseShape, track, useEditor } from '@tldraw/editor'
import { useCallback, useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import { TLUiDialogProps } from '../context/dialogs' import { TLUiDialogProps } from '../context/dialogs'
import { useTranslation } from '../hooks/useTranslation/useTranslation' import { useTranslation } from '../hooks/useTranslation/useTranslation'
import { Button } from './primitives/Button' import { TldrawUiButton } from './primitives/Button/TldrawUiButton'
import { DialogBody, DialogCloseButton, DialogFooter, DialogHeader } from './primitives/Dialog' import { TldrawUiButtonLabel } from './primitives/Button/TldrawUiButtonLabel'
import { Input } from './primitives/Input' import {
TldrawUiDialogBody,
TldrawUiDialogCloseButton,
TldrawUiDialogFooter,
TldrawUiDialogHeader,
} from './primitives/TldrawUiDialog'
import { TldrawUiInput } from './primitives/TldrawUiInput'
// A url can either be invalid, or valid with a protocol, or valid without a protocol. // A url can either be invalid, or valid with a protocol, or valid without a protocol.
// For example, "aol.com" would be valid with a protocol () // For example, "aol.com" would be valid with a protocol ()
@ -134,13 +140,13 @@ export const EditLinkDialogInner = track(function EditLinkDialogInner({
return ( return (
<> <>
<DialogHeader> <TldrawUiDialogHeader>
<DialogTitle>{msg('edit-link-title')}</DialogTitle> <DialogTitle>{msg('edit-link-title')}</DialogTitle>
<DialogCloseButton /> <TldrawUiDialogCloseButton />
</DialogHeader> </TldrawUiDialogHeader>
<DialogBody> <TldrawUiDialogBody>
<div className="tlui-edit-link-dialog"> <div className="tlui-edit-link-dialog">
<Input <TldrawUiInput
ref={rInput} ref={rInput}
className="tlui-edit-link-dialog__input" className="tlui-edit-link-dialog__input"
label="edit-link-url" label="edit-link-url"
@ -152,26 +158,26 @@ export const EditLinkDialogInner = track(function EditLinkDialogInner({
/> />
<div>{urlInputState.valid ? msg('edit-link-detail') : msg('edit-link-invalid-url')}</div> <div>{urlInputState.valid ? msg('edit-link-detail') : msg('edit-link-invalid-url')}</div>
</div> </div>
</DialogBody> </TldrawUiDialogBody>
<DialogFooter className="tlui-dialog__footer__actions"> <TldrawUiDialogFooter className="tlui-dialog__footer__actions">
<Button type="normal" onClick={handleCancel} onTouchEnd={handleCancel}> <TldrawUiButton type="normal" onClick={handleCancel} onTouchEnd={handleCancel}>
{msg('edit-link-cancel')} <TldrawUiButtonLabel>{msg('edit-link-cancel')}</TldrawUiButtonLabel>
</Button> </TldrawUiButton>
{isRemoving ? ( {isRemoving ? (
<Button type={'danger'} onTouchEnd={handleClear} onClick={handleClear}> <TldrawUiButton type={'danger'} onTouchEnd={handleClear} onClick={handleClear}>
{msg('edit-link-clear')} <TldrawUiButtonLabel>{msg('edit-link-clear')}</TldrawUiButtonLabel>
</Button> </TldrawUiButton>
) : ( ) : (
<Button <TldrawUiButton
type="primary" type="primary"
disabled={!urlInputState.valid} disabled={!urlInputState.valid}
onTouchEnd={handleComplete} onTouchEnd={handleComplete}
onClick={handleComplete} onClick={handleComplete}
> >
{msg('edit-link-save')} <TldrawUiButtonLabel>{msg('edit-link-save')}</TldrawUiButtonLabel>
</Button> </TldrawUiButton>
)} )}
</DialogFooter> </TldrawUiDialogFooter>
</> </>
) )
}) })

View file

@ -5,10 +5,16 @@ import { TLEmbedResult, getEmbedInfo } from '../../utils/embeds/embeds'
import { useAssetUrls } from '../context/asset-urls' import { useAssetUrls } from '../context/asset-urls'
import { TLUiDialogProps } from '../context/dialogs' import { TLUiDialogProps } from '../context/dialogs'
import { untranslated, useTranslation } from '../hooks/useTranslation/useTranslation' import { untranslated, useTranslation } from '../hooks/useTranslation/useTranslation'
import { Button } from './primitives/Button' import { TldrawUiButton } from './primitives/Button/TldrawUiButton'
import { DialogBody, DialogCloseButton, DialogFooter, DialogHeader } from './primitives/Dialog' import { TldrawUiButtonLabel } from './primitives/Button/TldrawUiButtonLabel'
import { Icon } from './primitives/Icon' import {
import { Input } from './primitives/Input' TldrawUiDialogBody,
TldrawUiDialogCloseButton,
TldrawUiDialogFooter,
TldrawUiDialogHeader,
} from './primitives/TldrawUiDialog'
import { TldrawUiIcon } from './primitives/TldrawUiIcon'
import { TldrawUiInput } from './primitives/TldrawUiInput'
export const EmbedDialog = track(function EmbedDialog({ onClose }: TLUiDialogProps) { export const EmbedDialog = track(function EmbedDialog({ onClose }: TLUiDialogProps) {
const editor = useEditor() const editor = useEditor()
@ -30,18 +36,18 @@ export const EmbedDialog = track(function EmbedDialog({ onClose }: TLUiDialogPro
return ( return (
<> <>
<DialogHeader> <TldrawUiDialogHeader>
<DialogTitle> <DialogTitle>
{embedDefinition {embedDefinition
? `${msg('embed-title')}${embedDefinition.title}` ? `${msg('embed-title')}${embedDefinition.title}`
: msg('embed-title')} : msg('embed-title')}
</DialogTitle> </DialogTitle>
<DialogCloseButton /> <TldrawUiDialogCloseButton />
</DialogHeader> </TldrawUiDialogHeader>
{embedDefinition ? ( {embedDefinition ? (
<> <>
<DialogBody className="tlui-embed-dialog__enter"> <TldrawUiDialogBody className="tlui-embed-dialog__enter">
<Input <TldrawUiInput
className="tlui-embed-dialog__input" className="tlui-embed-dialog__input"
label="embed-url" label="embed-url"
placeholder="http://example.com" placeholder="http://example.com"
@ -77,7 +83,7 @@ export const EmbedDialog = track(function EmbedDialog({ onClose }: TLUiDialogPro
className="tlui-embed-dialog__instruction__link" className="tlui-embed-dialog__instruction__link"
> >
Learn more. Learn more.
<Icon icon="external-link" small /> <TldrawUiIcon icon="external-link" small />
</a> </a>
)} )}
</div> </div>
@ -86,23 +92,25 @@ export const EmbedDialog = track(function EmbedDialog({ onClose }: TLUiDialogPro
{showError ? msg('embed-invalid-url') : '\xa0'} {showError ? msg('embed-invalid-url') : '\xa0'}
</div> </div>
)} )}
</DialogBody> </TldrawUiDialogBody>
<DialogFooter className="tlui-dialog__footer__actions"> <TldrawUiDialogFooter className="tlui-dialog__footer__actions">
<Button <TldrawUiButton
type="normal" type="normal"
onClick={() => { onClick={() => {
setEmbedDefinition(null) setEmbedDefinition(null)
setEmbedInfoForUrl(null) setEmbedInfoForUrl(null)
setUrl('') setUrl('')
}} }}
label="embed-back" >
/> <TldrawUiButtonLabel>{msg('embed-back')}</TldrawUiButtonLabel>
</TldrawUiButton>
<div className="tlui-embed__spacer" /> <div className="tlui-embed__spacer" />
<Button type="normal" label="embed-cancel" onClick={onClose} /> <TldrawUiButton type="normal" onClick={onClose}>
<Button <TldrawUiButtonLabel>{msg('embed-cancel')}</TldrawUiButtonLabel>
</TldrawUiButton>
<TldrawUiButton
type="primary" type="primary"
disabled={!embedInfoForUrl} disabled={!embedInfoForUrl}
label="embed-create"
onClick={() => { onClick={() => {
if (!embedInfoForUrl) return if (!embedInfoForUrl) return
@ -115,28 +123,26 @@ export const EmbedDialog = track(function EmbedDialog({ onClose }: TLUiDialogPro
onClose() onClose()
}} }}
/> >
</DialogFooter> <TldrawUiButtonLabel>{msg('embed-create')}</TldrawUiButtonLabel>
</TldrawUiButton>
</TldrawUiDialogFooter>
</> </>
) : ( ) : (
<> <>
<DialogBody className="tlui-embed-dialog__list"> <TldrawUiDialogBody className="tlui-embed-dialog__list">
{EMBED_DEFINITIONS.map((def) => { {EMBED_DEFINITIONS.map((def) => {
return ( return (
<Button <TldrawUiButton type="menu" key={def.type} onClick={() => setEmbedDefinition(def)}>
type="menu" <TldrawUiButtonLabel>{untranslated(def.title)}</TldrawUiButtonLabel>
key={def.type}
onClick={() => setEmbedDefinition(def)}
label={untranslated(def.title)}
>
<div <div
className="tlui-embed-dialog__item__image" className="tlui-embed-dialog__item__image"
style={{ backgroundImage: `url(${assetUrls.embedIcons[def.type]})` }} style={{ backgroundImage: `url(${assetUrls.embedIcons[def.type]})` }}
/> />
</Button> </TldrawUiButton>
) )
})} })}
</DialogBody> </TldrawUiDialogBody>
</> </>
)} )}
</> </>

View file

@ -2,12 +2,14 @@ import { memo } from 'react'
import { PORTRAIT_BREAKPOINT } from '../../constants' import { PORTRAIT_BREAKPOINT } from '../../constants'
import { useBreakpoint } from '../../context/breakpoints' import { useBreakpoint } from '../../context/breakpoints'
import { useTranslation } from '../../hooks/useTranslation/useTranslation' import { useTranslation } from '../../hooks/useTranslation/useTranslation'
import { TldrawUiMenuContextProvider } from '../menus/TldrawUiMenuContext' import { TldrawUiButton } from '../primitives/Button/TldrawUiButton'
import { TldrawUiButtonIcon } from '../primitives/Button/TldrawUiButtonIcon'
import { import {
DropdownMenuContent, TldrawUiDropdownMenuContent,
DropdownMenuRoot, TldrawUiDropdownMenuRoot,
DropdownMenuTrigger, TldrawUiDropdownMenuTrigger,
} from '../primitives/DropdownMenu' } from '../primitives/TldrawUiDropdownMenu'
import { TldrawUiMenuContextProvider } from '../primitives/menus/TldrawUiMenuContext'
import { DefaultHelpMenuContent } from './DefaultHelpMenuContent' import { DefaultHelpMenuContent } from './DefaultHelpMenuContent'
/** @public */ /** @public */
@ -29,19 +31,18 @@ export const DefaultHelpMenu = memo(function DefaultHelpMenu({ children }: TLUiH
return ( return (
<div className="tlui-help-menu"> <div className="tlui-help-menu">
<DropdownMenuRoot id="help menu"> <TldrawUiDropdownMenuRoot id="help menu">
<DropdownMenuTrigger <TldrawUiDropdownMenuTrigger>
type="help" <TldrawUiButton type="help" title={msg('help-menu.title')}>
smallIcon <TldrawUiButtonIcon icon="question-mark" small />
title={msg('help-menu.title')} </TldrawUiButton>
icon="question-mark" </TldrawUiDropdownMenuTrigger>
/> <TldrawUiDropdownMenuContent side="top" align="end" alignOffset={0} sideOffset={8}>
<DropdownMenuContent side="top" align="end" alignOffset={0} sideOffset={8}>
<TldrawUiMenuContextProvider type="menu" sourceId="help-menu"> <TldrawUiMenuContextProvider type="menu" sourceId="help-menu">
{content} {content}
</TldrawUiMenuContextProvider> </TldrawUiMenuContextProvider>
</DropdownMenuContent> </TldrawUiDropdownMenuContent>
</DropdownMenuRoot> </TldrawUiDropdownMenuRoot>
</div> </div>
) )
}) })

View file

@ -1,7 +1,7 @@
import { useTldrawUiComponents } from '../../context/components' import { useTldrawUiComponents } from '../../context/components'
import { useDialogs } from '../../context/dialogs' import { useDialogs } from '../../context/dialogs'
import { LanguageMenu } from '../LanguageMenu' import { LanguageMenu } from '../LanguageMenu'
import { TldrawUiMenuItem } from '../menus/TldrawUiMenuItem' import { TldrawUiMenuItem } from '../primitives/menus/TldrawUiMenuItem'
/** @public */ /** @public */
export function DefaultHelpMenuContent() { export function DefaultHelpMenuContent() {

View file

@ -1,7 +1,7 @@
import { useEditor } from '@tldraw/editor' import { useEditor } from '@tldraw/editor'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useActions } from '../../context/actions' import { useActions } from '../../context/actions'
import { TldrawUiMenuItem } from '../menus/TldrawUiMenuItem' import { TldrawUiMenuItem } from '../primitives/menus/TldrawUiMenuItem'
export function BackToContent() { export function BackToContent() {
const editor = useEditor() const editor = useEditor()

View file

@ -1,4 +1,4 @@
import { TldrawUiMenuContextProvider } from '../menus/TldrawUiMenuContext' import { TldrawUiMenuContextProvider } from '../primitives/menus/TldrawUiMenuContext'
import { DefaultHelperButtonsContent } from './DefaultHelperButtonsContent' import { DefaultHelperButtonsContent } from './DefaultHelperButtonsContent'
/** @public */ /** @public */

View file

@ -1,6 +1,6 @@
import { useEditor, useValue } from '@tldraw/editor' import { useEditor, useValue } from '@tldraw/editor'
import { useActions } from '../../context/actions' import { useActions } from '../../context/actions'
import { TldrawUiMenuItem } from '../menus/TldrawUiMenuItem' import { TldrawUiMenuItem } from '../primitives/menus/TldrawUiMenuItem'
export function ExitPenMode() { export function ExitPenMode() {
const editor = useEditor() const editor = useEditor()

View file

@ -1,6 +1,6 @@
import { useEditor, useValue } from '@tldraw/editor' import { useEditor, useValue } from '@tldraw/editor'
import { useActions } from '../../context/actions' import { useActions } from '../../context/actions'
import { TldrawUiMenuItem } from '../menus/TldrawUiMenuItem' import { TldrawUiMenuItem } from '../primitives/menus/TldrawUiMenuItem'
export function StopFollowing() { export function StopFollowing() {
const editor = useEditor() const editor = useEditor()

View file

@ -2,8 +2,12 @@ import { DialogTitle } from '@radix-ui/react-dialog'
import { memo } from 'react' import { memo } from 'react'
import { TLUiDialogProps } from '../../context/dialogs' import { TLUiDialogProps } from '../../context/dialogs'
import { useTranslation } from '../../hooks/useTranslation/useTranslation' import { useTranslation } from '../../hooks/useTranslation/useTranslation'
import { TldrawUiMenuContextProvider } from '../menus/TldrawUiMenuContext' import {
import { DialogBody, DialogCloseButton, DialogHeader } from '../primitives/Dialog' TldrawUiDialogBody,
TldrawUiDialogCloseButton,
TldrawUiDialogHeader,
} from '../primitives/TldrawUiDialog'
import { TldrawUiMenuContextProvider } from '../primitives/menus/TldrawUiMenuContext'
import { DefaultKeyboardShortcutsDialogContent } from './DefaultKeyboardShortcutsDialogContent' import { DefaultKeyboardShortcutsDialogContent } from './DefaultKeyboardShortcutsDialogContent'
/** @public */ /** @public */
@ -21,15 +25,15 @@ export const DefaultKeyboardShortcutsDialog = memo(function DefaultKeyboardShort
return ( return (
<> <>
<DialogHeader className="tlui-shortcuts-dialog__header"> <TldrawUiDialogHeader className="tlui-shortcuts-dialog__header">
<DialogTitle>{msg('shortcuts-title')}</DialogTitle> <DialogTitle>{msg('shortcuts-title')}</DialogTitle>
<DialogCloseButton /> <TldrawUiDialogCloseButton />
</DialogHeader> </TldrawUiDialogHeader>
<DialogBody className="tlui-shortcuts-dialog__body"> <TldrawUiDialogBody className="tlui-shortcuts-dialog__body">
<TldrawUiMenuContextProvider type="keyboard-shortcuts" sourceId="kbd"> <TldrawUiMenuContextProvider type="keyboard-shortcuts" sourceId="kbd">
{content} {content}
</TldrawUiMenuContextProvider> </TldrawUiMenuContextProvider>
</DialogBody> </TldrawUiDialogBody>
<div className="tlui-dialog__scrim" /> <div className="tlui-dialog__scrim" />
</> </>
) )

View file

@ -1,7 +1,7 @@
import { useActions } from '../../context/actions' import { useActions } from '../../context/actions'
import { useTools } from '../../hooks/useTools' import { useTools } from '../../hooks/useTools'
import { TldrawUiMenuGroup } from '../menus/TldrawUiMenuGroup' import { TldrawUiMenuGroup } from '../primitives/menus/TldrawUiMenuGroup'
import { TldrawUiMenuItem } from '../menus/TldrawUiMenuItem' import { TldrawUiMenuItem } from '../primitives/menus/TldrawUiMenuItem'
/** @public */ /** @public */
export function DefaultKeyboardShortcutsDialogContent() { export function DefaultKeyboardShortcutsDialogContent() {

View file

@ -1,9 +1,9 @@
import { useEditor } from '@tldraw/editor' import { useEditor } from '@tldraw/editor'
import { useUiEvents } from '../context/events' import { useUiEvents } from '../context/events'
import { useLanguages } from '../hooks/useTranslation/useLanguages' import { useLanguages } from '../hooks/useTranslation/useLanguages'
import { TldrawUiMenuCheckboxItem } from './menus/TldrawUiMenuCheckboxItem' import { TldrawUiMenuCheckboxItem } from './primitives/menus/TldrawUiMenuCheckboxItem'
import { TldrawUiMenuGroup } from './menus/TldrawUiMenuGroup' import { TldrawUiMenuGroup } from './primitives/menus/TldrawUiMenuGroup'
import { TldrawUiMenuSubmenu } from './menus/TldrawUiMenuSubmenu' import { TldrawUiMenuSubmenu } from './primitives/menus/TldrawUiMenuSubmenu'
export function LanguageMenu() { export function LanguageMenu() {
const editor = useEditor() const editor = useEditor()

View file

@ -3,8 +3,9 @@ import { useContainer } from '@tldraw/editor'
import { memo } from 'react' import { memo } from 'react'
import { useMenuIsOpen } from '../../hooks/useMenuIsOpen' import { useMenuIsOpen } from '../../hooks/useMenuIsOpen'
import { useTranslation } from '../../hooks/useTranslation/useTranslation' import { useTranslation } from '../../hooks/useTranslation/useTranslation'
import { TldrawUiMenuContextProvider } from '../menus/TldrawUiMenuContext' import { TldrawUiButton } from '../primitives/Button/TldrawUiButton'
import { Button } from '../primitives/Button' import { TldrawUiButtonIcon } from '../primitives/Button/TldrawUiButtonIcon'
import { TldrawUiMenuContextProvider } from '../primitives/menus/TldrawUiMenuContext'
import { DefaultMainMenuContent } from './DefaultMainMenuContent' import { DefaultMainMenuContent } from './DefaultMainMenuContent'
/** @public */ /** @public */
@ -26,14 +27,9 @@ export const DefaultMainMenu = memo(function DefaultMainMenu({ children }: TLUiM
return ( return (
<_Dropdown.Root dir="ltr" open={isOpen} onOpenChange={onOpenChange} modal={false}> <_Dropdown.Root dir="ltr" open={isOpen} onOpenChange={onOpenChange} modal={false}>
<_Dropdown.Trigger asChild dir="ltr"> <_Dropdown.Trigger asChild dir="ltr">
<Button <TldrawUiButton type="icon" data-testid="main.menu" title={msg('menu.title')}>
type="icon" <TldrawUiButtonIcon icon="menu" small />
className="tlui-menu__trigger" </TldrawUiButton>
data-testid="main.menu"
title={msg('menu.title')}
icon="menu"
smallIcon
/>
</_Dropdown.Trigger> </_Dropdown.Trigger>
<_Dropdown.Portal container={container}> <_Dropdown.Portal container={container}>
<_Dropdown.Content <_Dropdown.Content

View file

@ -29,9 +29,9 @@ import {
ZoomToFitMenuItem, ZoomToFitMenuItem,
ZoomToSelectionMenuItem, ZoomToSelectionMenuItem,
} from '../menu-items' } from '../menu-items'
import { TldrawUiMenuGroup } from '../menus/TldrawUiMenuGroup' import { TldrawUiMenuGroup } from '../primitives/menus/TldrawUiMenuGroup'
import { TldrawUiMenuItem } from '../menus/TldrawUiMenuItem' import { TldrawUiMenuItem } from '../primitives/menus/TldrawUiMenuItem'
import { TldrawUiMenuSubmenu } from '../menus/TldrawUiMenuSubmenu' import { TldrawUiMenuSubmenu } from '../primitives/menus/TldrawUiMenuSubmenu'
/** @public */ /** @public */
export function DefaultMainMenuContent() { export function DefaultMainMenuContent() {

View file

@ -0,0 +1,27 @@
import { memo } from 'react'
import { useBreakpoint } from '../context/breakpoints'
import { useTldrawUiComponents } from '../context/components'
/** @public */
export const DefaultMenuPanel = memo(function MenuPanel() {
const breakpoint = useBreakpoint()
const { MainMenu, QuickActions, ActionsMenu, PageMenu } = useTldrawUiComponents()
if (!MainMenu && !PageMenu && breakpoint < 6) return null
return (
<div className="tlui-menu-zone">
<div className="tlui-buttons__horizontal">
{MainMenu && <MainMenu />}
{PageMenu && <PageMenu />}
{breakpoint < 6 ? null : (
<>
{QuickActions && <QuickActions />}
{ActionsMenu && <ActionsMenu />}
</>
)}
</div>
</div>
)
})

View file

@ -9,8 +9,13 @@ import { useCallback } from 'react'
import { useTldrawUiComponents } from '../context/components' import { useTldrawUiComponents } from '../context/components'
import { useRelevantStyles } from '../hooks/useRevelantStyles' import { useRelevantStyles } from '../hooks/useRevelantStyles'
import { useTranslation } from '../hooks/useTranslation/useTranslation' import { useTranslation } from '../hooks/useTranslation/useTranslation'
import { Icon } from './primitives/Icon' import { TldrawUiButton } from './primitives/Button/TldrawUiButton'
import { Popover, PopoverContent, PopoverTrigger } from './primitives/Popover' import { TldrawUiButtonIcon } from './primitives/Button/TldrawUiButtonIcon'
import {
TldrawUiPopover,
TldrawUiPopoverContent,
TldrawUiPopoverTrigger,
} from './primitives/TldrawUiPopover'
export function MobileStylePanel() { export function MobileStylePanel() {
const editor = useEditor() const editor = useEditor()
@ -42,22 +47,24 @@ export function MobileStylePanel() {
if (!StylePanel) return null if (!StylePanel) return null
return ( return (
<Popover id="style menu" onOpenChange={handleStylesOpenChange}> <TldrawUiPopover id="mobile style menu" onOpenChange={handleStylesOpenChange}>
<PopoverTrigger <TldrawUiPopoverTrigger>
disabled={disableStylePanel} <TldrawUiButton
type="tool" type="tool"
data-testid="mobile.styles"
style={{
color: disableStylePanel ? 'var(--color-muted-1)' : currentColor,
}}
title={msg('style-panel.title')} title={msg('style-panel.title')}
data-testid="mobile.styles"
disabled={disableStylePanel}
style={{ color: disableStylePanel ? 'var(--color-muted-1)' : currentColor }}
> >
<Icon icon={disableStylePanel ? 'blob' : color?.type === 'mixed' ? 'mixed' : 'blob'} /> <TldrawUiButtonIcon
</PopoverTrigger> icon={disableStylePanel ? 'blob' : color?.type === 'mixed' ? 'mixed' : 'blob'}
<PopoverContent side="top" align="end"> />
</TldrawUiButton>
</TldrawUiPopoverTrigger>
<TldrawUiPopoverContent side="top" align="end">
<_StylePanel /> <_StylePanel />
</PopoverContent> </TldrawUiPopoverContent>
</Popover> </TldrawUiPopover>
) )
} }
@ -66,5 +73,5 @@ function _StylePanel() {
const relevantStyles = useRelevantStyles() const relevantStyles = useRelevantStyles()
if (!StylePanel) return null if (!StylePanel) return null
return <StylePanel relevantStyles={relevantStyles} /> return <StylePanel relevantStyles={relevantStyles} isMobile />
} }

View file

@ -5,8 +5,9 @@ import { useBreakpoint } from '../../context/breakpoints'
import { useTldrawUiComponents } from '../../context/components' import { useTldrawUiComponents } from '../../context/components'
import { useLocalStorageState } from '../../hooks/useLocalStorageState' import { useLocalStorageState } from '../../hooks/useLocalStorageState'
import { useTranslation } from '../../hooks/useTranslation/useTranslation' import { useTranslation } from '../../hooks/useTranslation/useTranslation'
import { Button } from '../primitives/Button' import { kbdStr } from '../../kbd-utils'
import { kbdStr } from '../primitives/shared' import { TldrawUiButton } from '../primitives/Button/TldrawUiButton'
import { TldrawUiButtonIcon } from '../primitives/Button/TldrawUiButtonIcon'
/** @public */ /** @public */
export const DefaultNavigationPanel = memo(function DefaultNavigationPanel() { export const DefaultNavigationPanel = memo(function DefaultNavigationPanel() {
@ -35,42 +36,46 @@ export const DefaultNavigationPanel = memo(function DefaultNavigationPanel() {
<> <>
{ZoomMenu && <ZoomMenu />} {ZoomMenu && <ZoomMenu />}
{Minimap && ( {Minimap && (
<Button <TldrawUiButton
type="icon" type="icon"
icon={collapsed ? 'chevrons-ne' : 'chevrons-sw'}
data-testid="minimap.toggle" data-testid="minimap.toggle"
title={msg('navigation-zone.toggle-minimap')} title={msg('navigation-zone.toggle-minimap')}
className="tlui-navigation-panel__toggle" className="tlui-navigation-panel__toggle"
onClick={toggleMinimap} onClick={toggleMinimap}
/> >
<TldrawUiButtonIcon icon={collapsed ? 'chevrons-ne' : 'chevrons-sw'} />
</TldrawUiButton>
)} )}
</> </>
) : ( ) : (
<> <>
<Button <TldrawUiButton
type="icon" type="icon"
icon="minus"
data-testid="minimap.zoom-out" data-testid="minimap.zoom-out"
title={`${msg(unwrapLabel(actions['zoom-out'].label))} ${kbdStr(actions['zoom-out'].kbd!)}`} title={`${msg(unwrapLabel(actions['zoom-out'].label))} ${kbdStr(actions['zoom-out'].kbd!)}`}
onClick={() => actions['zoom-out'].onSelect('navigation-zone')} onClick={() => actions['zoom-out'].onSelect('navigation-zone')}
/> >
<TldrawUiButtonIcon icon="minus" />
</TldrawUiButton>
{ZoomMenu && <ZoomMenu />} {ZoomMenu && <ZoomMenu />}
<Button <TldrawUiButton
type="icon" type="icon"
icon="plus"
data-testid="minimap.zoom-in" data-testid="minimap.zoom-in"
title={`${msg(unwrapLabel(actions['zoom-in'].label))} ${kbdStr(actions['zoom-in'].kbd!)}`} title={`${msg(unwrapLabel(actions['zoom-in'].label))} ${kbdStr(actions['zoom-in'].kbd!)}`}
onClick={() => actions['zoom-in'].onSelect('navigation-zone')} onClick={() => actions['zoom-in'].onSelect('navigation-zone')}
/> >
<TldrawUiButtonIcon icon="plus" />
</TldrawUiButton>
{Minimap && ( {Minimap && (
<Button <TldrawUiButton
type="icon" type="icon"
icon={collapsed ? 'chevrons-ne' : 'chevrons-sw'}
data-testid="minimap.toggle" data-testid="minimap.toggle"
title={msg('navigation-zone.toggle-minimap')} title={msg('navigation-zone.toggle-minimap')}
className="tlui-navigation-panel__toggle" className="tlui-navigation-panel__toggle"
onClick={toggleMinimap} onClick={toggleMinimap}
/> >
<TldrawUiButtonIcon icon={collapsed ? 'chevrons-ne' : 'chevrons-sw'} />
</TldrawUiButton>
)} )}
</> </>
)} )}

View file

@ -1,7 +1,7 @@
import classNames from 'classnames' import classNames from 'classnames'
import { useRef } from 'react' import { useRef } from 'react'
import { useTranslation } from '../../hooks/useTranslation/useTranslation' import { useTranslation } from '../../hooks/useTranslation/useTranslation'
import { Icon } from '../primitives/Icon' import { TldrawUiIcon } from '../primitives/TldrawUiIcon'
/** @public */ /** @public */
export function OfflineIndicator() { export function OfflineIndicator() {
@ -11,7 +11,7 @@ export function OfflineIndicator() {
return ( return (
<div className={classNames('tlui-offline-indicator')} ref={rContainer}> <div className={classNames('tlui-offline-indicator')} ref={rContainer}>
{msg('status.offline')} {msg('status.offline')}
<Icon aria-label="offline" icon="status-offline" small /> <TldrawUiIcon aria-label="offline" icon="status-offline" small />
</div> </div>
) )
} }

View file

@ -13,9 +13,15 @@ import { useBreakpoint } from '../../context/breakpoints'
import { useMenuIsOpen } from '../../hooks/useMenuIsOpen' import { useMenuIsOpen } from '../../hooks/useMenuIsOpen'
import { useReadonly } from '../../hooks/useReadonly' import { useReadonly } from '../../hooks/useReadonly'
import { useTranslation } from '../../hooks/useTranslation/useTranslation' import { useTranslation } from '../../hooks/useTranslation/useTranslation'
import { Button } from '../primitives/Button' import { TldrawUiButton } from '../primitives/Button/TldrawUiButton'
import { Icon } from '../primitives/Icon' import { TldrawUiButtonCheck } from '../primitives/Button/TldrawUiButtonCheck'
import { Popover, PopoverContent, PopoverTrigger } from '../primitives/Popover' import { TldrawUiButtonIcon } from '../primitives/Button/TldrawUiButtonIcon'
import { TldrawUiButtonLabel } from '../primitives/Button/TldrawUiButtonLabel'
import {
TldrawUiPopover,
TldrawUiPopoverContent,
TldrawUiPopoverTrigger,
} from '../primitives/TldrawUiPopover'
import { PageItemInput } from './PageItemInput' import { PageItemInput } from './PageItemInput'
import { PageItemSubmenu } from './PageItemSubmenu' import { PageItemSubmenu } from './PageItemSubmenu'
import { onMovePage } from './edit-pages-shared' import { onMovePage } from './edit-pages-shared'
@ -255,33 +261,30 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
}, [editor, msg, isReadonlyMode]) }, [editor, msg, isReadonlyMode])
return ( return (
<Popover id="pages" onOpenChange={onOpenChange} open={isOpen}> <TldrawUiPopover id="pages" onOpenChange={onOpenChange} open={isOpen}>
<PopoverTrigger <TldrawUiPopoverTrigger data-testid="main.page-menu">
className="tlui-page-menu__trigger tlui-menu__trigger" <TldrawUiButton type="menu" title={currentPage.name} className="tlui-page-menu__trigger">
data-testid="main.page-menu"
icon="chevron-down"
type="menu"
title={currentPage.name}
>
<div className="tlui-page-menu__name">{currentPage.name}</div> <div className="tlui-page-menu__name">{currentPage.name}</div>
</PopoverTrigger> <TldrawUiButtonIcon icon="chevron-down" small />
<PopoverContent side="bottom" align="start" sideOffset={6}> </TldrawUiButton>
</TldrawUiPopoverTrigger>
<TldrawUiPopoverContent side="bottom" align="start" sideOffset={6}>
<div className="tlui-page-menu__wrapper"> <div className="tlui-page-menu__wrapper">
<div className="tlui-page-menu__header"> <div className="tlui-page-menu__header">
<div className="tlui-page-menu__header__title">{msg('page-menu.title')}</div> <div className="tlui-page-menu__header__title">{msg('page-menu.title')}</div>
{!isReadonlyMode && ( {!isReadonlyMode && (
<div className="tlui-buttons__horizontal"> <div className="tlui-buttons__horizontal">
<Button <TldrawUiButton
type="icon" type="icon"
data-testid="page-menu.edit" data-testid="page-menu.edit"
title={msg(isEditing ? 'page-menu.edit-done' : 'page-menu.edit-start')} title={msg(isEditing ? 'page-menu.edit-done' : 'page-menu.edit-start')}
icon={isEditing ? 'check' : 'edit'}
onClick={toggleEditing} onClick={toggleEditing}
/> >
<Button <TldrawUiButtonIcon icon={isEditing ? 'check' : 'edit'} />
</TldrawUiButton>
<TldrawUiButton
type="icon" type="icon"
data-testid="page-menu.create" data-testid="page-menu.create"
icon="plus"
title={msg( title={msg(
maxPageCountReached maxPageCountReached
? 'page-menu.max-page-count-reached' ? 'page-menu.max-page-count-reached'
@ -289,7 +292,9 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
)} )}
disabled={maxPageCountReached} disabled={maxPageCountReached}
onClick={handleCreatePageClick} onClick={handleCreatePageClick}
/> >
<TldrawUiButtonIcon icon="plus" />
</TldrawUiButton>
</div> </div>
)} )}
</div> </div>
@ -314,24 +319,25 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
transform: `translate(0px, ${position.y + position.offsetY}px)`, transform: `translate(0px, ${position.y + position.offsetY}px)`,
}} }}
> >
<Button <TldrawUiButton
type="icon" type="icon"
tabIndex={-1} tabIndex={-1}
className="tlui-page_menu__item__sortable__handle" className="tlui-page_menu__item__sortable__handle"
icon="drag-handle-dots"
onPointerDown={handlePointerDown} onPointerDown={handlePointerDown}
onPointerUp={handlePointerUp} onPointerUp={handlePointerUp}
onPointerMove={handlePointerMove} onPointerMove={handlePointerMove}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
data-id={page.id} data-id={page.id}
data-index={index} data-index={index}
/> >
<TldrawUiButtonIcon icon="drag-handle-dots" />
</TldrawUiButton>
{breakpoint < PORTRAIT_BREAKPOINT.TABLET_SM && isCoarsePointer ? ( {breakpoint < PORTRAIT_BREAKPOINT.TABLET_SM && isCoarsePointer ? (
// sigh, this is a workaround for iOS Safari // sigh, this is a workaround for iOS Safari
// because the device and the radix popover seem // because the device and the radix popover seem
// to be fighting over scroll position. Nothing // to be fighting over scroll position. Nothing
// else seems to work! // else seems to work!
<Button <TldrawUiButton
type="normal" type="normal"
className="tlui-page-menu__item__button" className="tlui-page-menu__item__button"
onClick={() => { onClick={() => {
@ -341,10 +347,10 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
} }
}} }}
onDoubleClick={toggleEditing} onDoubleClick={toggleEditing}
isChecked={page.id === currentPage.id}
> >
<span>{page.name}</span> <TldrawUiButtonCheck checked={page.id === currentPage.id} />
</Button> <TldrawUiButtonLabel>{page.name}</TldrawUiButtonLabel>
</TldrawUiButton>
) : ( ) : (
<div <div
className="tlui-page_menu__item__sortable__title" className="tlui-page_menu__item__sortable__title"
@ -369,19 +375,16 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
data-testid={`page-menu-item-${page.id}`} data-testid={`page-menu-item-${page.id}`}
className="tlui-page-menu__item" className="tlui-page-menu__item"
> >
<Button <TldrawUiButton
type="normal" type="normal"
className="tlui-page-menu__item__button tlui-page-menu__item__button__checkbox" className="tlui-page-menu__item__button"
onClick={() => editor.setCurrentPage(page.id)} onClick={() => editor.setCurrentPage(page.id)}
onDoubleClick={toggleEditing} onDoubleClick={toggleEditing}
isChecked={page.id === currentPage.id}
title={msg('page-menu.go-to-page')} title={msg('page-menu.go-to-page')}
> >
<div className="tlui-page-menu__item__button__check"> <TldrawUiButtonCheck checked={page.id === currentPage.id} />
{page.id === currentPage.id && <Icon icon="check" />} <TldrawUiButtonLabel>{page.name}</TldrawUiButtonLabel>
</div> </TldrawUiButton>
<span>{page.name}</span>
</Button>
{!isReadonlyMode && ( {!isReadonlyMode && (
<div className="tlui-page_menu__item__submenu"> <div className="tlui-page_menu__item__submenu">
<PageItemSubmenu <PageItemSubmenu
@ -409,7 +412,7 @@ export const DefaultPageMenu = memo(function DefaultPageMenu() {
})} })}
</div> </div>
</div> </div>
</PopoverContent> </TldrawUiPopoverContent>
</Popover> </TldrawUiPopover>
) )
}) })

View file

@ -1,6 +1,6 @@
import { TLPageId, useEditor } from '@tldraw/editor' import { TLPageId, useEditor } from '@tldraw/editor'
import { useCallback, useRef } from 'react' import { useCallback, useRef } from 'react'
import { Input } from '../primitives/Input' import { TldrawUiInput } from '../primitives/TldrawUiInput'
export const PageItemInput = function PageItemInput({ export const PageItemInput = function PageItemInput({
name, name,
@ -31,7 +31,7 @@ export const PageItemInput = function PageItemInput({
) )
return ( return (
<Input <TldrawUiInput
className="tlui-page-menu__item__input" className="tlui-page-menu__item__input"
ref={(el) => (rInput.current = el)} ref={(el) => (rInput.current = el)}
defaultValue={name} defaultValue={name}

View file

@ -1,14 +1,16 @@
import { MAX_PAGES, PageRecordType, TLPageId, track, useEditor } from '@tldraw/editor' import { MAX_PAGES, PageRecordType, TLPageId, track, useEditor } from '@tldraw/editor'
import { useCallback } from 'react' import { useCallback } from 'react'
import { useTranslation } from '../../hooks/useTranslation/useTranslation' import { useTranslation } from '../../hooks/useTranslation/useTranslation'
import { TldrawUiMenuContextProvider } from '../menus/TldrawUiMenuContext' import { TldrawUiButton } from '../primitives/Button/TldrawUiButton'
import { TldrawUiMenuGroup } from '../menus/TldrawUiMenuGroup' import { TldrawUiButtonIcon } from '../primitives/Button/TldrawUiButtonIcon'
import { TldrawUiMenuItem } from '../menus/TldrawUiMenuItem'
import { import {
DropdownMenuContent, TldrawUiDropdownMenuContent,
DropdownMenuRoot, TldrawUiDropdownMenuRoot,
DropdownMenuTrigger, TldrawUiDropdownMenuTrigger,
} from '../primitives/DropdownMenu' } from '../primitives/TldrawUiDropdownMenu'
import { TldrawUiMenuContextProvider } from '../primitives/menus/TldrawUiMenuContext'
import { TldrawUiMenuGroup } from '../primitives/menus/TldrawUiMenuGroup'
import { TldrawUiMenuItem } from '../primitives/menus/TldrawUiMenuItem'
import { onMovePage } from './edit-pages-shared' import { onMovePage } from './edit-pages-shared'
export interface PageItemSubmenuProps { export interface PageItemSubmenuProps {
@ -48,13 +50,13 @@ export const PageItemSubmenu = track(function PageItemSubmenu({
}, [editor, item]) }, [editor, item])
return ( return (
<DropdownMenuRoot id={`page item submenu ${index}`}> <TldrawUiDropdownMenuRoot id={`page item submenu ${index}`}>
<DropdownMenuTrigger <TldrawUiDropdownMenuTrigger>
type="icon" <TldrawUiButton type="icon" title={msg('page-menu.submenu.title')}>
title={msg('page-menu.submenu.title')} <TldrawUiButtonIcon icon="dots-vertical" />
icon="dots-vertical" </TldrawUiButton>
/> </TldrawUiDropdownMenuTrigger>
<DropdownMenuContent alignOffset={0} side="right" sideOffset={-4}> <TldrawUiDropdownMenuContent alignOffset={0} side="right" sideOffset={-4}>
<TldrawUiMenuContextProvider type="menu" sourceId="page-menu"> <TldrawUiMenuContextProvider type="menu" sourceId="page-menu">
<TldrawUiMenuGroup id="modify"> <TldrawUiMenuGroup id="modify">
{onRename && ( {onRename && (
@ -87,7 +89,7 @@ export const PageItemSubmenu = track(function PageItemSubmenu({
</TldrawUiMenuGroup> </TldrawUiMenuGroup>
)} )}
</TldrawUiMenuContextProvider> </TldrawUiMenuContextProvider>
</DropdownMenuContent> </TldrawUiDropdownMenuContent>
</DropdownMenuRoot> </TldrawUiDropdownMenuRoot>
) )
}) })

View file

@ -1,5 +1,5 @@
import { memo } from 'react' import { memo } from 'react'
import { TldrawUiMenuContextProvider } from '../menus/TldrawUiMenuContext' import { TldrawUiMenuContextProvider } from '../primitives/menus/TldrawUiMenuContext'
import { DefaultQuickActionsContent } from './DefaultQuickActionsContent' import { DefaultQuickActionsContent } from './DefaultQuickActionsContent'
/** @public */ /** @public */

View file

@ -2,7 +2,7 @@ import { useEditor, useValue } from '@tldraw/editor'
import { useActions } from '../../context/actions' import { useActions } from '../../context/actions'
import { useCanRedo, useCanUndo, useUnlockedSelectedShapesCount } from '../../hooks/menu-hooks' import { useCanRedo, useCanUndo, useUnlockedSelectedShapesCount } from '../../hooks/menu-hooks'
import { useReadonly } from '../../hooks/useReadonly' import { useReadonly } from '../../hooks/useReadonly'
import { TldrawUiMenuItem } from '../menus/TldrawUiMenuItem' import { TldrawUiMenuItem } from '../primitives/menus/TldrawUiMenuItem'
/** @public */ /** @public */
export function DefaultQuickActionsContent() { export function DefaultQuickActionsContent() {

View file

@ -33,7 +33,7 @@ export const DefaultStylePanel = memo(function DefaultStylePanel({
return ( return (
<div <div
className={classNames('tlui-style-panel', { 'tlui-style-panel__wrapper': !isMobile })} className={classNames('', { 'tlui-style-panel__wrapper': !isMobile })}
data-ismobile={isMobile} data-ismobile={isMobile}
onPointerLeave={handlePointerOut} onPointerLeave={handlePointerOut}
> >

View file

@ -17,15 +17,16 @@ import {
useEditor, useEditor,
} from '@tldraw/editor' } from '@tldraw/editor'
import React from 'react' import React from 'react'
import { STYLES } from '../../../styles'
import { useUiEvents } from '../../context/events' import { useUiEvents } from '../../context/events'
import { useRelevantStyles } from '../../hooks/useRevelantStyles' import { useRelevantStyles } from '../../hooks/useRevelantStyles'
import { useTranslation } from '../../hooks/useTranslation/useTranslation' import { useTranslation } from '../../hooks/useTranslation/useTranslation'
import { Button } from '../primitives/Button' import { TldrawUiButton } from '../primitives/Button/TldrawUiButton'
import { ButtonPicker } from '../primitives/ButtonPicker' import { TldrawUiButtonIcon } from '../primitives/Button/TldrawUiButtonIcon'
import { Slider } from '../primitives/Slider' import { TldrawUiButtonPicker } from '../primitives/TldrawUiButtonPicker'
import { TldrawUiSlider } from '../primitives/TldrawUiSlider'
import { DoubleDropdownPicker } from './DoubleDropdownPicker' import { DoubleDropdownPicker } from './DoubleDropdownPicker'
import { DropdownPicker } from './DropdownPicker' import { DropdownPicker } from './DropdownPicker'
import { STYLES } from './styles'
/** @public */ /** @public */
export type TLUiStylePanelContentProps = { export type TLUiStylePanelContentProps = {
@ -137,7 +138,7 @@ function CommonStylePickerSet({
aria-label="style panel styles" aria-label="style panel styles"
> >
{color === undefined ? null : ( {color === undefined ? null : (
<ButtonPicker <TldrawUiButtonPicker
title={msg('style-panel.color')} title={msg('style-panel.color')}
uiType="color" uiType="color"
style={DefaultColorStyle} style={DefaultColorStyle}
@ -147,7 +148,7 @@ function CommonStylePickerSet({
/> />
)} )}
{opacity === undefined ? null : ( {opacity === undefined ? null : (
<Slider <TldrawUiSlider
data-testid="style.opacity" data-testid="style.opacity"
value={opacityIndex >= 0 ? opacityIndex : tldrawSupportedOpacities.length - 1} value={opacityIndex >= 0 ? opacityIndex : tldrawSupportedOpacities.length - 1}
label={ label={
@ -162,7 +163,7 @@ function CommonStylePickerSet({
{showPickers && ( {showPickers && (
<div className="tlui-style-panel__section" aria-label="style panel styles"> <div className="tlui-style-panel__section" aria-label="style panel styles">
{fill === undefined ? null : ( {fill === undefined ? null : (
<ButtonPicker <TldrawUiButtonPicker
title={msg('style-panel.fill')} title={msg('style-panel.fill')}
uiType="fill" uiType="fill"
style={DefaultFillStyle} style={DefaultFillStyle}
@ -172,7 +173,7 @@ function CommonStylePickerSet({
/> />
)} )}
{dash === undefined ? null : ( {dash === undefined ? null : (
<ButtonPicker <TldrawUiButtonPicker
title={msg('style-panel.dash')} title={msg('style-panel.dash')}
uiType="dash" uiType="dash"
style={DefaultDashStyle} style={DefaultDashStyle}
@ -182,7 +183,7 @@ function CommonStylePickerSet({
/> />
)} )}
{size === undefined ? null : ( {size === undefined ? null : (
<ButtonPicker <TldrawUiButtonPicker
title={msg('style-panel.size')} title={msg('style-panel.size')}
uiType="size" uiType="size"
style={DefaultSizeStyle} style={DefaultSizeStyle}
@ -211,7 +212,7 @@ function TextStylePickerSet({ styles }: { styles: ReadonlySharedStyleMap }) {
return ( return (
<div className="tlui-style-panel__section" aria-label="style panel text"> <div className="tlui-style-panel__section" aria-label="style panel text">
{font === undefined ? null : ( {font === undefined ? null : (
<ButtonPicker <TldrawUiButtonPicker
title={msg('style-panel.font')} title={msg('style-panel.font')}
uiType="font" uiType="font"
style={DefaultFontStyle} style={DefaultFontStyle}
@ -223,7 +224,7 @@ function TextStylePickerSet({ styles }: { styles: ReadonlySharedStyleMap }) {
{align === undefined ? null : ( {align === undefined ? null : (
<div className="tlui-style-panel__row"> <div className="tlui-style-panel__row">
<ButtonPicker <TldrawUiButtonPicker
title={msg('style-panel.align')} title={msg('style-panel.align')}
uiType="align" uiType="align"
style={DefaultHorizontalAlignStyle} style={DefaultHorizontalAlignStyle}
@ -233,13 +234,14 @@ function TextStylePickerSet({ styles }: { styles: ReadonlySharedStyleMap }) {
/> />
<div className="tlui-style-panel__row__extra-button"> <div className="tlui-style-panel__row__extra-button">
{verticalAlign === undefined ? ( {verticalAlign === undefined ? (
<Button <TldrawUiButton
type="icon" type="icon"
title={msg('style-panel.vertical-align')} title={msg('style-panel.vertical-align')}
data-testid="vertical-align" data-testid="vertical-align"
icon="vertical-align-center"
disabled disabled
/> >
<TldrawUiButtonIcon icon="vertical-align-center" />
</TldrawUiButton>
) : ( ) : (
<DropdownPicker <DropdownPicker
type="icon" type="icon"

View file

@ -1,15 +1,16 @@
import { SharedStyle, StyleProp } from '@tldraw/editor' import { SharedStyle, StyleProp } from '@tldraw/editor'
import * as React from 'react' import * as React from 'react'
import { StyleValuesForUi } from '../../../styles'
import { TLUiTranslationKey } from '../../hooks/useTranslation/TLUiTranslationKey' import { TLUiTranslationKey } from '../../hooks/useTranslation/TLUiTranslationKey'
import { useTranslation } from '../../hooks/useTranslation/useTranslation' import { useTranslation } from '../../hooks/useTranslation/useTranslation'
import { TLUiIconType } from '../../icon-types' import { TldrawUiButton } from '../primitives/Button/TldrawUiButton'
import { TldrawUiButtonIcon } from '../primitives/Button/TldrawUiButtonIcon'
import { import {
DropdownMenuContent, TldrawUiDropdownMenuContent,
DropdownMenuItem, TldrawUiDropdownMenuItem,
DropdownMenuRoot, TldrawUiDropdownMenuRoot,
DropdownMenuTrigger, TldrawUiDropdownMenuTrigger,
} from '../primitives/DropdownMenu' } from '../primitives/TldrawUiDropdownMenu'
import { StyleValuesForUi } from './styles'
interface DoubleDropdownPickerProps<T extends string> { interface DoubleDropdownPickerProps<T extends string> {
uiTypeA: string uiTypeA: string
@ -63,8 +64,9 @@ export const DoubleDropdownPicker = React.memo(function DoubleDropdownPicker<T e
{msg(label)} {msg(label)}
</div> </div>
<div className="tlui-buttons__horizontal"> <div className="tlui-buttons__horizontal">
<DropdownMenuRoot id={`style panel ${uiTypeA} A`}> <TldrawUiDropdownMenuRoot id={`style panel ${uiTypeA} A`}>
<DropdownMenuTrigger <TldrawUiDropdownMenuTrigger>
<TldrawUiButton
type="icon" type="icon"
data-testid={`style.${uiTypeA}`} data-testid={`style.${uiTypeA}`}
title={ title={
@ -74,34 +76,32 @@ export const DoubleDropdownPicker = React.memo(function DoubleDropdownPicker<T e
? msg('style-panel.mixed') ? msg('style-panel.mixed')
: msg(`${uiTypeA}-style.${valueA.value}` as TLUiTranslationKey)) : msg(`${uiTypeA}-style.${valueA.value}` as TLUiTranslationKey))
} }
icon={iconA as any} >
invertIcon <TldrawUiButtonIcon icon={iconA} small invertIcon />
smallIcon </TldrawUiButton>
/> </TldrawUiDropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" sideOffset={0} alignOffset={-2}> <TldrawUiDropdownMenuContent side="bottom" align="end" sideOffset={0} alignOffset={-2}>
<div className="tlui-buttons__grid"> <div className="tlui-buttons__grid">
{itemsA.map((item) => { {itemsA.map((item) => {
return ( return (
<DropdownMenuItem <TldrawUiDropdownMenuItem data-testid={`style.${uiTypeA}.${item.value}`}>
<TldrawUiButton
type="icon" type="icon"
title={
msg(labelA) +
' — ' +
msg(`${uiTypeA}-style.${item.value}` as TLUiTranslationKey)
}
data-testid={`style.${uiTypeA}.${item.value}`}
key={item.value} key={item.value}
icon={item.icon as TLUiIconType}
onClick={() => onValueChange(styleA, item.value, false)} onClick={() => onValueChange(styleA, item.value, false)}
invertIcon title={`${msg(labelA)}${msg(`${uiTypeA}-style.${item.value}`)}`}
/> >
<TldrawUiButtonIcon icon={item.icon} invertIcon />
</TldrawUiButton>
</TldrawUiDropdownMenuItem>
) )
})} })}
</div> </div>
</DropdownMenuContent> </TldrawUiDropdownMenuContent>
</DropdownMenuRoot> </TldrawUiDropdownMenuRoot>
<DropdownMenuRoot id={`style panel ${uiTypeB}`}> <TldrawUiDropdownMenuRoot id={`style panel ${uiTypeB}`}>
<DropdownMenuTrigger <TldrawUiDropdownMenuTrigger>
<TldrawUiButton
type="icon" type="icon"
data-testid={`style.${uiTypeB}`} data-testid={`style.${uiTypeB}`}
title={ title={
@ -111,30 +111,29 @@ export const DoubleDropdownPicker = React.memo(function DoubleDropdownPicker<T e
? msg('style-panel.mixed') ? msg('style-panel.mixed')
: msg(`${uiTypeB}-style.${valueB.value}` as TLUiTranslationKey)) : msg(`${uiTypeB}-style.${valueB.value}` as TLUiTranslationKey))
} }
icon={iconB as any} >
smallIcon <TldrawUiButtonIcon icon={iconB} small />
/> </TldrawUiButton>
<DropdownMenuContent side="bottom" align="end" sideOffset={0} alignOffset={-2}> </TldrawUiDropdownMenuTrigger>
<TldrawUiDropdownMenuContent side="bottom" align="end" sideOffset={0} alignOffset={-2}>
<div className="tlui-buttons__grid"> <div className="tlui-buttons__grid">
{itemsB.map((item) => { {itemsB.map((item) => {
return ( return (
<DropdownMenuItem <TldrawUiDropdownMenuItem key={item.value}>
<TldrawUiButton
type="icon" type="icon"
title={ title={`${msg(labelB)}${msg(`${uiTypeB}-style.${item.value}` as TLUiTranslationKey)}`}
msg(labelB) +
' — ' +
msg(`${uiTypeB}-style.${item.value}` as TLUiTranslationKey)
}
data-testid={`style.${uiTypeB}.${item.value}`} data-testid={`style.${uiTypeB}.${item.value}`}
key={item.value}
icon={item.icon as TLUiIconType}
onClick={() => onValueChange(styleB, item.value, false)} onClick={() => onValueChange(styleB, item.value, false)}
/> >
<TldrawUiButtonIcon icon={item.icon} />
</TldrawUiButton>
</TldrawUiDropdownMenuItem>
) )
})} })}
</div> </div>
</DropdownMenuContent> </TldrawUiDropdownMenuContent>
</DropdownMenuRoot> </TldrawUiDropdownMenuRoot>
</div> </div>
</div> </div>
) )

View file

@ -1,16 +1,18 @@
import { SharedStyle, StyleProp } from '@tldraw/editor' import { SharedStyle, StyleProp } from '@tldraw/editor'
import * as React from 'react' import * as React from 'react'
import { StyleValuesForUi } from '../../../styles'
import { TLUiTranslationKey } from '../../hooks/useTranslation/TLUiTranslationKey' import { TLUiTranslationKey } from '../../hooks/useTranslation/TLUiTranslationKey'
import { useTranslation } from '../../hooks/useTranslation/useTranslation' import { useTranslation } from '../../hooks/useTranslation/useTranslation'
import { TLUiIconType } from '../../icon-types' import { TLUiIconType } from '../../icon-types'
import { TLUiButtonProps } from '../primitives/Button' import { TLUiButtonProps, TldrawUiButton } from '../primitives/Button/TldrawUiButton'
import { TldrawUiButtonIcon } from '../primitives/Button/TldrawUiButtonIcon'
import { TldrawUiButtonLabel } from '../primitives/Button/TldrawUiButtonLabel'
import { import {
DropdownMenuContent, TldrawUiDropdownMenuContent,
DropdownMenuItem, TldrawUiDropdownMenuItem,
DropdownMenuRoot, TldrawUiDropdownMenuRoot,
DropdownMenuTrigger, TldrawUiDropdownMenuTrigger,
} from '../primitives/DropdownMenu' } from '../primitives/TldrawUiDropdownMenu'
import { StyleValuesForUi } from './styles'
interface DropdownPickerProps<T extends string> { interface DropdownPickerProps<T extends string> {
id: string id: string
@ -40,35 +42,38 @@ export const DropdownPicker = React.memo(function DropdownPicker<T extends strin
[items, value] [items, value]
) )
return ( const titleStr =
<DropdownMenuRoot id={`style panel ${id}`}>
<DropdownMenuTrigger
type={type}
data-testid={`style.${uiType}`}
title={
value.type === 'mixed' value.type === 'mixed'
? msg('style-panel.mixed') ? msg('style-panel.mixed')
: msg(`${uiType}-style.${value.value}` as TLUiTranslationKey) : msg(`${uiType}-style.${value.value}` as TLUiTranslationKey)
} const labelStr = label ? msg(label) : ''
label={label}
icon={(icon as TLUiIconType) ?? 'mixed'} return (
/> <TldrawUiDropdownMenuRoot id={`style panel ${id}`}>
<DropdownMenuContent side="left" align="center" alignOffset={0}> <TldrawUiDropdownMenuTrigger>
<TldrawUiButton type={type} data-testid={`style.${uiType}`} title={titleStr}>
<TldrawUiButtonLabel>{labelStr}</TldrawUiButtonLabel>
<TldrawUiButtonIcon icon={(icon as TLUiIconType) ?? 'mixed'} />
</TldrawUiButton>
</TldrawUiDropdownMenuTrigger>
<TldrawUiDropdownMenuContent side="left" align="center" alignOffset={0}>
<div className="tlui-buttons__grid"> <div className="tlui-buttons__grid">
{items.map((item) => { {items.map((item) => {
return ( return (
<DropdownMenuItem <TldrawUiDropdownMenuItem key={item.value}>
<TldrawUiButton
type="icon" type="icon"
data-testid={`style.${uiType}.${item.value}`} data-testid={`style.${uiType}.${item.value}`}
title={msg(`${uiType}-style.${item.value}` as TLUiTranslationKey)} title={msg(`${uiType}-style.${item.value}` as TLUiTranslationKey)}
key={item.value}
icon={item.icon as TLUiIconType}
onClick={() => onValueChange(style, item.value, false)} onClick={() => onValueChange(style, item.value, false)}
/> >
<TldrawUiButtonIcon icon={item.icon} />
</TldrawUiButton>
</TldrawUiDropdownMenuItem>
) )
})} })}
</div> </div>
</DropdownMenuContent> </TldrawUiDropdownMenuContent>
</DropdownMenuRoot> </TldrawUiDropdownMenuRoot>
) )
}) })

View file

@ -3,8 +3,9 @@ import * as React from 'react'
import { TLUiToast, useToasts } from '../context/toasts' import { TLUiToast, useToasts } from '../context/toasts'
import { useTranslation } from '../hooks/useTranslation/useTranslation' import { useTranslation } from '../hooks/useTranslation/useTranslation'
import { TLUiIconType } from '../icon-types' import { TLUiIconType } from '../icon-types'
import { Button } from './primitives/Button' import { TldrawUiButton } from './primitives/Button/TldrawUiButton'
import { Icon } from './primitives/Icon' import { TldrawUiButtonLabel } from './primitives/Button/TldrawUiButtonLabel'
import { TldrawUiIcon } from './primitives/TldrawUiIcon'
function Toast({ toast }: { toast: TLUiToast }) { function Toast({ toast }: { toast: TLUiToast }) {
const { removeToast } = useToasts() const { removeToast } = useToasts()
@ -26,7 +27,7 @@ function Toast({ toast }: { toast: TLUiToast }) {
> >
{toast.icon && ( {toast.icon && (
<div className="tlui-toast__icon"> <div className="tlui-toast__icon">
<Icon icon={toast.icon as TLUiIconType} /> <TldrawUiIcon icon={toast.icon as TLUiIconType} />
</div> </div>
)} )}
<div className="tlui-toast__main"> <div className="tlui-toast__main">
@ -40,22 +41,28 @@ function Toast({ toast }: { toast: TLUiToast }) {
<div className="tlui-toast__actions"> <div className="tlui-toast__actions">
{toast.actions.map((action, i) => ( {toast.actions.map((action, i) => (
<T.Action key={i} altText={action.label} asChild onClick={action.onClick}> <T.Action key={i} altText={action.label} asChild onClick={action.onClick}>
<Button type={action.type}>{action.label}</Button> <TldrawUiButton type={action.type}>
<TldrawUiButtonLabel>{action.label}</TldrawUiButtonLabel>
</TldrawUiButton>
</T.Action> </T.Action>
))} ))}
<T.Close asChild> <T.Close asChild>
<Button type="normal" className="tlui-toast__close" style={{ marginLeft: 'auto' }}> <TldrawUiButton
{toast.closeLabel ?? msg('toast.close')} type="normal"
</Button> className="tlui-toast__close"
style={{ marginLeft: 'auto' }}
>
<TldrawUiButtonLabel>{toast.closeLabel ?? msg('toast.close')}</TldrawUiButtonLabel>
</TldrawUiButton>
</T.Close> </T.Close>
</div> </div>
)} )}
</div> </div>
{!hasActions && ( {!hasActions && (
<T.Close asChild> <T.Close asChild>
<Button type="normal" className="tlui-toast__close"> <TldrawUiButton type="normal" className="tlui-toast__close">
{toast.closeLabel ?? msg('toast.close')} <TldrawUiButtonLabel>{toast.closeLabel ?? msg('toast.close')}</TldrawUiButtonLabel>
</Button> </TldrawUiButton>
</T.Close> </T.Close>
)} )}
</T.Root> </T.Root>

View file

@ -10,15 +10,16 @@ import { useReadonly } from '../../hooks/useReadonly'
import { TLUiToolbarItem, useToolbarSchema } from '../../hooks/useToolbarSchema' import { TLUiToolbarItem, useToolbarSchema } from '../../hooks/useToolbarSchema'
import { TLUiToolItem } from '../../hooks/useTools' import { TLUiToolItem } from '../../hooks/useTools'
import { useTranslation } from '../../hooks/useTranslation/useTranslation' import { useTranslation } from '../../hooks/useTranslation/useTranslation'
import { kbdStr } from '../../kbd-utils'
import { MobileStylePanel } from '../MobileStylePanel' import { MobileStylePanel } from '../MobileStylePanel'
import { Button } from '../primitives/Button' import { TldrawUiButton } from '../primitives/Button/TldrawUiButton'
import { TldrawUiButtonIcon } from '../primitives/Button/TldrawUiButtonIcon'
import { import {
DropdownMenuContent, TldrawUiDropdownMenuContent,
DropdownMenuItem, TldrawUiDropdownMenuItem,
DropdownMenuRoot, TldrawUiDropdownMenuRoot,
DropdownMenuTrigger, TldrawUiDropdownMenuTrigger,
} from '../primitives/DropdownMenu' } from '../primitives/TldrawUiDropdownMenu'
import { kbdStr } from '../primitives/shared'
import { ToggleToolLockedButton } from './ToggleToolLockedButton' import { ToggleToolLockedButton } from './ToggleToolLockedButton'
/** @public */ /** @public */
@ -143,18 +144,21 @@ export const DefaultToolbar = memo(function DefaultToolbar() {
)} )}
/> />
{/* The dropdown to select everything else */} {/* The dropdown to select everything else */}
<DropdownMenuRoot id="toolbar overflow" modal={false}> <TldrawUiDropdownMenuRoot id="toolbar overflow" modal={false}>
<DropdownMenuTrigger <TldrawUiDropdownMenuTrigger>
className="tlui-toolbar__overflow" <TldrawUiButton
icon="chevron-up"
type="tool"
data-testid="tools.more"
title={msg('tool-panel.more')} title={msg('tool-panel.more')}
/> type="tool"
<DropdownMenuContent side="top" align="center"> className="tlui-toolbar__overflow"
data-testid="tools.more"
>
<TldrawUiButtonIcon icon="chevron-up" />
</TldrawUiButton>
</TldrawUiDropdownMenuTrigger>
<TldrawUiDropdownMenuContent side="top" align="center">
<OverflowToolsContent toolbarItems={itemsInDropdown} /> <OverflowToolsContent toolbarItems={itemsInDropdown} />
</DropdownMenuContent> </TldrawUiDropdownMenuContent>
</DropdownMenuRoot> </TldrawUiDropdownMenuRoot>
</> </>
) : null} ) : null}
</div> </div>
@ -180,18 +184,22 @@ const OverflowToolsContent = track(function OverflowToolsContent({
<div className="tlui-buttons__grid"> <div className="tlui-buttons__grid">
{toolbarItems.map(({ toolItem: { id, meta, kbd, label, onSelect, icon } }) => { {toolbarItems.map(({ toolItem: { id, meta, kbd, label, onSelect, icon } }) => {
return ( return (
<DropdownMenuItem <TldrawUiDropdownMenuItem
key={id} key={id}
type="icon"
className="tlui-button-grid__button"
data-testid={`tools.more.${id}`}
data-tool={id} data-tool={id}
data-geo={meta?.geo ?? ''} data-geo={meta?.geo ?? ''}
aria-label={label} aria-label={label}
>
<TldrawUiButton
type="icon"
className="tlui-button-grid__button"
onClick={() => onSelect('toolbar')} onClick={() => onSelect('toolbar')}
data-testid={`tools.more.${id}`}
title={label ? `${msg(label)} ${kbd ? kbdStr(kbd) : ''}` : ''} title={label ? `${msg(label)} ${kbd ? kbdStr(kbd) : ''}` : ''}
icon={icon} >
/> <TldrawUiButtonIcon icon={icon} />
</TldrawUiButton>
</TldrawUiDropdownMenuItem>
) )
})} })}
</div> </div>
@ -208,21 +216,22 @@ function ToolbarButton({
isSelected: boolean isSelected: boolean
}) { }) {
return ( return (
<Button <TldrawUiButton
type="tool" type="tool"
data-testid={`tools.${item.id}`} data-testid={`tools.${item.id}`}
data-tool={item.id} data-tool={item.id}
data-geo={item.meta?.geo ?? ''} data-geo={item.meta?.geo ?? ''}
aria-label={item.label} aria-label={item.label}
title={title}
icon={item.icon}
data-state={isSelected ? 'selected' : undefined} data-state={isSelected ? 'selected' : undefined}
onClick={() => item.onSelect('toolbar')} onClick={() => item.onSelect('toolbar')}
title={title}
onTouchStart={(e) => { onTouchStart={(e) => {
preventDefault(e) preventDefault(e)
item.onSelect('toolbar') item.onSelect('toolbar')
}} }}
/> >
<TldrawUiButtonIcon icon={item.icon} />
</TldrawUiButton>
) )
} }

View file

@ -3,7 +3,8 @@ import classNames from 'classnames'
import { PORTRAIT_BREAKPOINT } from '../../constants' import { PORTRAIT_BREAKPOINT } from '../../constants'
import { useBreakpoint } from '../../context/breakpoints' import { useBreakpoint } from '../../context/breakpoints'
import { useTranslation } from '../../hooks/useTranslation/useTranslation' import { useTranslation } from '../../hooks/useTranslation/useTranslation'
import { Button } from '../primitives/Button' import { TldrawUiButton } from '../primitives/Button/TldrawUiButton'
import { TldrawUiButtonIcon } from '../primitives/Button/TldrawUiButtonIcon'
interface ToggleToolLockedButtonProps { interface ToggleToolLockedButtonProps {
activeToolId?: string activeToolId?: string
@ -32,15 +33,15 @@ export function ToggleToolLockedButton({ activeToolId }: ToggleToolLockedButtonP
if (!activeToolId || NOT_LOCKABLE_TOOLS.includes(activeToolId)) return null if (!activeToolId || NOT_LOCKABLE_TOOLS.includes(activeToolId)) return null
return ( return (
<Button <TldrawUiButton
type="normal" type="normal"
title={msg('action.toggle-tool-lock')} title={msg('action.toggle-tool-lock')}
className={classNames('tlui-toolbar__lock-button', { className={classNames('tlui-toolbar__lock-button', {
'tlui-toolbar__lock-button__mobile': breakpoint < PORTRAIT_BREAKPOINT.TABLET_SM, 'tlui-toolbar__lock-button__mobile': breakpoint < PORTRAIT_BREAKPOINT.TABLET_SM,
})} })}
icon={isToolLocked ? 'lock' : 'unlock'}
onClick={() => editor.updateInstanceState({ isToolLocked: !isToolLocked })} onClick={() => editor.updateInstanceState({ isToolLocked: !isToolLocked })}
smallIcon >
/> <TldrawUiButtonIcon icon={isToolLocked ? 'lock' : 'unlock'} small />
</TldrawUiButton>
) )
} }

View file

@ -5,8 +5,8 @@ import { PORTRAIT_BREAKPOINT } from '../../constants'
import { useBreakpoint } from '../../context/breakpoints' import { useBreakpoint } from '../../context/breakpoints'
import { useMenuIsOpen } from '../../hooks/useMenuIsOpen' import { useMenuIsOpen } from '../../hooks/useMenuIsOpen'
import { useTranslation } from '../../hooks/useTranslation/useTranslation' import { useTranslation } from '../../hooks/useTranslation/useTranslation'
import { TldrawUiMenuContextProvider } from '../menus/TldrawUiMenuContext' import { TldrawUiButton } from '../primitives/Button/TldrawUiButton'
import { Button } from '../primitives/Button' import { TldrawUiMenuContextProvider } from '../primitives/menus/TldrawUiMenuContext'
import { DefaultZoomMenuContent } from './DefaultZoomMenuContent' import { DefaultZoomMenuContent } from './DefaultZoomMenuContent'
/** @public */ /** @public */
@ -59,7 +59,7 @@ const ZoomTriggerButton = forwardRef<HTMLButtonElement, any>(
}, [editor]) }, [editor])
return ( return (
<Button <TldrawUiButton
ref={ref} ref={ref}
{...props} {...props}
type="icon" type="icon"
@ -76,7 +76,7 @@ const ZoomTriggerButton = forwardRef<HTMLButtonElement, any>(
{breakpoint < PORTRAIT_BREAKPOINT.MOBILE ? null : ( {breakpoint < PORTRAIT_BREAKPOINT.MOBILE ? null : (
<span style={{ flexGrow: 0, textAlign: 'center' }}>{Math.floor(zoom * 100)}%</span> <span style={{ flexGrow: 0, textAlign: 'center' }}>{Math.floor(zoom * 100)}%</span>
)} )}
</Button> </TldrawUiButton>
) )
} }
) )

View file

@ -1,6 +1,6 @@
import { useActions } from '../../context/actions' import { useActions } from '../../context/actions'
import { ZoomTo100MenuItem, ZoomToFitMenuItem, ZoomToSelectionMenuItem } from '../menu-items' import { ZoomTo100MenuItem, ZoomToFitMenuItem, ZoomToSelectionMenuItem } from '../menu-items'
import { TldrawUiMenuItem } from '../menus/TldrawUiMenuItem' import { TldrawUiMenuItem } from '../primitives/menus/TldrawUiMenuItem'
/** @public */ /** @public */
export function DefaultZoomMenuContent() { export function DefaultZoomMenuContent() {

View file

@ -21,10 +21,10 @@ import {
useThreeStackableItems, useThreeStackableItems,
useUnlockedSelectedShapesCount, useUnlockedSelectedShapesCount,
} from '../hooks/menu-hooks' } from '../hooks/menu-hooks'
import { TldrawUiMenuCheckboxItem } from './menus/TldrawUiMenuCheckboxItem' import { TldrawUiMenuCheckboxItem } from './primitives/menus/TldrawUiMenuCheckboxItem'
import { TldrawUiMenuGroup } from './menus/TldrawUiMenuGroup' import { TldrawUiMenuGroup } from './primitives/menus/TldrawUiMenuGroup'
import { TldrawUiMenuItem } from './menus/TldrawUiMenuItem' import { TldrawUiMenuItem } from './primitives/menus/TldrawUiMenuItem'
import { TldrawUiMenuSubmenu } from './menus/TldrawUiMenuSubmenu' import { TldrawUiMenuSubmenu } from './primitives/menus/TldrawUiMenuSubmenu'
/* -------------------- Selection ------------------- */ /* -------------------- Selection ------------------- */

View file

@ -1,80 +0,0 @@
import { useEditor } from '@tldraw/editor'
import classnames from 'classnames'
import * as React from 'react'
import { TLUiTranslationKey } from '../../hooks/useTranslation/TLUiTranslationKey'
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
import { TLUiIconType } from '../../icon-types'
import { Spinner } from '../Spinner'
import { Icon } from './Icon'
import { Kbd } from './Kbd'
/** @public */
export interface TLUiButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
loading?: boolean // TODO: loading spinner
disabled?: boolean
label?: TLUiTranslationKey | Exclude<string, TLUiTranslationKey>
icon?: TLUiIconType | Exclude<string, TLUiIconType>
spinner?: boolean
iconLeft?: TLUiIconType | Exclude<string, TLUiIconType>
smallIcon?: boolean
kbd?: string
isChecked?: boolean
invertIcon?: boolean
type: 'normal' | 'primary' | 'danger' | 'low' | 'icon' | 'tool' | 'menu' | 'help'
}
/** @public */
export const Button = React.forwardRef<HTMLButtonElement, TLUiButtonProps>(function Button(
{
label,
icon,
invertIcon,
iconLeft,
smallIcon,
kbd,
isChecked = false,
type,
children,
spinner,
disabled,
...props
},
ref
) {
const msg = useTranslation()
const labelStr = label ? msg(label) : ''
const editor = useEditor()
// If the button is getting disabled while it's focused, move focus to the editor
// so that the user can continue using keyboard shortcuts
const current = (ref as React.MutableRefObject<HTMLButtonElement | null>)?.current
if (disabled && current === document.activeElement) {
editor.getContainer().focus()
}
return (
<button
ref={ref}
draggable={false}
type="button"
disabled={disabled}
{...props}
title={props.title ?? labelStr}
className={classnames('tlui-button', `tlui-button__${type}`, props.className)}
>
{iconLeft && <Icon icon={iconLeft} className="tlui-button__icon-left" small />}
{children}
{label && (
<span className="tlui-button__label" draggable={false}>
{labelStr}
{isChecked && <Icon icon="check" />}
</span>
)}
{kbd && <Kbd>{kbd}</Kbd>}
{icon && !spinner && (
<Icon icon={icon} small={!!label || smallIcon} invertIcon={invertIcon} />
)}
{spinner && <Spinner />}
</button>
)
})

View file

@ -0,0 +1,36 @@
import { useEditor } from '@tldraw/editor'
import classnames from 'classnames'
import * as React from 'react'
/** @public */
export interface TLUiButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
disabled?: boolean
type: 'normal' | 'primary' | 'danger' | 'low' | 'icon' | 'tool' | 'menu' | 'help'
}
/** @public */
export const TldrawUiButton = React.forwardRef<HTMLButtonElement, TLUiButtonProps>(
function TldrawUiButton({ children, disabled, type, ...props }, ref) {
const editor = useEditor()
// If the button is getting disabled while it's focused, move focus to the editor
// so that the user can continue using keyboard shortcuts
const current = (ref as React.MutableRefObject<HTMLButtonElement | null>)?.current
if (disabled && current === document.activeElement) {
editor.getContainer().focus()
}
return (
<button
ref={ref}
type="button"
draggable={false}
disabled={disabled}
{...props}
className={classnames('tlui-button', `tlui-button__${type}`, props.className)}
>
{children}
</button>
)
}
)

View file

@ -0,0 +1,11 @@
import { TldrawUiIcon } from '../TldrawUiIcon'
/** @public */
export type TLUiButtonCheckProps = { checked: boolean }
/** @public */
export function TldrawUiButtonCheck({ checked }: TLUiButtonCheckProps) {
return (
<TldrawUiIcon icon={checked ? 'check' : 'checkbox-empty'} className="tlui-button__icon" small />
)
}

View file

@ -0,0 +1,15 @@
import { TldrawUiIcon } from '../TldrawUiIcon'
/** @public */
export type TLUiButtonIconProps = {
icon: string
small?: boolean
invertIcon?: boolean
}
/** @public */
export function TldrawUiButtonIcon({ icon, small, invertIcon }: TLUiButtonIconProps) {
return (
<TldrawUiIcon className="tlui-button__icon" icon={icon} small={small} invertIcon={invertIcon} />
)
}

View file

@ -0,0 +1,7 @@
/** @public */
export type TLUiButtonLabelProps = { children?: any }
/** @public */
export function TldrawUiButtonLabel({ children }: TLUiButtonLabelProps) {
return <span className="tlui-button__label">{children}</span>
}

View file

@ -0,0 +1,6 @@
import { Spinner } from '../../Spinner'
/** @public */
export function TldrawUiButtonSpinner() {
return <Spinner />
}

View file

@ -1,165 +0,0 @@
import { stopEventPropagation, useEditor } from '@tldraw/editor'
import classNames from 'classnames'
import * as React from 'react'
import { TLUiTranslationKey } from '../../hooks/useTranslation/TLUiTranslationKey'
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
import { TLUiIconType } from '../../icon-types'
import { Icon } from './Icon'
/** @public */
export interface TLUiInputProps {
disabled?: boolean
label?: TLUiTranslationKey | Exclude<string, TLUiTranslationKey>
icon?: TLUiIconType | Exclude<string, TLUiIconType>
iconLeft?: TLUiIconType | Exclude<string, TLUiIconType>
autofocus?: boolean
autoselect?: boolean
children?: any
defaultValue?: string
placeholder?: string
onComplete?: (value: string) => void
onValueChange?: (value: string) => void
onCancel?: (value: string) => void
onBlur?: (value: string) => void
className?: string
/**
* Usually on iOS when you focus an input, the browser will adjust the viewport to bring the input
* into view. Sometimes this doesn't work properly though - for example, if the input is newly
* created, iOS seems to have a hard time adjusting the viewport for it. This prop allows you to
* opt-in to some extra code to manually bring the input into view when the visual viewport of the
* browser changes, but we don't want to use it everywhere because generally the native behavior
* looks nicer in scenarios where it's sufficient.
*/
shouldManuallyMaintainScrollPositionWhenFocused?: boolean
value?: string
}
/** @public */
export const Input = React.forwardRef<HTMLInputElement, TLUiInputProps>(function Input(
{
className,
label,
icon,
iconLeft,
autoselect = false,
autofocus = false,
defaultValue,
placeholder,
onComplete,
onValueChange,
onCancel,
onBlur,
shouldManuallyMaintainScrollPositionWhenFocused = false,
children,
value,
},
ref
) {
const editor = useEditor()
const rInputRef = React.useRef<HTMLInputElement>(null)
// combine rInputRef and ref
React.useImperativeHandle(ref, () => rInputRef.current as HTMLInputElement)
const msg = useTranslation()
const rInitialValue = React.useRef<string>(defaultValue ?? '')
const rCurrentValue = React.useRef<string>(defaultValue ?? '')
const [isFocused, setIsFocused] = React.useState(false)
const handleFocus = React.useCallback(
(e: React.FocusEvent<HTMLInputElement>) => {
setIsFocused(true)
const elm = e.currentTarget as HTMLInputElement
rCurrentValue.current = elm.value
requestAnimationFrame(() => {
if (autoselect) {
elm.select()
}
})
},
[autoselect]
)
const handleChange = React.useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.currentTarget.value
rCurrentValue.current = value
onValueChange?.(value)
},
[onValueChange]
)
const handleKeyUp = React.useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
switch (e.key) {
case 'Enter': {
e.currentTarget.blur()
stopEventPropagation(e)
onComplete?.(e.currentTarget.value)
break
}
case 'Escape': {
e.currentTarget.value = rInitialValue.current
e.currentTarget.blur()
stopEventPropagation(e)
onCancel?.(e.currentTarget.value)
break
}
}
},
[onComplete, onCancel]
)
const handleBlur = React.useCallback(
(e: React.FocusEvent<HTMLInputElement>) => {
setIsFocused(false)
const value = e.currentTarget.value
onBlur?.(value)
},
[onBlur]
)
React.useEffect(() => {
if (!editor.environment.isIos) return
const visualViewport = window.visualViewport
if (isFocused && shouldManuallyMaintainScrollPositionWhenFocused && visualViewport) {
const onViewportChange = () => {
rInputRef.current?.scrollIntoView({ block: 'center' })
}
visualViewport.addEventListener('resize', onViewportChange)
visualViewport.addEventListener('scroll', onViewportChange)
requestAnimationFrame(() => {
rInputRef.current?.scrollIntoView({ block: 'center' })
})
return () => {
visualViewport.removeEventListener('resize', onViewportChange)
visualViewport.removeEventListener('scroll', onViewportChange)
}
}
}, [editor, isFocused, shouldManuallyMaintainScrollPositionWhenFocused])
return (
<div draggable={false} className="tlui-input__wrapper">
{children}
{label && <label>{msg(label)}</label>}
{iconLeft && <Icon icon={iconLeft} className="tlui-icon-left" small />}
<input
ref={rInputRef}
className={classNames('tlui-input', className)}
type="text"
defaultValue={defaultValue}
onKeyUp={handleKeyUp}
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
autoFocus={autofocus}
placeholder={placeholder}
value={value}
/>
{icon && <Icon icon={icon} small={!!label} />}
</div>
)
})

View file

@ -8,16 +8,15 @@ import {
useValue, useValue,
} from '@tldraw/editor' } from '@tldraw/editor'
import classNames from 'classnames' import classNames from 'classnames'
import * as React from 'react' import { memo, useMemo, useRef } from 'react'
import { useRef } from 'react' import { StyleValuesForUi } from '../../../styles'
import { TLUiTranslationKey } from '../../hooks/useTranslation/TLUiTranslationKey' import { TLUiTranslationKey } from '../../hooks/useTranslation/TLUiTranslationKey'
import { useTranslation } from '../../hooks/useTranslation/useTranslation' import { useTranslation } from '../../hooks/useTranslation/useTranslation'
import { TLUiIconType } from '../../icon-types' import { TldrawUiButton } from './Button/TldrawUiButton'
import { StyleValuesForUi } from '../StylePanel/styles' import { TldrawUiButtonIcon } from './Button/TldrawUiButtonIcon'
import { Button } from './Button'
/** @internal */ /** @public */
export interface ButtonPickerProps<T extends string> { export interface TLUiButtonPickerProps<T extends string> {
title: string title: string
uiType: string uiType: string
style: StyleProp<T> style: StyleProp<T>
@ -26,7 +25,10 @@ export interface ButtonPickerProps<T extends string> {
onValueChange: (style: StyleProp<T>, value: T, squashing: boolean) => void onValueChange: (style: StyleProp<T>, value: T, squashing: boolean) => void
} }
function _ButtonPicker<T extends string>(props: ButtonPickerProps<T>) { /** @public */
export const TldrawUiButtonPicker = memo(function TldrawUiButtonPicker<T extends string>(
props: TLUiButtonPickerProps<T>
) {
const { const {
uiType, uiType,
items, items,
@ -46,7 +48,7 @@ function _ButtonPicker<T extends string>(props: ButtonPickerProps<T>) {
handleButtonPointerDown, handleButtonPointerDown,
handleButtonPointerEnter, handleButtonPointerEnter,
handleButtonPointerUp, handleButtonPointerUp,
} = React.useMemo(() => { } = useMemo(() => {
const handlePointerUp = () => { const handlePointerUp = () => {
rPointing.current = false rPointing.current = false
window.removeEventListener('pointerup', handlePointerUp) window.removeEventListener('pointerup', handlePointerUp)
@ -99,7 +101,7 @@ function _ButtonPicker<T extends string>(props: ButtonPickerProps<T>) {
return ( return (
<div className={classNames('tlui-buttons__grid')}> <div className={classNames('tlui-buttons__grid')}>
{items.map((item) => ( {items.map((item) => (
<Button <TldrawUiButton
type="icon" type="icon"
key={item.value} key={item.value}
data-id={item.value} data-id={item.value}
@ -117,12 +119,10 @@ function _ButtonPicker<T extends string>(props: ButtonPickerProps<T>) {
onPointerDown={handleButtonPointerDown} onPointerDown={handleButtonPointerDown}
onPointerUp={handleButtonPointerUp} onPointerUp={handleButtonPointerUp}
onClick={handleButtonClick} onClick={handleButtonClick}
icon={item.icon as TLUiIconType} >
/> <TldrawUiButtonIcon icon={item.icon} />
</TldrawUiButton>
))} ))}
</div> </div>
) )
} })
/** @internal */
export const ButtonPicker = React.memo(_ButtonPicker) as typeof _ButtonPicker

View file

@ -1,7 +1,7 @@
import * as _Dialog from '@radix-ui/react-dialog' import * as _Dialog from '@radix-ui/react-dialog'
import classNames from 'classnames' import classNames from 'classnames'
import { Button } from './Button' import { TldrawUiButton } from './Button/TldrawUiButton'
import { Icon } from './Icon' import { TldrawUiButtonIcon } from './Button/TldrawUiButtonIcon'
/** @public */ /** @public */
export type TLUiDialogHeaderProps = { export type TLUiDialogHeaderProps = {
@ -10,7 +10,7 @@ export type TLUiDialogHeaderProps = {
} }
/** @public */ /** @public */
export function DialogHeader({ className, children }: TLUiDialogHeaderProps) { export function TldrawUiDialogHeader({ className, children }: TLUiDialogHeaderProps) {
return <div className={classNames('tlui-dialog__header', className)}>{children}</div> return <div className={classNames('tlui-dialog__header', className)}>{children}</div>
} }
@ -21,7 +21,7 @@ export type TLUiDialogTitleProps = {
} }
/** @public */ /** @public */
export function DialogTitle({ className, children }: TLUiDialogTitleProps) { export function TldrawUiDialogTitle({ className, children }: TLUiDialogTitleProps) {
return ( return (
<_Dialog.DialogTitle dir="ltr" className={classNames('tlui-dialog__header__title', className)}> <_Dialog.DialogTitle dir="ltr" className={classNames('tlui-dialog__header__title', className)}>
{children} {children}
@ -30,17 +30,17 @@ export function DialogTitle({ className, children }: TLUiDialogTitleProps) {
} }
/** @public */ /** @public */
export function DialogCloseButton() { export function TldrawUiDialogCloseButton() {
return ( return (
<div className="tlui-dialog__header__close"> <div className="tlui-dialog__header__close">
<_Dialog.DialogClose data-testid="dialog.close" dir="ltr" asChild> <_Dialog.DialogClose data-testid="dialog.close" dir="ltr" asChild>
<Button <TldrawUiButton
type="icon" type="icon"
aria-label="Close" aria-label="Close"
onTouchEnd={(e) => (e.target as HTMLButtonElement).click()} onTouchEnd={(e) => (e.target as HTMLButtonElement).click()}
> >
<Icon small icon="cross-2" /> <TldrawUiButtonIcon small icon="cross-2" />
</Button> </TldrawUiButton>
</_Dialog.DialogClose> </_Dialog.DialogClose>
</div> </div>
) )
@ -54,7 +54,7 @@ export type TLUiDialogBodyProps = {
} }
/** @public */ /** @public */
export function DialogBody({ className, children, style }: TLUiDialogBodyProps) { export function TldrawUiDialogBody({ className, children, style }: TLUiDialogBodyProps) {
return ( return (
<div className={classNames('tlui-dialog__body', className)} style={style}> <div className={classNames('tlui-dialog__body', className)} style={style}>
{children} {children}
@ -69,6 +69,6 @@ export type TLUiDialogFooterProps = {
} }
/** @public */ /** @public */
export function DialogFooter({ className, children }: TLUiDialogFooterProps) { export function TldrawUiDialogFooter({ className, children }: TLUiDialogFooterProps) {
return <div className={classNames('tlui-dialog__footer', className)}>{children}</div> return <div className={classNames('tlui-dialog__footer', className)}>{children}</div>
} }

View file

@ -1,8 +1,10 @@
import * as _DropdownMenu from '@radix-ui/react-dropdown-menu' import * as _DropdownMenu from '@radix-ui/react-dropdown-menu'
import { preventDefault, useContainer } from '@tldraw/editor' import { preventDefault, useContainer } from '@tldraw/editor'
import { useMenuIsOpen } from '../../hooks/useMenuIsOpen' import { useMenuIsOpen } from '../../hooks/useMenuIsOpen'
import { Button, TLUiButtonProps } from './Button' import { TldrawUiButton } from './Button/TldrawUiButton'
import { Icon } from './Icon' import { TldrawUiButtonIcon } from './Button/TldrawUiButtonIcon'
import { TldrawUiButtonLabel } from './Button/TldrawUiButtonLabel'
import { TldrawUiIcon } from './TldrawUiIcon'
/** @public */ /** @public */
export type TLUiDropdownMenuRootProps = { export type TLUiDropdownMenuRootProps = {
@ -13,7 +15,7 @@ export type TLUiDropdownMenuRootProps = {
} }
/** @public */ /** @public */
export function DropdownMenuRoot({ export function TldrawUiDropdownMenuRoot({
id, id,
children, children,
modal = false, modal = false,
@ -34,20 +36,21 @@ export function DropdownMenuRoot({
} }
/** @public */ /** @public */
export interface TLUiDropdownMenuTriggerProps extends TLUiButtonProps { export interface TLUiDropdownMenuTriggerProps {
children?: any children?: any
} }
/** @public */ /** @public */
export function DropdownMenuTrigger({ children, ...rest }: TLUiDropdownMenuTriggerProps) { export function TldrawUiDropdownMenuTrigger({ children, ...rest }: TLUiDropdownMenuTriggerProps) {
return ( return (
<_DropdownMenu.Trigger <_DropdownMenu.Trigger
dir="ltr" dir="ltr"
asChild asChild
// Firefox fix: Stop the dropdown immediately closing after touch // Firefox fix: Stop the dropdown immediately closing after touch
onTouchEnd={(e) => preventDefault(e)} onTouchEnd={(e) => preventDefault(e)}
{...rest}
> >
<Button {...rest}>{children}</Button> {children}
</_DropdownMenu.Trigger> </_DropdownMenu.Trigger>
) )
} }
@ -63,7 +66,7 @@ export type TLUiDropdownMenuContentProps = {
} }
/** @public */ /** @public */
export function DropdownMenuContent({ export function TldrawUiDropdownMenuContent({
side = 'bottom', side = 'bottom',
align = 'start', align = 'start',
sideOffset = 8, sideOffset = 8,
@ -92,7 +95,7 @@ export function DropdownMenuContent({
export type TLUiDropdownMenuSubProps = { id: string; children: any } export type TLUiDropdownMenuSubProps = { id: string; children: any }
/** @public */ /** @public */
export function DropdownMenuSub({ id, children }: TLUiDropdownMenuSubProps) { export function TldrawUiDropdownMenuSub({ id, children }: TLUiDropdownMenuSubProps) {
const [open, onOpenChange] = useMenuIsOpen(id) const [open, onOpenChange] = useMenuIsOpen(id)
return ( return (
@ -111,21 +114,22 @@ export type TLUiDropdownMenuSubTriggerProps = {
} }
/** @public */ /** @public */
export function DropdownMenuSubTrigger({ export function TldrawUiDropdownMenuSubTrigger({
label, label,
title, title,
disabled, disabled,
}: TLUiDropdownMenuSubTriggerProps) { }: TLUiDropdownMenuSubTriggerProps) {
return ( return (
<_DropdownMenu.SubTrigger dir="ltr" asChild> <_DropdownMenu.SubTrigger dir="ltr" asChild>
<Button <TldrawUiButton
type="menu" type="menu"
className="tlui-menu__submenu__trigger" className="tlui-menu__submenu__trigger"
disabled={disabled} disabled={disabled}
title={title} title={title}
label={label} >
icon="chevron-right" <TldrawUiButtonLabel>{label}</TldrawUiButtonLabel>
/> <TldrawUiButtonIcon icon="chevron-right" small />
</TldrawUiButton>
</_DropdownMenu.SubTrigger> </_DropdownMenu.SubTrigger>
) )
} }
@ -139,7 +143,7 @@ export type TLUiDropdownMenuSubContentProps = {
} }
/** @public */ /** @public */
export function DropdownMenuSubContent({ export function TldrawUiDropdownMenuSubContent({
alignOffset = -1, alignOffset = -1,
sideOffset = -4, sideOffset = -4,
children, children,
@ -166,7 +170,10 @@ export type TLUiDropdownMenuGroupProps = {
} }
/** @public */ /** @public */
export function DropdownMenuGroup({ children, size = 'medium' }: TLUiDropdownMenuGroupProps) { export function TldrawUiDropdownMenuGroup({
children,
size = 'medium',
}: TLUiDropdownMenuGroupProps) {
return ( return (
<_DropdownMenu.Group dir="ltr" className="tlui-menu__group" data-size={size}> <_DropdownMenu.Group dir="ltr" className="tlui-menu__group" data-size={size}>
{children} {children}
@ -175,28 +182,25 @@ export function DropdownMenuGroup({ children, size = 'medium' }: TLUiDropdownMen
} }
/** @public */ /** @public */
export function DropdownMenuIndicator() { export function TldrawUiDropdownMenuIndicator() {
return ( return (
<_DropdownMenu.ItemIndicator dir="ltr" asChild> <_DropdownMenu.ItemIndicator dir="ltr" asChild>
<Icon icon="check" /> <TldrawUiIcon icon="check" />
</_DropdownMenu.ItemIndicator> </_DropdownMenu.ItemIndicator>
) )
} }
/** @public */ /** @public */
export interface TLUiDropdownMenuItemProps extends TLUiButtonProps { export interface TLUiDropdownMenuItemProps {
noClose?: boolean noClose?: boolean
children: any
} }
/** @public */ /** @public */
export function DropdownMenuItem({ noClose, ...props }: TLUiDropdownMenuItemProps) { export function TldrawUiDropdownMenuItem({ noClose, children }: TLUiDropdownMenuItemProps) {
return ( return (
<_DropdownMenu.Item <_DropdownMenu.Item dir="ltr" asChild onClick={noClose ? preventDefault : undefined}>
dir="ltr" {children}
asChild
onClick={noClose || props.isChecked !== undefined ? preventDefault : undefined}
>
<Button {...props} />
</_DropdownMenu.Item> </_DropdownMenu.Item>
) )
} }
@ -211,7 +215,7 @@ export interface TLUiDropdownMenuCheckboxItemProps {
} }
/** @public */ /** @public */
export function DropdownMenuCheckboxItem({ export function TldrawUiDropdownMenuCheckboxItem({
children, children,
onSelect, onSelect,
...rest ...rest
@ -228,38 +232,7 @@ export function DropdownMenuCheckboxItem({
> >
<div className="tlui-button__checkbox__indicator"> <div className="tlui-button__checkbox__indicator">
<_DropdownMenu.ItemIndicator dir="ltr"> <_DropdownMenu.ItemIndicator dir="ltr">
<Icon icon="check" small /> <TldrawUiIcon icon="check" small />
</_DropdownMenu.ItemIndicator>
</div>
{children}
</_DropdownMenu.CheckboxItem>
)
}
/** @public */
export interface TLUiDropdownMenuRadioItemProps {
checked?: boolean
onSelect?: (e: Event) => void
disabled?: boolean
title: string
children: any
}
/** @public */
export function DropdownMenuRadioItem({ children, ...rest }: TLUiDropdownMenuRadioItemProps) {
return (
<_DropdownMenu.CheckboxItem
dir="ltr"
className="tlui-button tlui-button__menu tlui-button__checkbox"
{...rest}
onSelect={(e) => {
preventDefault(e)
rest.onSelect?.(e)
}}
>
<div className="tlui-button__checkbox__indicator">
<_DropdownMenu.ItemIndicator dir="ltr">
<Icon icon="check" small />
</_DropdownMenu.ItemIndicator> </_DropdownMenu.ItemIndicator>
</div> </div>
{children} {children}

View file

@ -14,7 +14,7 @@ export interface TLUiIconProps extends React.HTMLProps<HTMLDivElement> {
} }
/** @public */ /** @public */
export const Icon = memo(function Icon({ export const TldrawUiIcon = memo(function TldrawUi({
small, small,
invertIcon, invertIcon,
icon, icon,

View file

@ -0,0 +1,167 @@
import { stopEventPropagation, useEditor } from '@tldraw/editor'
import classNames from 'classnames'
import * as React from 'react'
import { TLUiTranslationKey } from '../../hooks/useTranslation/TLUiTranslationKey'
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
import { TLUiIconType } from '../../icon-types'
import { TldrawUiIcon } from './TldrawUiIcon'
/** @public */
export interface TLUiInputProps {
disabled?: boolean
label?: TLUiTranslationKey | Exclude<string, TLUiTranslationKey>
icon?: TLUiIconType | Exclude<string, TLUiIconType>
iconLeft?: TLUiIconType | Exclude<string, TLUiIconType>
autofocus?: boolean
autoselect?: boolean
children?: any
defaultValue?: string
placeholder?: string
onComplete?: (value: string) => void
onValueChange?: (value: string) => void
onCancel?: (value: string) => void
onBlur?: (value: string) => void
className?: string
/**
* Usually on iOS when you focus an input, the browser will adjust the viewport to bring the input
* into view. Sometimes this doesn't work properly though - for example, if the input is newly
* created, iOS seems to have a hard time adjusting the viewport for it. This prop allows you to
* opt-in to some extra code to manually bring the input into view when the visual viewport of the
* browser changes, but we don't want to use it everywhere because generally the native behavior
* looks nicer in scenarios where it's sufficient.
*/
shouldManuallyMaintainScrollPositionWhenFocused?: boolean
value?: string
}
/** @public */
export const TldrawUiInput = React.forwardRef<HTMLInputElement, TLUiInputProps>(
function TldrawUiInput(
{
className,
label,
icon,
iconLeft,
autoselect = false,
autofocus = false,
defaultValue,
placeholder,
onComplete,
onValueChange,
onCancel,
onBlur,
shouldManuallyMaintainScrollPositionWhenFocused = false,
children,
value,
},
ref
) {
const editor = useEditor()
const rInputRef = React.useRef<HTMLInputElement>(null)
// combine rInputRef and ref
React.useImperativeHandle(ref, () => rInputRef.current as HTMLInputElement)
const msg = useTranslation()
const rInitialValue = React.useRef<string>(defaultValue ?? '')
const rCurrentValue = React.useRef<string>(defaultValue ?? '')
const [isFocused, setIsFocused] = React.useState(false)
const handleFocus = React.useCallback(
(e: React.FocusEvent<HTMLInputElement>) => {
setIsFocused(true)
const elm = e.currentTarget as HTMLInputElement
rCurrentValue.current = elm.value
requestAnimationFrame(() => {
if (autoselect) {
elm.select()
}
})
},
[autoselect]
)
const handleChange = React.useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.currentTarget.value
rCurrentValue.current = value
onValueChange?.(value)
},
[onValueChange]
)
const handleKeyUp = React.useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
switch (e.key) {
case 'Enter': {
e.currentTarget.blur()
stopEventPropagation(e)
onComplete?.(e.currentTarget.value)
break
}
case 'Escape': {
e.currentTarget.value = rInitialValue.current
e.currentTarget.blur()
stopEventPropagation(e)
onCancel?.(e.currentTarget.value)
break
}
}
},
[onComplete, onCancel]
)
const handleBlur = React.useCallback(
(e: React.FocusEvent<HTMLInputElement>) => {
setIsFocused(false)
const value = e.currentTarget.value
onBlur?.(value)
},
[onBlur]
)
React.useEffect(() => {
if (!editor.environment.isIos) return
const visualViewport = window.visualViewport
if (isFocused && shouldManuallyMaintainScrollPositionWhenFocused && visualViewport) {
const onViewportChange = () => {
rInputRef.current?.scrollIntoView({ block: 'center' })
}
visualViewport.addEventListener('resize', onViewportChange)
visualViewport.addEventListener('scroll', onViewportChange)
requestAnimationFrame(() => {
rInputRef.current?.scrollIntoView({ block: 'center' })
})
return () => {
visualViewport.removeEventListener('resize', onViewportChange)
visualViewport.removeEventListener('scroll', onViewportChange)
}
}
}, [editor, isFocused, shouldManuallyMaintainScrollPositionWhenFocused])
return (
<div draggable={false} className="tlui-input__wrapper">
{children}
{label && <label>{msg(label)}</label>}
{iconLeft && <TldrawUiIcon icon={iconLeft} className="tlui-icon-left" small />}
<input
ref={rInputRef}
className={classNames('tlui-input', className)}
type="text"
defaultValue={defaultValue}
onKeyUp={handleKeyUp}
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
autoFocus={autofocus}
placeholder={placeholder}
value={value}
/>
{icon && <TldrawUiIcon icon={icon} small={!!label} />}
</div>
)
}
)

View file

@ -1,14 +1,14 @@
import { PORTRAIT_BREAKPOINT } from '../../constants' import { PORTRAIT_BREAKPOINT } from '../../constants'
import { useBreakpoint } from '../../context/breakpoints' import { useBreakpoint } from '../../context/breakpoints'
import { kbd } from './shared' import { kbd } from '../../kbd-utils'
/** @internal */ /** @public */
export interface KbdProps { export interface TLUiKbdProps {
children: string children: string
} }
/** @internal */ /** @public */
export function Kbd({ children }: KbdProps) { export function TldrawUiKbd({ children }: TLUiKbdProps) {
const breakpoint = useBreakpoint() const breakpoint = useBreakpoint()
if (breakpoint < PORTRAIT_BREAKPOINT.MOBILE) return null if (breakpoint < PORTRAIT_BREAKPOINT.MOBILE) return null
return ( return (

View file

@ -2,7 +2,6 @@ import * as PopoverPrimitive from '@radix-ui/react-popover'
import { useContainer } from '@tldraw/editor' import { useContainer } from '@tldraw/editor'
import React from 'react' import React from 'react'
import { useMenuIsOpen } from '../../hooks/useMenuIsOpen' import { useMenuIsOpen } from '../../hooks/useMenuIsOpen'
import { Button, TLUiButtonProps } from './Button'
/** @public */ /** @public */
export type TLUiPopoverProps = { export type TLUiPopoverProps = {
@ -13,7 +12,7 @@ export type TLUiPopoverProps = {
} }
/** @public */ /** @public */
export function Popover({ id, children, onOpenChange, open }: TLUiPopoverProps) { export function TldrawUiPopover({ id, children, onOpenChange, open }: TLUiPopoverProps) {
const [isOpen, handleOpenChange] = useMenuIsOpen(id, onOpenChange) const [isOpen, handleOpenChange] = useMenuIsOpen(id, onOpenChange)
return ( return (
@ -27,15 +26,15 @@ export function Popover({ id, children, onOpenChange, open }: TLUiPopoverProps)
} }
/** @public */ /** @public */
export interface TLUiPopoverTriggerProps extends TLUiButtonProps { export interface TLUiPopoverTriggerProps {
children?: React.ReactNode children?: React.ReactNode
} }
/** @public */ /** @public */
export function PopoverTrigger({ children, ...rest }: TLUiPopoverTriggerProps) { export function TldrawUiPopoverTrigger({ children }: TLUiPopoverTriggerProps) {
return ( return (
<PopoverPrimitive.Trigger asChild dir="ltr"> <PopoverPrimitive.Trigger asChild dir="ltr">
<Button {...rest}>{children}</Button> {children}
</PopoverPrimitive.Trigger> </PopoverPrimitive.Trigger>
) )
} }
@ -50,7 +49,7 @@ export type TLUiPopoverContentProps = {
} }
/** @public */ /** @public */
export function PopoverContent({ export function TldrawUiPopoverContent({
side, side,
children, children,
align = 'center', align = 'center',

View file

@ -5,7 +5,7 @@ import { TLUiTranslationKey } from '../../hooks/useTranslation/TLUiTranslationKe
import { useTranslation } from '../../hooks/useTranslation/useTranslation' import { useTranslation } from '../../hooks/useTranslation/useTranslation'
/** @internal */ /** @internal */
export interface SliderProps { export interface TLUiSliderProps {
steps: number steps: number
value: number | null value: number | null
label: string label: string
@ -15,7 +15,7 @@ export interface SliderProps {
} }
/** @internal */ /** @internal */
export const Slider = memo(function Slider(props: SliderProps) { export const TldrawUiSlider = memo(function Slider(props: TLUiSliderProps) {
const { title, steps, value, label, onValueChange } = props const { title, steps, value, label, onValueChange } = props
const editor = useEditor() const editor = useEditor()
const msg = useTranslation() const msg = useTranslation()

View file

@ -1,13 +1,13 @@
import * as _ContextMenu from '@radix-ui/react-context-menu' import * as _ContextMenu from '@radix-ui/react-context-menu'
import * as _DropdownMenu from '@radix-ui/react-dropdown-menu' import * as _DropdownMenu from '@radix-ui/react-dropdown-menu'
import { preventDefault } from '@tldraw/editor' import { preventDefault } from '@tldraw/editor'
import { unwrapLabel } from '../../context/actions' import { unwrapLabel } from '../../../context/actions'
import { TLUiEventSource } from '../../context/events' import { TLUiEventSource } from '../../../context/events'
import { useReadonly } from '../../hooks/useReadonly' import { useReadonly } from '../../../hooks/useReadonly'
import { TLUiTranslationKey } from '../../hooks/useTranslation/TLUiTranslationKey' import { TLUiTranslationKey } from '../../../hooks/useTranslation/TLUiTranslationKey'
import { useTranslation } from '../../hooks/useTranslation/useTranslation' import { useTranslation } from '../../../hooks/useTranslation/useTranslation'
import { Icon } from '../primitives/Icon' import { TldrawUiIcon } from '../TldrawUiIcon'
import { Kbd } from '../primitives/Kbd' import { TldrawUiKbd } from '../TldrawUiKbd'
import { useTldrawUiMenuContext } from './TldrawUiMenuContext' import { useTldrawUiMenuContext } from './TldrawUiMenuContext'
/** @public */ /** @public */
@ -63,13 +63,13 @@ export function TldrawUiMenuCheckboxItem<
disabled={disabled} disabled={disabled}
checked={checked} checked={checked}
> >
<Icon small icon={checked ? 'check' : 'checkbox-empty'} /> <TldrawUiIcon small icon={checked ? 'check' : 'checkbox-empty'} />
{labelStr && ( {labelStr && (
<span className="tlui-button__label" draggable={false}> <span className="tlui-button__label" draggable={false}>
{labelStr} {labelStr}
</span> </span>
)} )}
{kbd && <Kbd>{kbd}</Kbd>} {kbd && <TldrawUiKbd>{kbd}</TldrawUiKbd>}
</_DropdownMenu.CheckboxItem> </_DropdownMenu.CheckboxItem>
) )
} }
@ -87,13 +87,13 @@ export function TldrawUiMenuCheckboxItem<
disabled={disabled} disabled={disabled}
checked={checked} checked={checked}
> >
<Icon small icon={checked ? 'check' : 'checkbox-empty'} /> <TldrawUiIcon small icon={checked ? 'check' : 'checkbox-empty'} />
{labelStr && ( {labelStr && (
<span className="tlui-button__label" draggable={false}> <span className="tlui-button__label" draggable={false}>
{labelStr} {labelStr}
</span> </span>
)} )}
{kbd && <Kbd>{kbd}</Kbd>} {kbd && <TldrawUiKbd>{kbd}</TldrawUiKbd>}
</_ContextMenu.CheckboxItem> </_ContextMenu.CheckboxItem>
) )
} }

View file

@ -1,5 +1,5 @@
import { createContext, useContext } from 'react' import { createContext, useContext } from 'react'
import { TLUiEventSource } from '../../context/events' import { TLUiEventSource } from '../../../context/events'
/** @public */ /** @public */
export type TldrawUiMenuContextType = export type TldrawUiMenuContextType =

View file

@ -1,8 +1,8 @@
import { ContextMenuGroup } from '@radix-ui/react-context-menu' import { ContextMenuGroup } from '@radix-ui/react-context-menu'
import { unwrapLabel } from '../../context/actions' import { unwrapLabel } from '../../../context/actions'
import { TLUiTranslationKey } from '../../hooks/useTranslation/TLUiTranslationKey' import { TLUiTranslationKey } from '../../../hooks/useTranslation/TLUiTranslationKey'
import { useTranslation } from '../../hooks/useTranslation/useTranslation' import { useTranslation } from '../../../hooks/useTranslation/useTranslation'
import { DropdownMenuGroup } from '../primitives/DropdownMenu' import { TldrawUiDropdownMenuGroup } from '../TldrawUiDropdownMenu'
import { useTldrawUiMenuContext } from './TldrawUiMenuContext' import { useTldrawUiMenuContext } from './TldrawUiMenuContext'
/** @public */ /** @public */
@ -33,9 +33,12 @@ export function TldrawUiMenuGroup({ id, label, small = false, children }: TLUiMe
} }
case 'menu': { case 'menu': {
return ( return (
<DropdownMenuGroup data-testid={`${sourceId}-group.${id}`} size={small ? 'tiny' : 'medium'}> <TldrawUiDropdownMenuGroup
data-testid={`${sourceId}-group.${id}`}
size={small ? 'tiny' : 'medium'}
>
{children} {children}
</DropdownMenuGroup> </TldrawUiDropdownMenuGroup>
) )
} }
case 'context-menu': { case 'context-menu': {

View file

@ -1,16 +1,18 @@
import { ContextMenuItem } from '@radix-ui/react-context-menu' import { ContextMenuItem } from '@radix-ui/react-context-menu'
import { preventDefault } from '@tldraw/editor' import { preventDefault } from '@tldraw/editor'
import { useState } from 'react' import { useState } from 'react'
import { unwrapLabel } from '../../context/actions' import { unwrapLabel } from '../../../context/actions'
import { TLUiEventSource } from '../../context/events' import { TLUiEventSource } from '../../../context/events'
import { useReadonly } from '../../hooks/useReadonly' import { useReadonly } from '../../../hooks/useReadonly'
import { TLUiTranslationKey } from '../../hooks/useTranslation/TLUiTranslationKey' import { TLUiTranslationKey } from '../../../hooks/useTranslation/TLUiTranslationKey'
import { useTranslation } from '../../hooks/useTranslation/useTranslation' import { useTranslation } from '../../../hooks/useTranslation/useTranslation'
import { Spinner } from '../Spinner' import { kbdStr } from '../../../kbd-utils'
import { Button } from '../primitives/Button' import { Spinner } from '../../Spinner'
import { DropdownMenuItem } from '../primitives/DropdownMenu' import { TldrawUiButton } from '../Button/TldrawUiButton'
import { Kbd } from '../primitives/Kbd' import { TldrawUiButtonIcon } from '../Button/TldrawUiButtonIcon'
import { kbdStr } from '../primitives/shared' import { TldrawUiButtonLabel } from '../Button/TldrawUiButtonLabel'
import { TldrawUiDropdownMenuItem } from '../TldrawUiDropdownMenu'
import { TldrawUiKbd } from '../TldrawUiKbd'
import { useTldrawUiMenuContext } from './TldrawUiMenuContext' import { useTldrawUiMenuContext } from './TldrawUiMenuContext'
/** @public */ /** @public */
@ -90,11 +92,10 @@ export function TldrawUiMenuItem<
switch (menuType) { switch (menuType) {
case 'menu': { case 'menu': {
return ( return (
<DropdownMenuItem <TldrawUiDropdownMenuItem>
<TldrawUiButton
type="menu" type="menu"
data-testid={`${sourceId}.${id}`} data-testid={`${sourceId}.${id}`}
kbd={kbd}
label={labelStr}
disabled={disabled} disabled={disabled}
title={titleStr} title={titleStr}
onClick={(e) => { onClick={(e) => {
@ -107,7 +108,11 @@ export function TldrawUiMenuItem<
onSelect(sourceId) onSelect(sourceId)
} }
}} }}
/> >
<TldrawUiButtonLabel>{labelStr}</TldrawUiButtonLabel>
{kbd && <TldrawUiKbd>{kbd}</TldrawUiKbd>}
</TldrawUiButton>
</TldrawUiDropdownMenuItem>
) )
} }
case 'context-menu': { case 'context-menu': {
@ -133,37 +138,37 @@ export function TldrawUiMenuItem<
<span className="tlui-button__label" draggable={false}> <span className="tlui-button__label" draggable={false}>
{labelStr} {labelStr}
</span> </span>
{kbd && <Kbd>{kbd}</Kbd>} {kbd && <TldrawUiKbd>{kbd}</TldrawUiKbd>}
{spinner && <Spinner />} {spinner && <Spinner />}
</ContextMenuItem> </ContextMenuItem>
) )
} }
case 'panel': { case 'panel': {
return ( return (
<Button <TldrawUiButton
data-testid={`${sourceId}.${id}`} data-testid={`${sourceId}.${id}`}
icon={icon}
type="menu" type="menu"
label={labelStr}
title={titleStr} title={titleStr}
onClick={() => onSelect(sourceId)}
smallIcon
disabled={disabled} disabled={disabled}
/> onClick={() => onSelect(sourceId)}
>
<TldrawUiButtonLabel>{labelStr}</TldrawUiButtonLabel>
{icon && <TldrawUiButtonIcon icon={icon} />}
</TldrawUiButton>
) )
} }
case 'small-icons': case 'small-icons':
case 'icons': { case 'icons': {
return ( return (
<Button <TldrawUiButton
data-testid={`${sourceId}.${id}`} data-testid={`${sourceId}.${id}`}
icon={icon}
type="icon" type="icon"
title={titleStr} title={titleStr}
onClick={() => onSelect(sourceId)}
disabled={disabled} disabled={disabled}
smallIcon={menuType === 'small-icons'} onClick={() => onSelect(sourceId)}
/> >
<TldrawUiButtonIcon icon={icon!} small={menuType === 'small-icons'} />
</TldrawUiButton>
) )
} }
case 'keyboard-shortcuts': { case 'keyboard-shortcuts': {
@ -173,14 +178,17 @@ export function TldrawUiMenuItem<
<div className="tlui-shortcuts-dialog__key-pair" data-testid={`${sourceId}.${id}`}> <div className="tlui-shortcuts-dialog__key-pair" data-testid={`${sourceId}.${id}`}>
<div className="tlui-shortcuts-dialog__key-pair__key">{labelStr}</div> <div className="tlui-shortcuts-dialog__key-pair__key">{labelStr}</div>
<div className="tlui-shortcuts-dialog__key-pair__value"> <div className="tlui-shortcuts-dialog__key-pair__value">
<Kbd>{kbd!}</Kbd> <TldrawUiKbd>{kbd!}</TldrawUiKbd>
</div> </div>
</div> </div>
) )
} }
case 'helper-buttons': { case 'helper-buttons': {
return ( return (
<Button type="low" label={labelStr} iconLeft={icon} onClick={() => onSelect(sourceId)} /> <TldrawUiButton type="low" onClick={() => onSelect(sourceId)}>
<TldrawUiButtonIcon icon={icon!} />
<TldrawUiButtonLabel>{labelStr}</TldrawUiButtonLabel>
</TldrawUiButton>
) )
} }
default: { default: {

View file

@ -5,15 +5,17 @@ import {
ContextMenuSubTrigger, ContextMenuSubTrigger,
} from '@radix-ui/react-context-menu' } from '@radix-ui/react-context-menu'
import { useContainer } from '@tldraw/editor' import { useContainer } from '@tldraw/editor'
import { useMenuIsOpen } from '../../hooks/useMenuIsOpen' import { useMenuIsOpen } from '../../../hooks/useMenuIsOpen'
import { TLUiTranslationKey } from '../../hooks/useTranslation/TLUiTranslationKey' import { TLUiTranslationKey } from '../../../hooks/useTranslation/TLUiTranslationKey'
import { useTranslation } from '../../hooks/useTranslation/useTranslation' import { useTranslation } from '../../../hooks/useTranslation/useTranslation'
import { Button } from '../primitives/Button' import { TldrawUiButton } from '../Button/TldrawUiButton'
import { TldrawUiButtonIcon } from '../Button/TldrawUiButtonIcon'
import { TldrawUiButtonLabel } from '../Button/TldrawUiButtonLabel'
import { import {
DropdownMenuSub, TldrawUiDropdownMenuSub,
DropdownMenuSubContent, TldrawUiDropdownMenuSubContent,
DropdownMenuSubTrigger, TldrawUiDropdownMenuSubTrigger,
} from '../primitives/DropdownMenu' } from '../TldrawUiDropdownMenu'
import { useTldrawUiMenuContext } from './TldrawUiMenuContext' import { useTldrawUiMenuContext } from './TldrawUiMenuContext'
/** @public */ /** @public */
@ -46,34 +48,33 @@ export function TldrawUiMenuSubmenu<Translation extends string = string>({
switch (menuType) { switch (menuType) {
case 'menu': { case 'menu': {
return ( return (
<DropdownMenuSub id={`${sourceId}-sub.${id}`}> <TldrawUiDropdownMenuSub id={`${sourceId}-sub.${id}`}>
<DropdownMenuSubTrigger <TldrawUiDropdownMenuSubTrigger
id={`${sourceId}-sub.${id}`} id={`${sourceId}-sub.${id}`}
disabled={disabled} disabled={disabled}
label={labelStr!} label={labelStr!}
title={labelStr!} title={labelStr!}
/> />
<DropdownMenuSubContent id={`${sourceId}-sub-content.${id}`} data-size={size}> <TldrawUiDropdownMenuSubContent id={`${sourceId}-sub-content.${id}`} data-size={size}>
{children} {children}
</DropdownMenuSubContent> </TldrawUiDropdownMenuSubContent>
</DropdownMenuSub> </TldrawUiDropdownMenuSub>
) )
} }
case 'context-menu': { case 'context-menu': {
if (disabled) return null
return ( return (
<ContextMenuSubWithMenu id={`${sourceId}-sub.${id}`}> <ContextMenuSubWithMenu id={`${sourceId}-sub.${id}`}>
<ContextMenuSubTrigger <ContextMenuSubTrigger dir="ltr" disabled={disabled} asChild>
dir="ltr" <TldrawUiButton
disabled={disabled}
data-testid={`${sourceId}-sub-trigger.${id}`} data-testid={`${sourceId}-sub-trigger.${id}`}
asChild
>
<Button
type="menu" type="menu"
className="tlui-menu__submenu__trigger" className="tlui-menu__submenu__trigger"
label={labelStr} >
icon="chevron-right" <TldrawUiButtonLabel>{labelStr}</TldrawUiButtonLabel>
/> <TldrawUiButtonIcon icon="chevron-right" small />
</TldrawUiButton>
</ContextMenuSubTrigger> </ContextMenuSubTrigger>
<ContextMenuPortal container={container}> <ContextMenuPortal container={container}>
<ContextMenuSubContent <ContextMenuSubContent

View file

@ -76,6 +76,7 @@ export function TldrawUiContextProvider({
</AssetUrlsProvider> </AssetUrlsProvider>
) )
} }
function InternalProviders({ function InternalProviders({
overrides, overrides,
children, children,

View file

@ -19,6 +19,7 @@ import {
TLUiKeyboardShortcutsDialogProps, TLUiKeyboardShortcutsDialogProps,
} from '../components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialog' } from '../components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialog'
import { DefaultMainMenu, TLUiMainMenuProps } from '../components/MainMenu/DefaultMainMenu' import { DefaultMainMenu, TLUiMainMenuProps } from '../components/MainMenu/DefaultMainMenu'
import { DefaultMenuPanel } from '../components/MenuPanel'
import { DefaultMinimap } from '../components/Minimap/DefaultMinimap' import { DefaultMinimap } from '../components/Minimap/DefaultMinimap'
import { DefaultNavigationPanel } from '../components/NavigationPanel/DefaultNavigationPanel' import { DefaultNavigationPanel } from '../components/NavigationPanel/DefaultNavigationPanel'
import { DefaultPageMenu } from '../components/PageMenu/DefaultPageMenu' import { DefaultPageMenu } from '../components/PageMenu/DefaultPageMenu'
@ -45,6 +46,9 @@ export interface BaseTLUiComponents {
QuickActions: ComponentType<TLUiQuickActionsProps> QuickActions: ComponentType<TLUiQuickActionsProps>
HelperButtons: ComponentType<TLUiHelperButtonsProps> HelperButtons: ComponentType<TLUiHelperButtonsProps>
DebugMenu: ComponentType DebugMenu: ComponentType
MenuPanel: ComponentType
TopPanel: ComponentType
SharePanel: ComponentType
} }
/** @public */ /** @public */
@ -54,7 +58,8 @@ export type TLUiComponents = Partial<{
const TldrawUiComponentsContext = createContext({} as TLUiComponents) const TldrawUiComponentsContext = createContext({} as TLUiComponents)
type ComponentsContextProviderProps = { /** @public */
export type TLUiComponentsProviderProps = {
overrides?: TLUiComponents overrides?: TLUiComponents
children: any children: any
} }
@ -63,7 +68,7 @@ type ComponentsContextProviderProps = {
export function TldrawUiComponentsProvider({ export function TldrawUiComponentsProvider({
overrides = {}, overrides = {},
children, children,
}: ComponentsContextProviderProps) { }: TLUiComponentsProviderProps) {
const _overrides = useShallowObjectIdentity(overrides) const _overrides = useShallowObjectIdentity(overrides)
return ( return (
@ -84,6 +89,7 @@ export function TldrawUiComponentsProvider({
QuickActions: DefaultQuickActions, QuickActions: DefaultQuickActions,
HelperButtons: DefaultHelperButtons, HelperButtons: DefaultHelperButtons,
DebugMenu: DefaultDebugMenu, DebugMenu: DefaultDebugMenu,
MenuPanel: DefaultMenuPanel,
..._overrides, ..._overrides,
}), }),
[_overrides] [_overrides]

View file

@ -1,11 +1,3 @@
/** @internal */
export function toStartCase(str: string) {
return str
.split(' ')
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
.join(' ')
}
const isDarwin = const isDarwin =
typeof window === 'undefined' typeof window === 'undefined'
? false ? false
@ -13,7 +5,7 @@ const isDarwin =
const cmdKey = isDarwin ? '⌘' : 'Ctrl' const cmdKey = isDarwin ? '⌘' : 'Ctrl'
const altKey = isDarwin ? '⌥' : 'Alt' const altKey = isDarwin ? '⌥' : 'Alt'
/** @internal */ /** @public */
export function kbd(str: string) { export function kbd(str: string) {
return str return str
.split(',')[0] .split(',')[0]
@ -24,17 +16,7 @@ export function kbd(str: string) {
}) })
} }
/** @internal */ /** @public */
export function kbdStr(str: string) { export function kbdStr(str: string) {
return ( return '— ' + kbd(str).join('')
'— ' +
str
.split(',')[0]
.split('')
.map((sub) => {
const subStr = sub.replace(/\$/g, cmdKey).replace(/\?/g, altKey).replace(/!/g, '⇧')
return subStr[0].toUpperCase() + subStr.slice(1)
})
.join('')
)
} }