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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,7 @@
import * as Popover from '@radix-ui/react-popover'
import {
Button,
TldrawUiButton,
TldrawUiButtonIcon,
USER_COLORS,
track,
useContainer,
@ -88,13 +89,14 @@ export const UserPresenceColorPicker = track(function UserPresenceColorPicker()
return (
<Popover.Root onOpenChange={handleOpenChange} open={isOpen}>
<Popover.Trigger dir="ltr" asChild>
<Button
<TldrawUiButton
type="icon"
className="tlui-people-menu__user__color"
icon="color"
style={{ color: editor.user.getColor() }}
title={msg('people-menu.change-color')}
/>
>
<TldrawUiButtonIcon icon="color" />
</TldrawUiButton>
</Popover.Trigger>
<Popover.Portal container={container}>
<Popover.Content
@ -106,11 +108,11 @@ export const UserPresenceColorPicker = track(function UserPresenceColorPicker()
>
<div className={'tlui-buttons__grid'}>
{USER_COLORS.map((item: string) => (
<Button
<TldrawUiButton
type="icon"
key={item}
data-id={item}
data-wd={item}
data-testid={item}
aria-label={item}
data-state={value === item ? 'hinted' : undefined}
title={item}
@ -120,8 +122,9 @@ export const UserPresenceColorPicker = track(function UserPresenceColorPicker()
onPointerDown={handleButtonPointerDown}
onPointerUp={handleButtonPointerUp}
onClick={handleButtonClick}
icon={'color'}
/>
>
<TldrawUiButtonIcon icon="color" />
</TldrawUiButton>
))}
</div>
</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 { UI_OVERRIDE_TODO_EVENT } from '../../utils/useHandleUiEvent'
import { UserPresenceColorPicker } from './UserPresenceColorPicker'
@ -36,7 +44,7 @@ export function UserPresenceEditor() {
<div className="tlui-people-menu__user">
<UserPresenceColorPicker />
{isEditingName ? (
<Input
<TldrawUiInput
className="tlui-people-menu__user__input"
defaultValue={userName}
onValueChange={handleValueChange}
@ -62,14 +70,15 @@ export function UserPresenceEditor() {
) : null}
</>
)}
<Button
<TldrawUiButton
type="icon"
className="tlui-people-menu__user__edit"
data-wd="people-menu.change-name"
data-testid="people-menu.change-name"
title={msg('people-menu.change-name')}
icon={isEditingName ? 'check' : 'edit'}
onClick={toggleEditingName}
/>
>
<TldrawUiButtonIcon icon={isEditingName ? 'check' : 'edit'} />
</TldrawUiButton>
</div>
)
}

View file

@ -54,6 +54,13 @@ const components: TLComponents = {
</DefaultKeyboardShortcutsDialog>
)
},
SharePanel: () => {
return (
<div className="tlui-share-zone" draggable={false}>
<ExportMenu />
</div>
)
},
}
type SnapshotEditorProps = {
@ -79,11 +86,6 @@ export function SnapshotsEditor(props: SnapshotEditorProps) {
editor.updateInstanceState({ isReadonly: true })
}}
components={components}
shareZone={
<div className="tlui-share-zone" draggable={false}>
<ExportMenu />
</div>
}
renderDebugMenuItems={() => <DebugMenuItems />}
autoFocus
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({
onClose,
@ -107,16 +113,16 @@ export function MigrationAnnouncement({
marginTop: 8,
}}
>
<Button
<TldrawUiButton
type="normal"
style={{ fontSize: 14, marginRight: 'auto' }}
onClick={downloadFile}
>
Download original
</Button>
<Button style={{ fontSize: 14 }} type="primary" onClick={onClose}>
Continue
</Button>
<TldrawUiButtonLabel>Download original</TldrawUiButtonLabel>
</TldrawUiButton>
<TldrawUiButton style={{ fontSize: 14 }} type="primary" onClick={onClose}>
<TldrawUiButtonLabel>Continue</TldrawUiButtonLabel>
</TldrawUiButton>
</div>
</div>
</div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,6 +17,9 @@ const components: Required<TLUiComponents> = {
QuickActions: null,
HelperButtons: null,
DebugMenu: null,
SharePanel: null,
MenuPanel: null,
TopPanel: null,
}
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'
// There's a guide at the bottom of this file!
const components: TLComponents = {
SharePanel: CustomShareZone,
TopPanel: CustomTopZone,
}
// [1]
export default function Example() {
return (
<div className="tldraw__editor">
<Tldraw topZone={<CustomTopZone />} shareZone={<CustomShareZone />} />
<Tldraw components={components} />
</div>
)
}
@ -46,8 +51,8 @@ function CustomShareZone() {
}
/*
This example shows how to pass in a custom component to the share zone and top zone.
The share zone is in the top right corner above the style menu, the top zone is in
This example shows how to pass in a custom component to the share panel and top panel.
The share panel is in the top right corner above the style menu, the top panel is in
the top center.
[1]