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' }}
>
<TldrawUiButtonCheck checked={dontShowAgain} />
<TldrawUiButtonLabel>
{msg('file-system.confirm-clear.dont-show-again')}
</Button>
<Button type="normal" onClick={onCancel}>
{msg('file-system.confirm-clear.cancel')}
</Button>
<Button
</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' }}
>
<TldrawUiButtonCheck checked={dontShowAgain} />
<TldrawUiButtonLabel>
{msg('file-system.confirm-open.dont-show-again')}
</Button>
<Button type="normal" onClick={onCancel}>
{msg('file-system.confirm-open.cancel')}
</Button>
<Button
</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]

View file

@ -28,6 +28,7 @@ import { JSX as JSX_2 } from 'react/jsx-runtime';
import { LANGUAGES } from '@tldraw/editor';
import { Mat } from '@tldraw/editor';
import { MatModel } from '@tldraw/editor';
import { MemoExoticComponent } from 'react';
import { MigrationFailureReason } from '@tldraw/editor';
import { Migrations } from '@tldraw/editor';
import { NamedExoticComponent } from 'react';
@ -49,6 +50,7 @@ import { ShapeUtil } from '@tldraw/editor';
import { SharedStyle } from '@tldraw/editor';
import { StateNode } from '@tldraw/editor';
import { StoreSnapshot } from '@tldraw/editor';
import { StyleProp } from '@tldraw/editor';
import { SvgExportContext } from '@tldraw/editor';
import { T } from '@tldraw/editor';
import { TLAnyShapeUtilConstructor } from '@tldraw/editor';
@ -265,9 +267,6 @@ export function BreakPointProvider({ forceMobile, children, }: {
// @internal (undocumented)
export function buildFromV1Document(editor: Editor, document: LegacyTldrawDocument): void;
// @public (undocumented)
export const Button: React_3.ForwardRefExoticComponent<TLUiButtonProps & React_3.RefAttributes<HTMLButtonElement>>;
// @public
export function containBoxSize(originalSize: BoxWidthHeight, containBoxSize: BoxWidthHeight): BoxWidthHeight;
@ -360,21 +359,6 @@ export const DefaultZoomMenu: NamedExoticComponent<TLUiZoomMenuProps>;
// @public (undocumented)
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
export function downsizeImage(blob: Blob, width: number, height: number, opts?: {
type?: string | undefined;
@ -439,36 +423,6 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
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)
export class EmbedShapeUtil extends BaseBoxShapeUtil<TLEmbedShape> {
// (undocumented)
@ -815,9 +769,6 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
static type: "highlight";
}
// @public (undocumented)
export const Icon: NamedExoticComponent<TLUiIconProps>;
// @public (undocumented)
export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
// (undocumented)
@ -856,9 +807,6 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
static type: "image";
}
// @public (undocumented)
export const Input: React_3.ForwardRefExoticComponent<TLUiInputProps & React_3.RefAttributes<HTMLInputElement>>;
// @public (undocumented)
export function isGifAnimated(file: Blob): Promise<boolean>;
@ -1059,15 +1007,6 @@ export function parseTldrawJsonFile({ json, schema, }: {
json: string;
}): 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
export function removeFrame(editor: Editor, ids: TLShapeId[]): void;
@ -1292,13 +1231,25 @@ export interface TldrawUiBaseProps {
components?: TLUiComponents;
hideUi?: boolean;
renderDebugMenuItems?: () => React_2.ReactNode;
shareZone?: ReactNode;
// @internal
topZone?: ReactNode;
}
// @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)
export function TldrawUiContextProvider({ overrides, components, assetUrls, onUiEvent, forceMobile, children, }: TldrawUiContextProviderProps): JSX_2.Element;
@ -1313,6 +1264,57 @@ export interface TldrawUiContextProviderProps {
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)
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)
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
export type TldrawUiProps = TldrawUiBaseProps & TldrawUiContextProviderProps;
// @internal (undocumented)
export const TldrawUiSlider: NamedExoticComponent<TLUiSliderProps>;
// @public (undocumented)
export interface TLUiActionItem<TransationKey extends string = string, IconType extends string = string> {
// (undocumented)
@ -1364,29 +1378,44 @@ export type TLUiActionsMenuProps = {
// @public (undocumented)
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)
export interface TLUiButtonProps extends React_3.HTMLAttributes<HTMLButtonElement> {
// (undocumented)
disabled?: boolean;
// (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';
}
@ -1395,6 +1424,12 @@ export type TLUiComponents = Partial<{
[K in keyof BaseTLUiComponents]: BaseTLUiComponents[K] | null;
}>;
// @public (undocumented)
export type TLUiComponentsProviderProps = {
overrides?: TLUiComponents;
children: any;
};
// @public (undocumented)
export interface TLUiContextMenuProps {
// (undocumented)
@ -1491,23 +1526,11 @@ export type TLUiDropdownMenuGroupProps = {
};
// @public (undocumented)
export interface TLUiDropdownMenuItemProps extends TLUiButtonProps {
// (undocumented)
noClose?: boolean;
}
// @public (undocumented)
export interface TLUiDropdownMenuRadioItemProps {
// (undocumented)
checked?: boolean;
export interface TLUiDropdownMenuItemProps {
// (undocumented)
children: any;
// (undocumented)
disabled?: boolean;
// (undocumented)
onSelect?: (e: Event) => void;
// (undocumented)
title: string;
noClose?: boolean;
}
// @public (undocumented)
@ -1533,7 +1556,7 @@ export type TLUiDropdownMenuSubTriggerProps = {
};
// @public (undocumented)
export interface TLUiDropdownMenuTriggerProps extends TLUiButtonProps {
export interface TLUiDropdownMenuTriggerProps {
// (undocumented)
children?: any;
}
@ -1772,6 +1795,12 @@ export interface TLUiInputProps {
value?: string;
}
// @public (undocumented)
export interface TLUiKbdProps {
// (undocumented)
children: string;
}
// @public (undocumented)
export type TLUiKeyboardShortcutsDialogProps = TLUiDialogProps & {
children?: any;
@ -1867,7 +1896,7 @@ export type TLUiPopoverProps = {
};
// @public (undocumented)
export interface TLUiPopoverTriggerProps extends TLUiButtonProps {
export interface TLUiPopoverTriggerProps {
// (undocumented)
children?: React_2.ReactNode;
}
@ -1877,6 +1906,22 @@ export type TLUiQuickActionsProps = {
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)
export type TLUiStylePanelContentProps = {
relevantStyles: ReturnType<typeof useRelevantStyles>;
@ -2080,6 +2125,9 @@ export function useTldrawUiComponents(): Partial<{
QuickActions: ComponentType<TLUiQuickActionsProps> | null;
HelperButtons: ComponentType<TLUiHelperButtonsProps> | null;
DebugMenu: ComponentType | null;
MenuPanel: ComponentType | null;
TopPanel: ComponentType | null;
SharePanel: ComponentType | null;
}>;
// @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 { OfflineIndicator } from './lib/ui/components/OfflineIndicator/OfflineIndicator'
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 {
TldrawUiContextProvider,
type TldrawUiContextProviderProps,
@ -99,7 +96,7 @@ export {
export { type TLUiTranslationKey } from './lib/ui/hooks/useTranslation/TLUiTranslationKey'
export { type TLUiTranslation } from './lib/ui/hooks/useTranslation/translations'
export {
useTranslation as useTranslation,
useTranslation,
type TLUiTranslationContextType,
} from './lib/ui/hooks/useTranslation/useTranslation'
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
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 {
TldrawUiComponentsProvider,
useTldrawUiComponents,
type TLUiComponents,
type TLUiComponentsProviderProps,
} from './lib/ui/context/components'
// Menus / UI elements that can be customized
export { DefaultPageMenu } from './lib/ui/components/PageMenu/DefaultPageMenu'
export {
@ -236,45 +211,108 @@ export { DefaultToolbar } from './lib/ui/components/Toolbar/DefaultToolbar'
export { type TLComponents } from './lib/Tldraw'
/* ------------------- Primitives ------------------- */
// Button
export {
DialogBody,
DialogCloseButton,
DialogFooter,
DialogHeader,
DialogTitle,
TldrawUiButton,
type TLUiButtonProps,
} from './lib/ui/components/primitives/Button/TldrawUiButton'
export {
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 TLUiDialogFooterProps,
type TLUiDialogHeaderProps,
type TLUiDialogTitleProps,
} from './lib/ui/components/primitives/Dialog'
} from './lib/ui/components/primitives/TldrawUiDialog'
// Dropdown Menu
export {
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuIndicator,
DropdownMenuItem,
DropdownMenuRadioItem,
DropdownMenuRoot,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
TldrawUiDropdownMenuCheckboxItem,
TldrawUiDropdownMenuContent,
TldrawUiDropdownMenuGroup,
TldrawUiDropdownMenuIndicator,
TldrawUiDropdownMenuItem,
TldrawUiDropdownMenuRoot,
TldrawUiDropdownMenuSub,
TldrawUiDropdownMenuSubTrigger,
TldrawUiDropdownMenuTrigger,
type TLUiDropdownMenuCheckboxItemProps,
type TLUiDropdownMenuContentProps,
type TLUiDropdownMenuGroupProps,
type TLUiDropdownMenuItemProps,
type TLUiDropdownMenuRadioItemProps,
type TLUiDropdownMenuRootProps,
type TLUiDropdownMenuSubProps,
type TLUiDropdownMenuSubTriggerProps,
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 {
Popover,
PopoverContent,
PopoverTrigger,
TldrawUiPopover,
TldrawUiPopoverContent,
TldrawUiPopoverTrigger,
type TLUiPopoverContentProps,
type TLUiPopoverProps,
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
}[]
// todo: default styles prop?
export const STYLES = {
color: [
{ value: 'black', icon: 'color' },

View file

@ -75,6 +75,10 @@
opacity: 1;
}
.tlui-button__icon + .tlui-button__label {
margin-left: var(--space-2);
}
@media (hover: hover) {
.tlui-button::after {
background-color: var(--color-muted-2);
@ -127,46 +131,6 @@
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 */
.tlui-button__menu {
@ -1225,11 +1189,6 @@
width: 128px;
}
.tlui-page-menu__trigger > span {
flex-grow: 2;
margin-right: var(--space-4);
}
.tlui-page-menu__header {
display: flex;
flex-direction: row;
@ -1373,17 +1332,29 @@
.tlui-page_menu__item__submenu[data-isediting='true'] {
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) {
.tlui-page_menu__item__submenu {
opacity: 0;
display: block;
}
.tlui-page_menu__item__submenu:hover,
.tlui-page-menu__item:focus-within > .tlui-page_menu__item__submenu,
.tlui-page_menu__item__sortable:focus-within > .tlui-page_menu__item__submenu {
.tlui-page_menu__item__submenu[data-isediting='true'] > .tlui-button {
opacity: 0;
}
.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;
}
}

View file

@ -6,9 +6,9 @@ import { TLUiAssetUrlOverrides } from './assetUrls'
import { DebugPanel } from './components/DebugPanel'
import { Dialogs } from './components/Dialogs'
import { FollowingIndicator } from './components/FollowingIndicator'
import { MenuZone } from './components/MenuZone'
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 {
TldrawUiContextProvider,
@ -20,6 +20,7 @@ import { TLUiComponents, useTldrawUiComponents } from './context/components'
import { useNativeClipboardEvents } from './hooks/useClipboardEvents'
import { useEditorEvents } from './hooks/useEditorEvents'
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'
import { useReadonly } from './hooks/useReadonly'
import { useRelevantStyles } from './hooks/useRevelantStyles'
import { useTranslation } from './hooks/useTranslation/useTranslation'
@ -51,17 +52,6 @@ export interface TldrawUiBaseProps {
*/
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)
*/
@ -75,8 +65,6 @@ export interface TldrawUiBaseProps {
* @public
*/
export const TldrawUi = React.memo(function TldrawUi({
shareZone,
topZone,
renderDebugMenuItems,
children,
hideUi,
@ -85,12 +73,7 @@ export const TldrawUi = React.memo(function TldrawUi({
}: TldrawUiProps) {
return (
<TldrawUiContextProvider {...rest} components={components}>
<TldrawUiInner
hideUi={hideUi}
shareZone={shareZone}
topZone={topZone}
renderDebugMenuItems={renderDebugMenuItems}
>
<TldrawUiInner hideUi={hideUi} renderDebugMenuItems={renderDebugMenuItems}>
{children}
</TldrawUiInner>
</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 msg = useTranslation()
const breakpoint = useBreakpoint()
const isReadonlyMode = useValue('isReadonlyMode', () => editor.getInstanceState().isReadonly, [
editor,
])
const isReadonlyMode = useReadonly()
const isFocusMode = useValue('focus', () => editor.getInstanceState().isFocusMode, [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()
useNativeClipboardEvents()
@ -149,24 +139,25 @@ const TldrawUiContent = React.memo(function TldrawUI({ shareZone, topZone }: Tld
>
{isFocusMode ? (
<div className="tlui-layout__top">
<Button
<TldrawUiButton
type="icon"
className="tlui-focus-button"
title={`${msg('focus-mode.toggle-focus-mode')}`}
icon="dot"
title={msg('focus-mode.toggle-focus-mode')}
onClick={() => toggleFocus.onSelect('menu')}
/>
>
<TldrawUiButtonIcon icon="dot" />
</TldrawUiButton>
</div>
) : (
<>
<div className="tlui-layout__top">
<div className="tlui-layout__top__left">
<MenuZone />
{MenuPanel && <MenuPanel />}
{HelperButtons && <HelperButtons />}
</div>
<div className="tlui-layout__top__center">{topZone}</div>
<div className="tlui-layout__top__center">{TopPanel && <TopPanel />}</div>
<div className="tlui-layout__top__right">
{shareZone}
{SharePanel && <SharePanel />}
{StylePanel && breakpoint >= PORTRAIT_BREAKPOINT.TABLET_SM && !isReadonlyMode && (
<_StylePanel />
)}

View file

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

View file

@ -9,7 +9,7 @@ import {
useThreeStackableItems,
useUnlockedSelectedShapesCount,
} from '../../hooks/menu-hooks'
import { TldrawUiMenuItem } from '../menus/TldrawUiMenuItem'
import { TldrawUiMenuItem } from '../primitives/menus/TldrawUiMenuItem'
/** @public */
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 { memo, useCallback } from 'react'
import { useMenuIsOpen } from '../../hooks/useMenuIsOpen'
import { TldrawUiMenuContextProvider } from '../menus/TldrawUiMenuContext'
import { TldrawUiMenuContextProvider } from '../primitives/menus/TldrawUiMenuContext'
import { DefaultContextMenuContent } from './DefaultContextMenuContent'
/** @public */

View file

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

View file

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

View file

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

View file

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

View file

@ -5,10 +5,16 @@ import { TLEmbedResult, getEmbedInfo } from '../../utils/embeds/embeds'
import { useAssetUrls } from '../context/asset-urls'
import { TLUiDialogProps } from '../context/dialogs'
import { untranslated, useTranslation } from '../hooks/useTranslation/useTranslation'
import { Button } from './primitives/Button'
import { DialogBody, DialogCloseButton, DialogFooter, DialogHeader } from './primitives/Dialog'
import { Icon } from './primitives/Icon'
import { Input } from './primitives/Input'
import { TldrawUiButton } from './primitives/Button/TldrawUiButton'
import { TldrawUiButtonLabel } from './primitives/Button/TldrawUiButtonLabel'
import {
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) {
const editor = useEditor()
@ -30,18 +36,18 @@ export const EmbedDialog = track(function EmbedDialog({ onClose }: TLUiDialogPro
return (
<>
<DialogHeader>
<TldrawUiDialogHeader>
<DialogTitle>
{embedDefinition
? `${msg('embed-title')}${embedDefinition.title}`
: msg('embed-title')}
</DialogTitle>
<DialogCloseButton />
</DialogHeader>
<TldrawUiDialogCloseButton />
</TldrawUiDialogHeader>
{embedDefinition ? (
<>
<DialogBody className="tlui-embed-dialog__enter">
<Input
<TldrawUiDialogBody className="tlui-embed-dialog__enter">
<TldrawUiInput
className="tlui-embed-dialog__input"
label="embed-url"
placeholder="http://example.com"
@ -77,7 +83,7 @@ export const EmbedDialog = track(function EmbedDialog({ onClose }: TLUiDialogPro
className="tlui-embed-dialog__instruction__link"
>
Learn more.
<Icon icon="external-link" small />
<TldrawUiIcon icon="external-link" small />
</a>
)}
</div>
@ -86,23 +92,25 @@ export const EmbedDialog = track(function EmbedDialog({ onClose }: TLUiDialogPro
{showError ? msg('embed-invalid-url') : '\xa0'}
</div>
)}
</DialogBody>
<DialogFooter className="tlui-dialog__footer__actions">
<Button
</TldrawUiDialogBody>
<TldrawUiDialogFooter className="tlui-dialog__footer__actions">
<TldrawUiButton
type="normal"
onClick={() => {
setEmbedDefinition(null)
setEmbedInfoForUrl(null)
setUrl('')
}}
label="embed-back"
/>
>
<TldrawUiButtonLabel>{msg('embed-back')}</TldrawUiButtonLabel>
</TldrawUiButton>
<div className="tlui-embed__spacer" />
<Button type="normal" label="embed-cancel" onClick={onClose} />
<Button
<TldrawUiButton type="normal" onClick={onClose}>
<TldrawUiButtonLabel>{msg('embed-cancel')}</TldrawUiButtonLabel>
</TldrawUiButton>
<TldrawUiButton
type="primary"
disabled={!embedInfoForUrl}
label="embed-create"
onClick={() => {
if (!embedInfoForUrl) return
@ -115,28 +123,26 @@ export const EmbedDialog = track(function EmbedDialog({ onClose }: TLUiDialogPro
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) => {
return (
<Button
type="menu"
key={def.type}
onClick={() => setEmbedDefinition(def)}
label={untranslated(def.title)}
>
<TldrawUiButton type="menu" key={def.type} onClick={() => setEmbedDefinition(def)}>
<TldrawUiButtonLabel>{untranslated(def.title)}</TldrawUiButtonLabel>
<div
className="tlui-embed-dialog__item__image"
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 { useBreakpoint } from '../../context/breakpoints'
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
import { TldrawUiMenuContextProvider } from '../menus/TldrawUiMenuContext'
import { TldrawUiButton } from '../primitives/Button/TldrawUiButton'
import { TldrawUiButtonIcon } from '../primitives/Button/TldrawUiButtonIcon'
import {
DropdownMenuContent,
DropdownMenuRoot,
DropdownMenuTrigger,
} from '../primitives/DropdownMenu'
TldrawUiDropdownMenuContent,
TldrawUiDropdownMenuRoot,
TldrawUiDropdownMenuTrigger,
} from '../primitives/TldrawUiDropdownMenu'
import { TldrawUiMenuContextProvider } from '../primitives/menus/TldrawUiMenuContext'
import { DefaultHelpMenuContent } from './DefaultHelpMenuContent'
/** @public */
@ -29,19 +31,18 @@ export const DefaultHelpMenu = memo(function DefaultHelpMenu({ children }: TLUiH
return (
<div className="tlui-help-menu">
<DropdownMenuRoot id="help menu">
<DropdownMenuTrigger
type="help"
smallIcon
title={msg('help-menu.title')}
icon="question-mark"
/>
<DropdownMenuContent side="top" align="end" alignOffset={0} sideOffset={8}>
<TldrawUiDropdownMenuRoot id="help menu">
<TldrawUiDropdownMenuTrigger>
<TldrawUiButton type="help" title={msg('help-menu.title')}>
<TldrawUiButtonIcon icon="question-mark" small />
</TldrawUiButton>
</TldrawUiDropdownMenuTrigger>
<TldrawUiDropdownMenuContent side="top" align="end" alignOffset={0} sideOffset={8}>
<TldrawUiMenuContextProvider type="menu" sourceId="help-menu">
{content}
</TldrawUiMenuContextProvider>
</DropdownMenuContent>
</DropdownMenuRoot>
</TldrawUiDropdownMenuContent>
</TldrawUiDropdownMenuRoot>
</div>
)
})

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -29,9 +29,9 @@ import {
ZoomToFitMenuItem,
ZoomToSelectionMenuItem,
} from '../menu-items'
import { TldrawUiMenuGroup } from '../menus/TldrawUiMenuGroup'
import { TldrawUiMenuItem } from '../menus/TldrawUiMenuItem'
import { TldrawUiMenuSubmenu } from '../menus/TldrawUiMenuSubmenu'
import { TldrawUiMenuGroup } from '../primitives/menus/TldrawUiMenuGroup'
import { TldrawUiMenuItem } from '../primitives/menus/TldrawUiMenuItem'
import { TldrawUiMenuSubmenu } from '../primitives/menus/TldrawUiMenuSubmenu'
/** @public */
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 { useRelevantStyles } from '../hooks/useRevelantStyles'
import { useTranslation } from '../hooks/useTranslation/useTranslation'
import { Icon } from './primitives/Icon'
import { Popover, PopoverContent, PopoverTrigger } from './primitives/Popover'
import { TldrawUiButton } from './primitives/Button/TldrawUiButton'
import { TldrawUiButtonIcon } from './primitives/Button/TldrawUiButtonIcon'
import {
TldrawUiPopover,
TldrawUiPopoverContent,
TldrawUiPopoverTrigger,
} from './primitives/TldrawUiPopover'
export function MobileStylePanel() {
const editor = useEditor()
@ -42,22 +47,24 @@ export function MobileStylePanel() {
if (!StylePanel) return null
return (
<Popover id="style menu" onOpenChange={handleStylesOpenChange}>
<PopoverTrigger
disabled={disableStylePanel}
<TldrawUiPopover id="mobile style menu" onOpenChange={handleStylesOpenChange}>
<TldrawUiPopoverTrigger>
<TldrawUiButton
type="tool"
data-testid="mobile.styles"
style={{
color: disableStylePanel ? 'var(--color-muted-1)' : currentColor,
}}
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'} />
</PopoverTrigger>
<PopoverContent side="top" align="end">
<TldrawUiButtonIcon
icon={disableStylePanel ? 'blob' : color?.type === 'mixed' ? 'mixed' : 'blob'}
/>
</TldrawUiButton>
</TldrawUiPopoverTrigger>
<TldrawUiPopoverContent side="top" align="end">
<_StylePanel />
</PopoverContent>
</Popover>
</TldrawUiPopoverContent>
</TldrawUiPopover>
)
}
@ -66,5 +73,5 @@ function _StylePanel() {
const relevantStyles = useRelevantStyles()
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 { useLocalStorageState } from '../../hooks/useLocalStorageState'
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
import { Button } from '../primitives/Button'
import { kbdStr } from '../primitives/shared'
import { kbdStr } from '../../kbd-utils'
import { TldrawUiButton } from '../primitives/Button/TldrawUiButton'
import { TldrawUiButtonIcon } from '../primitives/Button/TldrawUiButtonIcon'
/** @public */
export const DefaultNavigationPanel = memo(function DefaultNavigationPanel() {
@ -35,42 +36,46 @@ export const DefaultNavigationPanel = memo(function DefaultNavigationPanel() {
<>
{ZoomMenu && <ZoomMenu />}
{Minimap && (
<Button
<TldrawUiButton
type="icon"
icon={collapsed ? 'chevrons-ne' : 'chevrons-sw'}
data-testid="minimap.toggle"
title={msg('navigation-zone.toggle-minimap')}
className="tlui-navigation-panel__toggle"
onClick={toggleMinimap}
/>
>
<TldrawUiButtonIcon icon={collapsed ? 'chevrons-ne' : 'chevrons-sw'} />
</TldrawUiButton>
)}
</>
) : (
<>
<Button
<TldrawUiButton
type="icon"
icon="minus"
data-testid="minimap.zoom-out"
title={`${msg(unwrapLabel(actions['zoom-out'].label))} ${kbdStr(actions['zoom-out'].kbd!)}`}
onClick={() => actions['zoom-out'].onSelect('navigation-zone')}
/>
>
<TldrawUiButtonIcon icon="minus" />
</TldrawUiButton>
{ZoomMenu && <ZoomMenu />}
<Button
<TldrawUiButton
type="icon"
icon="plus"
data-testid="minimap.zoom-in"
title={`${msg(unwrapLabel(actions['zoom-in'].label))} ${kbdStr(actions['zoom-in'].kbd!)}`}
onClick={() => actions['zoom-in'].onSelect('navigation-zone')}
/>
>
<TldrawUiButtonIcon icon="plus" />
</TldrawUiButton>
{Minimap && (
<Button
<TldrawUiButton
type="icon"
icon={collapsed ? 'chevrons-ne' : 'chevrons-sw'}
data-testid="minimap.toggle"
title={msg('navigation-zone.toggle-minimap')}
className="tlui-navigation-panel__toggle"
onClick={toggleMinimap}
/>
>
<TldrawUiButtonIcon icon={collapsed ? 'chevrons-ne' : 'chevrons-sw'} />
</TldrawUiButton>
)}
</>
)}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,7 +3,8 @@ import classNames from 'classnames'
import { PORTRAIT_BREAKPOINT } from '../../constants'
import { useBreakpoint } from '../../context/breakpoints'
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 {
activeToolId?: string
@ -32,15 +33,15 @@ export function ToggleToolLockedButton({ activeToolId }: ToggleToolLockedButtonP
if (!activeToolId || NOT_LOCKABLE_TOOLS.includes(activeToolId)) return null
return (
<Button
<TldrawUiButton
type="normal"
title={msg('action.toggle-tool-lock')}
className={classNames('tlui-toolbar__lock-button', {
'tlui-toolbar__lock-button__mobile': breakpoint < PORTRAIT_BREAKPOINT.TABLET_SM,
})}
icon={isToolLocked ? 'lock' : 'unlock'}
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 { useMenuIsOpen } from '../../hooks/useMenuIsOpen'
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
import { TldrawUiMenuContextProvider } from '../menus/TldrawUiMenuContext'
import { Button } from '../primitives/Button'
import { TldrawUiButton } from '../primitives/Button/TldrawUiButton'
import { TldrawUiMenuContextProvider } from '../primitives/menus/TldrawUiMenuContext'
import { DefaultZoomMenuContent } from './DefaultZoomMenuContent'
/** @public */
@ -59,7 +59,7 @@ const ZoomTriggerButton = forwardRef<HTMLButtonElement, any>(
}, [editor])
return (
<Button
<TldrawUiButton
ref={ref}
{...props}
type="icon"
@ -76,7 +76,7 @@ const ZoomTriggerButton = forwardRef<HTMLButtonElement, any>(
{breakpoint < PORTRAIT_BREAKPOINT.MOBILE ? null : (
<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 { ZoomTo100MenuItem, ZoomToFitMenuItem, ZoomToSelectionMenuItem } from '../menu-items'
import { TldrawUiMenuItem } from '../menus/TldrawUiMenuItem'
import { TldrawUiMenuItem } from '../primitives/menus/TldrawUiMenuItem'
/** @public */
export function DefaultZoomMenuContent() {

View file

@ -21,10 +21,10 @@ import {
useThreeStackableItems,
useUnlockedSelectedShapesCount,
} from '../hooks/menu-hooks'
import { TldrawUiMenuCheckboxItem } from './menus/TldrawUiMenuCheckboxItem'
import { TldrawUiMenuGroup } from './menus/TldrawUiMenuGroup'
import { TldrawUiMenuItem } from './menus/TldrawUiMenuItem'
import { TldrawUiMenuSubmenu } from './menus/TldrawUiMenuSubmenu'
import { TldrawUiMenuCheckboxItem } from './primitives/menus/TldrawUiMenuCheckboxItem'
import { TldrawUiMenuGroup } from './primitives/menus/TldrawUiMenuGroup'
import { TldrawUiMenuItem } from './primitives/menus/TldrawUiMenuItem'
import { TldrawUiMenuSubmenu } from './primitives/menus/TldrawUiMenuSubmenu'
/* -------------------- 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,
} from '@tldraw/editor'
import classNames from 'classnames'
import * as React from 'react'
import { useRef } from 'react'
import { memo, useMemo, useRef } from 'react'
import { StyleValuesForUi } from '../../../styles'
import { TLUiTranslationKey } from '../../hooks/useTranslation/TLUiTranslationKey'
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
import { TLUiIconType } from '../../icon-types'
import { StyleValuesForUi } from '../StylePanel/styles'
import { Button } from './Button'
import { TldrawUiButton } from './Button/TldrawUiButton'
import { TldrawUiButtonIcon } from './Button/TldrawUiButtonIcon'
/** @internal */
export interface ButtonPickerProps<T extends string> {
/** @public */
export interface TLUiButtonPickerProps<T extends string> {
title: string
uiType: string
style: StyleProp<T>
@ -26,7 +25,10 @@ export interface ButtonPickerProps<T extends string> {
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 {
uiType,
items,
@ -46,7 +48,7 @@ function _ButtonPicker<T extends string>(props: ButtonPickerProps<T>) {
handleButtonPointerDown,
handleButtonPointerEnter,
handleButtonPointerUp,
} = React.useMemo(() => {
} = useMemo(() => {
const handlePointerUp = () => {
rPointing.current = false
window.removeEventListener('pointerup', handlePointerUp)
@ -99,7 +101,7 @@ function _ButtonPicker<T extends string>(props: ButtonPickerProps<T>) {
return (
<div className={classNames('tlui-buttons__grid')}>
{items.map((item) => (
<Button
<TldrawUiButton
type="icon"
key={item.value}
data-id={item.value}
@ -117,12 +119,10 @@ function _ButtonPicker<T extends string>(props: ButtonPickerProps<T>) {
onPointerDown={handleButtonPointerDown}
onPointerUp={handleButtonPointerUp}
onClick={handleButtonClick}
icon={item.icon as TLUiIconType}
/>
>
<TldrawUiButtonIcon icon={item.icon} />
</TldrawUiButton>
))}
</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 classNames from 'classnames'
import { Button } from './Button'
import { Icon } from './Icon'
import { TldrawUiButton } from './Button/TldrawUiButton'
import { TldrawUiButtonIcon } from './Button/TldrawUiButtonIcon'
/** @public */
export type TLUiDialogHeaderProps = {
@ -10,7 +10,7 @@ export type TLUiDialogHeaderProps = {
}
/** @public */
export function DialogHeader({ className, children }: TLUiDialogHeaderProps) {
export function TldrawUiDialogHeader({ className, children }: TLUiDialogHeaderProps) {
return <div className={classNames('tlui-dialog__header', className)}>{children}</div>
}
@ -21,7 +21,7 @@ export type TLUiDialogTitleProps = {
}
/** @public */
export function DialogTitle({ className, children }: TLUiDialogTitleProps) {
export function TldrawUiDialogTitle({ className, children }: TLUiDialogTitleProps) {
return (
<_Dialog.DialogTitle dir="ltr" className={classNames('tlui-dialog__header__title', className)}>
{children}
@ -30,17 +30,17 @@ export function DialogTitle({ className, children }: TLUiDialogTitleProps) {
}
/** @public */
export function DialogCloseButton() {
export function TldrawUiDialogCloseButton() {
return (
<div className="tlui-dialog__header__close">
<_Dialog.DialogClose data-testid="dialog.close" dir="ltr" asChild>
<Button
<TldrawUiButton
type="icon"
aria-label="Close"
onTouchEnd={(e) => (e.target as HTMLButtonElement).click()}
>
<Icon small icon="cross-2" />
</Button>
<TldrawUiButtonIcon small icon="cross-2" />
</TldrawUiButton>
</_Dialog.DialogClose>
</div>
)
@ -54,7 +54,7 @@ export type TLUiDialogBodyProps = {
}
/** @public */
export function DialogBody({ className, children, style }: TLUiDialogBodyProps) {
export function TldrawUiDialogBody({ className, children, style }: TLUiDialogBodyProps) {
return (
<div className={classNames('tlui-dialog__body', className)} style={style}>
{children}
@ -69,6 +69,6 @@ export type TLUiDialogFooterProps = {
}
/** @public */
export function DialogFooter({ className, children }: TLUiDialogFooterProps) {
export function TldrawUiDialogFooter({ className, children }: TLUiDialogFooterProps) {
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 { preventDefault, useContainer } from '@tldraw/editor'
import { useMenuIsOpen } from '../../hooks/useMenuIsOpen'
import { Button, TLUiButtonProps } from './Button'
import { Icon } from './Icon'
import { TldrawUiButton } from './Button/TldrawUiButton'
import { TldrawUiButtonIcon } from './Button/TldrawUiButtonIcon'
import { TldrawUiButtonLabel } from './Button/TldrawUiButtonLabel'
import { TldrawUiIcon } from './TldrawUiIcon'
/** @public */
export type TLUiDropdownMenuRootProps = {
@ -13,7 +15,7 @@ export type TLUiDropdownMenuRootProps = {
}
/** @public */
export function DropdownMenuRoot({
export function TldrawUiDropdownMenuRoot({
id,
children,
modal = false,
@ -34,20 +36,21 @@ export function DropdownMenuRoot({
}
/** @public */
export interface TLUiDropdownMenuTriggerProps extends TLUiButtonProps {
export interface TLUiDropdownMenuTriggerProps {
children?: any
}
/** @public */
export function DropdownMenuTrigger({ children, ...rest }: TLUiDropdownMenuTriggerProps) {
export function TldrawUiDropdownMenuTrigger({ children, ...rest }: TLUiDropdownMenuTriggerProps) {
return (
<_DropdownMenu.Trigger
dir="ltr"
asChild
// Firefox fix: Stop the dropdown immediately closing after touch
onTouchEnd={(e) => preventDefault(e)}
{...rest}
>
<Button {...rest}>{children}</Button>
{children}
</_DropdownMenu.Trigger>
)
}
@ -63,7 +66,7 @@ export type TLUiDropdownMenuContentProps = {
}
/** @public */
export function DropdownMenuContent({
export function TldrawUiDropdownMenuContent({
side = 'bottom',
align = 'start',
sideOffset = 8,
@ -92,7 +95,7 @@ export function DropdownMenuContent({
export type TLUiDropdownMenuSubProps = { id: string; children: any }
/** @public */
export function DropdownMenuSub({ id, children }: TLUiDropdownMenuSubProps) {
export function TldrawUiDropdownMenuSub({ id, children }: TLUiDropdownMenuSubProps) {
const [open, onOpenChange] = useMenuIsOpen(id)
return (
@ -111,21 +114,22 @@ export type TLUiDropdownMenuSubTriggerProps = {
}
/** @public */
export function DropdownMenuSubTrigger({
export function TldrawUiDropdownMenuSubTrigger({
label,
title,
disabled,
}: TLUiDropdownMenuSubTriggerProps) {
return (
<_DropdownMenu.SubTrigger dir="ltr" asChild>
<Button
<TldrawUiButton
type="menu"
className="tlui-menu__submenu__trigger"
disabled={disabled}
title={title}
label={label}
icon="chevron-right"
/>
>
<TldrawUiButtonLabel>{label}</TldrawUiButtonLabel>
<TldrawUiButtonIcon icon="chevron-right" small />
</TldrawUiButton>
</_DropdownMenu.SubTrigger>
)
}
@ -139,7 +143,7 @@ export type TLUiDropdownMenuSubContentProps = {
}
/** @public */
export function DropdownMenuSubContent({
export function TldrawUiDropdownMenuSubContent({
alignOffset = -1,
sideOffset = -4,
children,
@ -166,7 +170,10 @@ export type TLUiDropdownMenuGroupProps = {
}
/** @public */
export function DropdownMenuGroup({ children, size = 'medium' }: TLUiDropdownMenuGroupProps) {
export function TldrawUiDropdownMenuGroup({
children,
size = 'medium',
}: TLUiDropdownMenuGroupProps) {
return (
<_DropdownMenu.Group dir="ltr" className="tlui-menu__group" data-size={size}>
{children}
@ -175,28 +182,25 @@ export function DropdownMenuGroup({ children, size = 'medium' }: TLUiDropdownMen
}
/** @public */
export function DropdownMenuIndicator() {
export function TldrawUiDropdownMenuIndicator() {
return (
<_DropdownMenu.ItemIndicator dir="ltr" asChild>
<Icon icon="check" />
<TldrawUiIcon icon="check" />
</_DropdownMenu.ItemIndicator>
)
}
/** @public */
export interface TLUiDropdownMenuItemProps extends TLUiButtonProps {
export interface TLUiDropdownMenuItemProps {
noClose?: boolean
children: any
}
/** @public */
export function DropdownMenuItem({ noClose, ...props }: TLUiDropdownMenuItemProps) {
export function TldrawUiDropdownMenuItem({ noClose, children }: TLUiDropdownMenuItemProps) {
return (
<_DropdownMenu.Item
dir="ltr"
asChild
onClick={noClose || props.isChecked !== undefined ? preventDefault : undefined}
>
<Button {...props} />
<_DropdownMenu.Item dir="ltr" asChild onClick={noClose ? preventDefault : undefined}>
{children}
</_DropdownMenu.Item>
)
}
@ -211,7 +215,7 @@ export interface TLUiDropdownMenuCheckboxItemProps {
}
/** @public */
export function DropdownMenuCheckboxItem({
export function TldrawUiDropdownMenuCheckboxItem({
children,
onSelect,
...rest
@ -228,38 +232,7 @@ export function DropdownMenuCheckboxItem({
>
<div className="tlui-button__checkbox__indicator">
<_DropdownMenu.ItemIndicator dir="ltr">
<Icon 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 />
<TldrawUiIcon icon="check" small />
</_DropdownMenu.ItemIndicator>
</div>
{children}

View file

@ -14,7 +14,7 @@ export interface TLUiIconProps extends React.HTMLProps<HTMLDivElement> {
}
/** @public */
export const Icon = memo(function Icon({
export const TldrawUiIcon = memo(function TldrawUi({
small,
invertIcon,
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 { useBreakpoint } from '../../context/breakpoints'
import { kbd } from './shared'
import { kbd } from '../../kbd-utils'
/** @internal */
export interface KbdProps {
/** @public */
export interface TLUiKbdProps {
children: string
}
/** @internal */
export function Kbd({ children }: KbdProps) {
/** @public */
export function TldrawUiKbd({ children }: TLUiKbdProps) {
const breakpoint = useBreakpoint()
if (breakpoint < PORTRAIT_BREAKPOINT.MOBILE) return null
return (

View file

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

View file

@ -5,7 +5,7 @@ import { TLUiTranslationKey } from '../../hooks/useTranslation/TLUiTranslationKe
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
/** @internal */
export interface SliderProps {
export interface TLUiSliderProps {
steps: number
value: number | null
label: string
@ -15,7 +15,7 @@ export interface SliderProps {
}
/** @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 editor = useEditor()
const msg = useTranslation()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -19,6 +19,7 @@ import {
TLUiKeyboardShortcutsDialogProps,
} from '../components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialog'
import { DefaultMainMenu, TLUiMainMenuProps } from '../components/MainMenu/DefaultMainMenu'
import { DefaultMenuPanel } from '../components/MenuPanel'
import { DefaultMinimap } from '../components/Minimap/DefaultMinimap'
import { DefaultNavigationPanel } from '../components/NavigationPanel/DefaultNavigationPanel'
import { DefaultPageMenu } from '../components/PageMenu/DefaultPageMenu'
@ -45,6 +46,9 @@ export interface BaseTLUiComponents {
QuickActions: ComponentType<TLUiQuickActionsProps>
HelperButtons: ComponentType<TLUiHelperButtonsProps>
DebugMenu: ComponentType
MenuPanel: ComponentType
TopPanel: ComponentType
SharePanel: ComponentType
}
/** @public */
@ -54,7 +58,8 @@ export type TLUiComponents = Partial<{
const TldrawUiComponentsContext = createContext({} as TLUiComponents)
type ComponentsContextProviderProps = {
/** @public */
export type TLUiComponentsProviderProps = {
overrides?: TLUiComponents
children: any
}
@ -63,7 +68,7 @@ type ComponentsContextProviderProps = {
export function TldrawUiComponentsProvider({
overrides = {},
children,
}: ComponentsContextProviderProps) {
}: TLUiComponentsProviderProps) {
const _overrides = useShallowObjectIdentity(overrides)
return (
@ -84,6 +89,7 @@ export function TldrawUiComponentsProvider({
QuickActions: DefaultQuickActions,
HelperButtons: DefaultHelperButtons,
DebugMenu: DefaultDebugMenu,
MenuPanel: DefaultMenuPanel,
..._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 =
typeof window === 'undefined'
? false
@ -13,7 +5,7 @@ const isDarwin =
const cmdKey = isDarwin ? '⌘' : 'Ctrl'
const altKey = isDarwin ? '⌥' : 'Alt'
/** @internal */
/** @public */
export function kbd(str: string) {
return str
.split(',')[0]
@ -24,17 +16,7 @@ export function kbd(str: string) {
})
}
/** @internal */
/** @public */
export function kbdStr(str: string) {
return (
'— ' +
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('')
)
return '— ' + kbd(str).join('')
}