Tighten up editor ui (#2102)

This PR tightens up the editor UI. It removes padding around the editor.

![Kapture 2023-10-28 at 18 27
15](https://github.com/tldraw/tldraw/assets/23072548/18075308-7b62-43a1-8c80-ff4e4136197b)

<img width="1196" alt="image"
src="https://github.com/tldraw/tldraw/assets/23072548/a8205ef1-b142-4fdc-9745-e400c0c4939a">

<img width="1196" alt="image"
src="https://github.com/tldraw/tldraw/assets/23072548/87e9dcd1-39f5-466a-a256-9cbd2ff2cf7e">

### Change Type

- [x] `minor` — New feature

### Release Notes

- Small adjustment to editor ui.
This commit is contained in:
Steve Ruiz 2023-10-28 22:58:32 +01:00 committed by GitHub
parent d018cb9877
commit ddb73cb6cf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 921 additions and 990 deletions

View file

@ -38,6 +38,7 @@ export async function setupPage(page: PlaywrightTestArgs['page']) {
await page.evaluate(() => {
editor.user.updateUserPreferences({ animationSpeed: 0 })
})
await page.mouse.move(50, 50)
}
export async function setupPageWithShapes(page: PlaywrightTestArgs['page']) {

View file

@ -221,13 +221,7 @@ test.describe('Export snapshots', () => {
})
async function snapshotTest(page: Page) {
const downloadEvent = page.waitForEvent('download')
await page.click('[data-testid="main.menu"]')
await page.click('[data-testid="menu-item.edit"]')
await page.click('[data-testid="menu-item.export-as"]')
await page.click('[data-testid="menu-item.export-as-svg"]')
const download = await downloadEvent
page.waitForEvent('download').then(async (download) => {
const path = (await download.path()) as string
assert(path)
await rename(path, path + '.svg')
@ -248,4 +242,6 @@ async function snapshotTest(page: Page) {
omitBackground: true,
clip,
})
})
await page.evaluate(() => (window as any)['tldraw-export']())
}

View file

@ -76,8 +76,14 @@ test.describe('Shape Tools', () => {
if (!(await page.getByTestId(`tools.more`).isVisible())) {
throw Error(`Tool more is not visible`)
}
await page.getByTestId('tools.more').click()
if (!(await page.getByTestId(`tools.more.${tool}`).isVisible())) {
throw Error(`Tool in more panel is not visible`)
}
await page.getByTestId(`tools.more.${tool}`).click()
await page.getByTestId(`tools.more`).click()
}
if (!(await page.getByTestId(`tools.${tool}`).isVisible())) {
@ -106,6 +112,8 @@ test.describe('Shape Tools', () => {
// Find and click the button
if (!(await page.getByTestId(`tools.${tool}`).isVisible())) {
await page.getByTestId('tools.more').click()
await page.getByTestId(`tools.more.${tool}`).click()
await page.getByTestId('tools.more').click()
}
await page.getByTestId(`tools.${tool}`).click()
@ -124,7 +132,7 @@ test.describe('Shape Tools', () => {
expect(await getAllShapeTypes(page)).toEqual([shape])
// Reset for next time
await page.mouse.click(0, 0) // to ensure we're not focused
await page.mouse.click(50, 50) // to ensure we're not focused
await page.keyboard.press('v') // go to the select tool
await page.keyboard.press('Control+a')
await page.keyboard.press('Backspace')
@ -142,7 +150,10 @@ test.describe('Shape Tools', () => {
// Find and click the button
if (!(await page.getByTestId(`tools.${tool}`).isVisible())) {
await page.getByTestId('tools.more').click()
await page.getByTestId(`tools.more.${tool}`).click()
await page.getByTestId('tools.more').click()
}
await page.getByTestId(`tools.${tool}`).click()
// Button should be selected
@ -163,7 +174,7 @@ test.describe('Shape Tools', () => {
expect(await getAllShapeTypes(page)).toEqual([shape])
// Reset for next time
await page.mouse.click(0, 0) // to ensure we're not focused
await page.mouse.click(50, 50) // to ensure we're not focused
await page.keyboard.press('v')
await page.keyboard.press('Control+a')
await page.keyboard.press('Backspace')

View file

@ -71,7 +71,8 @@ test.describe('smoke tests', () => {
expect(await getSelectedShapeColor()).toBe('black')
// when on a mobile device...
const hasMobileMenu = await page.isVisible('.tlui-toolbar__styles__button')
const mobileStylesButton = page.getByTestId('mobile.styles')
const hasMobileMenu = await mobileStylesButton.isVisible()
if (hasMobileMenu) {
// open the style menu

View file

@ -1,5 +1,6 @@
import { Tldraw } from '@tldraw/tldraw'
import { Tldraw, useActions } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css'
import { useEffect } from 'react'
;(window as any).__tldraw_ui_event = { id: 'NOTHING_YET' }
;(window as any).__tldraw_editor_events = []
@ -15,7 +16,19 @@ export default function EndToEnd() {
onUiEvent={(name, data) => {
;(window as any).__tldraw_ui_event = { name, data }
}}
/>
>
<SneakyExportButton />
</Tldraw>
</div>
)
}
function SneakyExportButton() {
const actions = useActions()
useEffect(() => {
;(window as any)['tldraw-export'] = () => actions['export-as-svg'].onSelect('unknown')
}, [actions])
return null
}

View file

@ -1,3 +1,3 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 3L27 3L27 27L3 27L3 3Z" stroke="black" stroke-width="2"/>
<path d="M8 8H22V22H8V8Z" stroke="black" stroke-opacity="0.5" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 173 B

After

Width:  |  Height:  |  Size: 184 B

View file

@ -295,7 +295,7 @@
"style-panel.align": "Align",
"style-panel.vertical-align": "Vertical align",
"style-panel.position": "Position",
"style-panel.arrowheads": "Arrowheads",
"style-panel.arrowheads": "Arrows",
"style-panel.arrowhead-start": "Start",
"style-panel.arrowhead-end": "End",
"style-panel.color": "Color",

View file

@ -18,12 +18,15 @@
/* Radius */
--radius-0: 2px;
--radius-1: 4px;
--radius-2: 7px;
--radius-2: 6px;
--radius-3: 9px;
--radius-4: 12px;
--radius-5: 16px;
--radius-4: 13px;
--layer-background: 100;
--layer-grid: 150;
--layer-canvas: 200;
--layer-shapes: 300;
--layer-overlays: 400;
--layer-following-indicator: 1000;
/* Misc */
--tl-zoom: 1;
--tl-dpr-multiple: 100;
@ -98,8 +101,10 @@
--color-brush-fill: rgba(144, 144, 144, 0.102);
--color-brush-stroke: rgba(144, 144, 144, 0.251);
--color-grid: rgb(109, 109, 109);
--color-low: rgb(237, 240, 242);
--color-low: hsl(204, 16%, 94%);
--color-low-border: hsl(204, 16%, 92%);
--color-culled: rgb(235, 238, 240);
--color-muted-none: rgba(0, 0, 0, 0);
--color-muted-0: rgba(0, 0, 0, 0.02);
--color-muted-1: rgba(0, 0, 0, 0.1);
--color-muted-2: rgba(0, 0, 0, 0.042);
@ -116,19 +121,19 @@
--color-selection-stroke: #2f80ed;
--color-text-0: #1d1d1d;
--color-text-1: #2d2d2d;
--color-text-2: #5f6369;
--color-text-3: #b6b7ba;
--color-text-3: #a4a5a7;
--color-text-shadow: #ffffff;
--color-primary: #2f80ed;
--color-warn: #d10b0b;
--color-text: #000000;
--color-laser: #ff0000;
--shadow-1: 0px 1px 2px rgba(0, 0, 0, 0.22), 0px 1px 3px rgba(0, 0, 0, 0.09);
--shadow-2: 0px 0px 2px rgba(0, 0, 0, 0.12), 0px 2px 3px rgba(0, 0, 0, 0.24),
--shadow-1: 0px 1px 2px rgba(0, 0, 0, 0.25), 0px 1px 3px rgba(0, 0, 0, 0.09);
--shadow-2: 0px 0px 2px rgba(0, 0, 0, 0.16), 0px 2px 3px rgba(0, 0, 0, 0.24),
0px 2px 6px rgba(0, 0, 0, 0.1), inset 0px 0px 0px 1px var(--color-panel-contrast);
--shadow-3: 0px 1px 2px rgba(0, 0, 0, 0.25), 0px 2px 6px rgba(0, 0, 0, 0.14),
--shadow-3: 0px 1px 2px rgba(0, 0, 0, 0.28), 0px 2px 6px rgba(0, 0, 0, 0.14),
inset 0px 0px 0px 1px var(--color-panel-contrast);
--shadow-4: 0px 0px 3px rgba(0, 0, 0, 0.16), 0px 5px 4px rgba(0, 0, 0, 0.16),
--shadow-4: 0px 0px 3px rgba(0, 0, 0, 0.19), 0px 5px 4px rgba(0, 0, 0, 0.16),
0px 2px 16px rgba(0, 0, 0, 0.06), inset 0px 0px 0px 1px var(--color-panel-contrast);
}
@ -139,7 +144,9 @@
--color-brush-stroke: rgba(180, 180, 180, 0.25);
--color-grid: #909090;
--color-low: #2c3136;
--color-culled: rgb(47, 52, 57);
--color-low-border: #30363b
--color-culled: blue;
--color-muted-none: rgba(255, 255, 255, 0);
--color-muted-0: rgba(255, 255, 255, 0.02);
--color-muted-1: rgba(255, 255, 255, 0.1);
--color-muted-2: rgba(255, 255, 255, 0.05);
@ -156,10 +163,10 @@
--color-selection-stroke: #2f80ed;
--color-text-0: #f0eded;
--color-text-1: #d9d9d9;
--color-text-2: #8e9094;
--color-text-3: #515a62;
--color-text-3: #6d747b;
--color-text-shadow: #292f35;
--color-primary: #2f80ed;
--color-warn: #d10b0b;
--color-warn: #ef6464;
--color-text: #f8f9fa;
--color-laser: #ff0000;
@ -234,12 +241,12 @@ input,
.tl-shapes {
position: relative;
z-index: 2;
z-index: var(--layer-shapes);
}
.tl-overlays {
position: relative;
z-index: 3;
z-index: var(--layer-overlays);
}
.tl-overlays__item {
@ -275,6 +282,7 @@ input,
position: absolute;
background-color: var(--color-background);
inset: 0px;
z-index: var(--layer-background);
}
/* --------------------- Grid Layer --------------------- */
@ -287,7 +295,7 @@ input,
height: 100%;
touch-action: none;
pointer-events: none;
z-index: 2;
z-index: var(--layer-grid);
contain: strict;
}
@ -586,7 +594,7 @@ input,
transform-origin: top right;
background-color: var(--color-background);
padding: 2px 4px;
border-radius: 4px;
border-radius: var(--radius-1);
}
/* --------------------- Nametag -------------------- */
@ -967,7 +975,7 @@ input,
color: var(--color-text);
text-overflow: ellipsis;
text-decoration: none;
color: var(--color-text-2);
color: var(--color-text-1);
cursor: var(--tl-cursor-pointer);
}
@ -1516,17 +1524,18 @@ it from receiving any pointer events or affecting the cursor. */
width: 400px;
max-height: 100%;
background-color: var(--color-panel);
padding: var(--space-6);
border-radius: var(--radius-4);
padding: 16px;
border-radius: 16px;
box-shadow: var(--shadow-2);
font-size: 14px;
font-weight: 400;
display: flex;
flex-direction: column;
gap: var(--space-5);
overflow: auto;
z-index: 600;
gap: 12px;
}
.tl-error-boundary__content__expanded {
width: 600px;
}
@ -1549,7 +1558,7 @@ it from receiving any pointer events or affecting the cursor. */
overflow: auto;
font-size: 12px;
max-height: 320px;
margin: 0px;
}
.tl-error-boundary__content button {
@ -1574,7 +1583,7 @@ it from receiving any pointer events or affecting the cursor. */
text-decoration: none;
}
.tl-error-boundary__content a:hover {
color: var(--color-text-2);
color: var(--color-text-1);
}
.tl-error-boundary__content__error {
@ -1594,8 +1603,8 @@ it from receiving any pointer events or affecting the cursor. */
display: flex;
justify-content: space-between;
gap: var(--space-4);
margin: calc(var(--space-4) * -1);
margin-top: 0px;
margin: 0px;
margin-left: -4px;
}
.tl-error-boundary__content__actions__group {
display: flex;

View file

@ -1024,10 +1024,11 @@ export function parseTldrawJsonFile({ json, schema, }: {
function RadioItem({ children, onSelect, ...rest }: DropdownMenuCheckboxItemProps): JSX.Element;
// @public (undocumented)
function Root({ id, children, modal, }: {
function Root({ id, children, modal, debugOpen, }: {
id: string;
children: any;
modal?: boolean;
debugOpen?: boolean;
}): JSX.Element;
// @public (undocumented)
@ -1309,7 +1310,7 @@ export interface TLUiButtonProps extends React_3.HTMLAttributes<HTMLButtonElemen
// (undocumented)
spinner?: boolean;
// (undocumented)
type?: 'danger' | 'normal' | 'primary';
type: 'danger' | 'help' | 'icon' | 'low' | 'menu' | 'normal' | 'primary' | 'tool';
}
// @public (undocumented)
@ -1518,7 +1519,7 @@ export interface TLUiToast {
// (undocumented)
description?: string;
// (undocumented)
icon?: string;
icon?: TLUiIconType;
// (undocumented)
id: string;
// (undocumented)
@ -1534,7 +1535,7 @@ export interface TLUiToastAction {
// (undocumented)
onClick: () => void;
// (undocumented)
type: 'primary' | 'secondary' | 'warn';
type: 'danger' | 'normal' | 'primary';
}
// @public (undocumented)

View file

@ -4137,11 +4137,11 @@
"excerptTokens": [
{
"kind": "Content",
"text": "export declare function Root({ id, children, modal, }: "
"text": "export declare function Root({ id, children, modal, debugOpen, }: "
},
{
"kind": "Content",
"text": "{\n id: string;\n children: any;\n modal?: boolean;\n}"
"text": "{\n id: string;\n children: any;\n modal?: boolean;\n debugOpen?: boolean;\n}"
},
{
"kind": "Content",
@ -4166,7 +4166,7 @@
"overloadIndex": 1,
"parameters": [
{
"parameterName": "{ id, children, modal, }",
"parameterName": "{ id, children, modal, debugOpen, }",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
@ -14545,11 +14545,11 @@
"excerptTokens": [
{
"kind": "Content",
"text": "type?: "
"text": "type: "
},
{
"kind": "Content",
"text": "'danger' | 'normal' | 'primary'"
"text": "'danger' | 'help' | 'icon' | 'low' | 'menu' | 'normal' | 'primary' | 'tool'"
},
{
"kind": "Content",
@ -14557,7 +14557,7 @@
}
],
"isReadonly": false,
"isOptional": true,
"isOptional": false,
"releaseTag": "Public",
"name": "type",
"propertyTypeTokenRange": {
@ -14583,7 +14583,7 @@
"text": "export interface TLUiContextMenuProps "
}
],
"fileUrlPath": "packages/tldraw/.tsbuild-api/lib/ui/components/ContextMenu.d.ts",
"fileUrlPath": "packages/tldraw/src/lib/ui/components/ContextMenu.tsx",
"releaseTag": "Public",
"name": "TLUiContextMenuProps",
"preserveMemberOrder": false,
@ -14606,7 +14606,6 @@
"text": ";"
}
],
"fileUrlPath": "packages/tldraw/src/lib/ui/components/ContextMenu.tsx",
"isReadonly": false,
"isOptional": false,
"releaseTag": "Public",
@ -16746,8 +16745,9 @@
"text": "icon?: "
},
{
"kind": "Content",
"text": "string"
"kind": "Reference",
"text": "TLUiIconType",
"canonicalReference": "@tldraw/tldraw!TLUiIconType:type"
},
{
"kind": "Content",
@ -16927,7 +16927,7 @@
},
{
"kind": "Content",
"text": "'primary' | 'secondary' | 'warn'"
"text": "'danger' | 'normal' | 'primary'"
},
{
"kind": "Content",

File diff suppressed because it is too large Load diff

View file

@ -141,6 +141,7 @@ const TldrawUiContent = React.memo(function TldrawUI({
{isFocusMode ? (
<div className="tlui-layout__top">
<Button
type="icon"
className="tlui-focus-button"
title={`${msg('focus-mode.toggle-focus-mode')}`}
icon="dot"

View file

@ -25,9 +25,9 @@ export const ActionsMenu = memo(function ActionsMenu() {
return (
<Button
key={id}
className="tlui-button-grid__button"
data-testid={`menu-item.${item.id}`}
icon={icon}
type="icon"
title={
label
? kbd
@ -46,13 +46,14 @@ export const ActionsMenu = memo(function ActionsMenu() {
}
return (
<Popover id="actions menu">
<Popover id="actions-menu">
<PopoverTrigger>
<Button
className="tlui-menu__trigger"
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
/>
</PopoverTrigger>
@ -63,7 +64,7 @@ export const ActionsMenu = memo(function ActionsMenu() {
dir="ltr"
sideOffset={6}
>
<div className="tlui-actions-menu tlui-button-grid__four">
<div className="tlui-actions-menu tlui-buttons__grid">
{menuSchema.map(getActionMenuItem)}
</div>
</PopoverPrimitive.Content>

View file

@ -43,6 +43,7 @@ export function BackToContent() {
<Button
iconLeft={action.icon}
label={action.label}
type="low"
onClick={() => {
action.onSelect('helper-buttons')
setShowBackToContent(false)

View file

@ -144,7 +144,7 @@ function ContextMenuContent() {
<_ContextMenu.Sub key={item.id} onOpenChange={handleSubOpenChange}>
<_ContextMenu.SubTrigger dir="ltr" disabled={item.disabled} asChild>
<Button
className="tlui-menu__button"
type="menu"
label={item.label}
data-testid={`menu-item.${item.id}`}
icon="chevron-right"
@ -170,7 +170,7 @@ function ContextMenuContent() {
return (
<_ContextMenu.CheckboxItem
key={id}
className="tlui-button tlui-menu__button tlui-menu__checkbox-item"
className="tlui-button tlui-button__menu tlui-button__checkbox"
dir="ltr"
disabled={item.disabled}
onSelect={(e) => {
@ -179,18 +179,13 @@ function ContextMenuContent() {
}}
title={labelStr ? labelStr : undefined}
checked={item.checked}
>
<div
className="tlui-menu__checkbox-item__check"
style={{
transformOrigin: '75% center',
transform: `scale(${item.checked ? 1 : 0.5})`,
opacity: item.checked ? 1 : 0.5,
}}
>
<Icon small icon={item.checked ? 'check' : 'checkbox-empty'} />
</div>
{labelStr && <span>{labelStr}</span>}
{labelStr && (
<span className="tlui-button__label" draggable={false}>
{labelStr}
</span>
)}
{kbd && <Kbd>{kbd}</Kbd>}
</_ContextMenu.CheckboxItem>
)
@ -199,7 +194,7 @@ function ContextMenuContent() {
return (
<_ContextMenu.Item key={id} dir="ltr" asChild>
<Button
className="tlui-menu__button"
type="menu"
data-testid={`menu-item.${id}`}
kbd={kbd}
label={labelToUse}

View file

@ -14,7 +14,7 @@ import {
import * as React from 'react'
import { useDialogs } from '../hooks/useDialogsProvider'
import { useToasts } from '../hooks/useToastsProvider'
import { useTranslation } from '../hooks/useTranslation/useTranslation'
import { untranslated, useTranslation } from '../hooks/useTranslation/useTranslation'
import { Button } from './primitives/Button'
import * as Dialog from './primitives/Dialog'
import * as DropdownMenu from './primitives/DropdownMenu'
@ -53,7 +53,7 @@ export const DebugPanel = React.memo(function DebugPanel({
<ShapeCount />
<DropdownMenu.Root id="debug">
<DropdownMenu.Trigger>
<Button icon="dots-horizontal" title={msg('debug-panel.more')} />
<Button type="icon" icon="dots-horizontal" title={msg('debug-panel.more')} />
</DropdownMenu.Trigger>
<DropdownMenu.Content side="top" align="end" alignOffset={0}>
<DebugMenuContent renderDebugMenuItems={renderDebugMenuItems} />
@ -83,29 +83,60 @@ const DebugMenuContent = track(function DebugMenuContent({
const editor = useEditor()
const { addToast } = useToasts()
const { addDialog } = useDialogs()
const [error, setError] = React.useState<boolean>(false)
return (
<>
<DropdownMenu.Group>
<DropdownMenu.Item
type="menu"
onClick={() => {
addToast({
id: uniqueId(),
title: 'Something happened',
description: 'Hey, attend to this thing over here. It might be important!',
keepOpen: true,
// icon?: string
// title?: string
// description?: string
// actions?: TLUiToastAction[]
})
addToast({
id: uniqueId(),
title: 'Something happened',
description: 'Hey, attend to this thing over here. It might be important!',
keepOpen: true,
icon: 'twitter',
actions: [
{
label: 'Primary',
type: 'primary',
onClick: () => {
void null
},
},
{
label: 'Normal',
type: 'normal',
onClick: () => {
void null
},
},
{
label: 'Danger',
type: 'danger',
onClick: () => {
void null
},
},
],
})
}}
>
<span>Show toast</span>
</DropdownMenu.Item>
label={untranslated('Show toast')}
/>
<DropdownMenu.Item
type="menu"
onClick={() => {
addDialog({
component: ({ onClose }) => (
@ -124,13 +155,15 @@ const DebugMenuContent = track(function DebugMenuContent({
},
})
}}
>
<span>Show dialog</span>
</DropdownMenu.Item>
<DropdownMenu.Item onClick={() => createNShapes(editor, 100)}>
<span>Create 100 shapes</span>
</DropdownMenu.Item>
label={untranslated('Show dialog')}
/>
<DropdownMenu.Item
type="menu"
onClick={() => createNShapes(editor, 100)}
label={untranslated('Create 100 shapes')}
/>
<DropdownMenu.Item
type="menu"
onClick={() => {
function countDescendants({ children }: HTMLElement) {
let count = 0
@ -158,27 +191,25 @@ const DebugMenuContent = track(function DebugMenuContent({
window.alert(`Shapes ${shapes.length}, DOM nodes:${descendants}`)
}}
>
<span>Count shapes and nodes</span>
</DropdownMenu.Item>
label={untranslated('Count shapes / nodes')}
/>
{(() => {
if (error) throw Error('oh no!')
})()}
<DropdownMenu.Item
type="menu"
onClick={() => {
setError(true)
}}
>
<span>Throw error</span>
</DropdownMenu.Item>
label={untranslated('Throw error')}
/>
<DropdownMenu.Item
type="menu"
onClick={() => {
hardResetEditor()
}}
>
<span>Hard reset</span>
</DropdownMenu.Item>
label={untranslated('Hard reset')}
/>
</DropdownMenu.Group>
<DropdownMenu.Group>
<DebugFlagToggle flag={debugFlags.debugSvg} />
@ -206,8 +237,14 @@ function Toggle({
onChange: (newValue: boolean) => void
}) {
return (
<DropdownMenu.CheckboxItem title={label} checked={value} onSelect={() => onChange(!value)}>
<DropdownMenu.CheckboxItem
title={untranslated(label)}
checked={value}
onSelect={() => onChange(!value)}
>
<span className="tlui-button__label" draggable={false}>
{label}
</span>
</DropdownMenu.CheckboxItem>
)
}
@ -262,14 +299,17 @@ function ExampleDialog({
<Dialog.Footer className="tlui-dialog__footer__actions">
{displayDontShowAgain && (
<Button
type="normal"
onClick={() => setDontShowAgain(!dontShowAgain)}
iconLeft={dontShowAgain ? 'checkbox-checked' : 'checkbox-empty'}
iconLeft={dontShowAgain ? 'check' : 'checkbox-empty'}
style={{ marginRight: 'auto' }}
>
{`Don't show again`}
</Button>
)}
<Button onClick={onCancel}>{cancel}</Button>
<Button type="normal" onClick={onCancel}>
{cancel}
</Button>
<Button type="primary" onClick={async () => onContinue()}>
{confirm}
</Button>

View file

@ -13,6 +13,7 @@ export const DuplicateButton = track(function DuplicateButton() {
return (
<Button
icon={action.icon}
type="icon"
onClick={() => action.onSelect('quick-actions')}
disabled={!(editor.isIn('select') && editor.selectedShapeIds.length > 0)}
title={`${msg(action.label!)} ${kbdStr(action.kbd!)}`}

View file

@ -157,7 +157,7 @@ export const EditLinkDialogInner = track(function EditLinkDialogInner({
</div>
</Dialog.Body>
<Dialog.Footer className="tlui-dialog__footer__actions">
<Button onClick={handleCancel} onTouchEnd={handleCancel}>
<Button type="normal" onClick={handleCancel} onTouchEnd={handleCancel}>
{msg('edit-link-dialog.cancel')}
</Button>
{isRemoving ? (

View file

@ -3,7 +3,7 @@ import { useRef, useState } from 'react'
import { TLEmbedResult, getEmbedInfo } from '../../utils/embeds'
import { useAssetUrls } from '../hooks/useAssetUrls'
import { TLUiDialogProps } from '../hooks/useDialogsProvider'
import { useTranslation } from '../hooks/useTranslation/useTranslation'
import { untranslated, useTranslation } from '../hooks/useTranslation/useTranslation'
import { Button } from './primitives/Button'
import * as Dialog from './primitives/Dialog'
import { Icon } from './primitives/Icon'
@ -88,6 +88,7 @@ export const EmbedDialog = track(function EmbedDialog({ onClose }: TLUiDialogPro
</Dialog.Body>
<Dialog.Footer className="tlui-dialog__footer__actions">
<Button
type="normal"
onClick={() => {
setEmbedDefinition(null)
setEmbedInfoForUrl(null)
@ -96,7 +97,7 @@ export const EmbedDialog = track(function EmbedDialog({ onClose }: TLUiDialogPro
label="embed-dialog.back"
/>
<div className="tlui-embed__spacer" />
<Button label="embed-dialog.cancel" onClick={onClose} />
<Button type="normal" label="embed-dialog.cancel" onClick={onClose} />
<Button
type="primary"
disabled={!embedInfoForUrl}
@ -121,25 +122,20 @@ export const EmbedDialog = track(function EmbedDialog({ onClose }: TLUiDialogPro
<Dialog.Body className="tlui-embed-dialog__list">
{EMBED_DEFINITIONS.map((def) => {
return (
<button
<Button
type="menu"
key={def.type}
className="tlui-embed-dialog__item"
onClick={() => setEmbedDefinition(def)}
label={untranslated(def.title)}
>
<div className="tlui-embed-dialog__item__image">
<div
className="tlui-embed-dialog__item__image__img"
style={{
backgroundImage: `url(${assetUrls.embedIcons[def.type]})`,
}}
className="tlui-embed-dialog__item__image"
style={{ backgroundImage: `url(${assetUrls.embedIcons[def.type]})` }}
/>
</div>
<div className="tlui-embed-dialog__item__title">{def.title}</div>
</button>
</Button>
)
})}
</Dialog.Body>
<div className="tlui-dialog__scrim" />
</>
)}
</>

View file

@ -10,5 +10,5 @@ export function FollowingIndicator() {
function FollowingIndicatorInner({ userId }: { userId: string }) {
const presence = usePresence(userId)
if (!presence) return null
return <div className="tlui-following" style={{ borderColor: presence.color }} />
return <div className="tlui-following-indicator" style={{ borderColor: presence.color }} />
}

View file

@ -9,8 +9,8 @@ import { TLUiTranslationKey } from '../hooks/useTranslation/TLUiTranslationKey'
import { useTranslation } from '../hooks/useTranslation/useTranslation'
import { TLUiIconType } from '../icon-types'
import { LanguageMenu } from './LanguageMenu'
import { Button } from './primitives/Button'
import * as M from './primitives/DropdownMenu'
import { Icon } from './primitives/Icon'
interface HelpMenuLink {
label: TLUiTranslationKey
@ -32,12 +32,14 @@ export const HelpMenu = React.memo(function HelpMenu() {
return (
<div className="tlui-help-menu">
<Root dir="ltr" open={isOpen} onOpenChange={onOpenChange} modal={false}>
<Trigger
className="tlui-button tlui-help-menu__button"
dir="ltr"
<Trigger asChild dir="ltr">
<Button
type="help"
className="tlui-button"
smallIcon
title={msg('help-menu.title')}
>
<Icon icon="question-mark" />
icon="question-mark"
/>
</Trigger>
<Portal container={container} dir="ltr">
<Content
@ -90,6 +92,7 @@ function HelpMenuContent() {
const { id, kbd, label, onSelect, icon } = item.actionItem
return (
<M.Item
type="menu"
key={id}
kbd={kbd}
label={label}

View file

@ -25,7 +25,7 @@ export function LanguageMenu() {
checked={locale === currentLanguage}
onSelect={() => handleLanguageSelect(locale)}
>
<span>{label}</span>
<span className="tlui-button__label">{label}</span>
</D.RadioItem>
))}
</D.Group>

View file

@ -17,10 +17,12 @@ export const Menu = React.memo(function Menu() {
<M.Root id="main menu">
<M.Trigger>
<Button
type="icon"
className="tlui-menu__trigger"
data-testid="main.menu"
title={msg('menu.title')}
icon="menu"
smallIcon
/>
</M.Trigger>
<M.Content alignOffset={0} sideOffset={6}>
@ -100,7 +102,7 @@ function MenuContent() {
checked={item.checked}
disabled={item.disabled}
>
{labelStr && <span>{labelStr}</span>}
{labelStr && <span className="tlui-button__label">{labelStr}</span>}
{kbd && <Kbd>{kbd}</Kbd>}
</M.CheckboxItem>
)
@ -109,6 +111,7 @@ function MenuContent() {
// Item is a button
return (
<M.Item
type="menu"
key={id}
data-testid={`menu-item.${item.id}`}
kbd={kbd}

View file

@ -17,13 +17,11 @@ export const MenuZone = track(function MenuZone() {
return (
<div className="tlui-menu-zone">
<div className="tlui-menu-zone__controls">
<div className="tlui-buttons__horizontal">
<Menu />
<div className="tlui-menu-zone__divider" />
<PageMenu />
{breakpoint >= 6 && !isReadonly && !editor.isInAny('hand', 'zoom') && (
<>
<div className="tlui-menu-zone__divider" />
<UndoButton />
<RedoButton />
<TrashButton />

View file

@ -43,7 +43,7 @@ export function MobileStylePanel() {
<Popover id="style menu" onOpenChange={handleStylesOpenChange}>
<PopoverTrigger disabled={disableStylePanel}>
<Button
className="tlui-toolbar__tools__button tlui-toolbar__styles__button"
type="tool"
data-testid="mobile.styles"
style={{
color: disableStylePanel ? 'var(--color-muted-1)' : currentColor,

View file

@ -16,7 +16,7 @@ export const MoveToPageMenu = track(function MoveToPageMenu() {
<_ContextMenu.Sub>
<_ContextMenu.SubTrigger dir="ltr" asChild>
<Button
className="tlui-menu__button"
type="menu"
label="context-menu.move-to-page"
data-testid="menu-item.move-to-page"
icon="chevron-right"
@ -60,8 +60,9 @@ export const MoveToPageMenu = track(function MoveToPageMenu() {
asChild
>
<Button
type="menu"
title={page.name}
className="tlui-menu__button tlui-context-menu__move-to-page__name"
className="tlui-context-menu__move-to-page__name"
>
<span>{page.name}</span>
</Button>
@ -88,8 +89,9 @@ export const MoveToPageMenu = track(function MoveToPageMenu() {
asChild
>
<Button
type="menu"
title={msg('context.pages.new-page')}
className="tlui-menu__button tlui-context-menu__move-to-page__name"
className="tlui-context-menu__move-to-page__name"
>
{msg('context.pages.new-page')}
</Button>

View file

@ -26,23 +26,25 @@ export const NavigationZone = memo(function NavigationZone() {
return (
<div className="tlui-navigation-zone">
<div className="tlui-navigation-zone__controls">
<div className="tlui-buttons__horizontal">
{breakpoint < 6 ? (
<ZoomMenu />
) : collapsed ? (
<>
<ZoomMenu />
<Button
type="icon"
icon={collapsed ? 'chevrons-ne' : 'chevrons-sw'}
data-testid="minimap.toggle"
title={msg('navigation-zone.toggle-minimap')}
className="tlui-navigation-zone__toggle"
data-testid="minimap.toggle"
onClick={toggleMinimap}
icon={collapsed ? 'chevrons-ne' : 'chevrons-sw'}
/>
</>
) : (
<>
<Button
type="icon"
icon="minus"
data-testid="minimap.zoom-out"
title={`${msg(actions['zoom-out'].label!)} ${kbdStr(actions['zoom-out'].kbd!)}`}
@ -50,16 +52,19 @@ export const NavigationZone = memo(function NavigationZone() {
/>
<ZoomMenu />
<Button
type="icon"
icon="plus"
data-testid="minimap.zoom-in"
title={`${msg(actions['zoom-in'].label!)} ${kbdStr(actions['zoom-in'].kbd!)}`}
onClick={() => actions['zoom-in'].onSelect('navigation-zone')}
/>
<Button
type="icon"
icon={collapsed ? 'chevrons-ne' : 'chevrons-sw'}
data-testid="minimap.toggle"
title={msg('navigation-zone.toggle-minimap')}
className="tlui-navigation-zone__toggle"
onClick={toggleMinimap}
icon={collapsed ? 'chevrons-ne' : 'chevrons-sw'}
/>
</>
)}

View file

@ -24,6 +24,7 @@ export const ZoomMenu = track(function ZoomMenu() {
<M.Root id="zoom">
<M.Trigger>
<Button
type="icon"
title={`${msg('navigation-zone.zoom')}`}
data-testid="minimap.zoom-menu"
className={breakpoint < 5 ? 'tlui-zoom-menu__button' : 'tlui-zoom-menu__button__pct'}
@ -74,6 +75,7 @@ function ZoomMenuItem(props: {
return (
<M.Item
type="menu"
label={actions[action].label}
kbd={actions[action].kbd}
data-testid={props['data-testid']}

View file

@ -45,13 +45,13 @@ export const PageItemSubmenu = track(function PageItemSubmenu({
return (
<M.Root id={`page item submenu ${index}`}>
<M.Trigger>
<Button title={msg('page-menu.submenu.title')} icon="dots-vertical" />
<Button type="icon" title={msg('page-menu.submenu.title')} icon="dots-vertical" />
</M.Trigger>
<M.Content alignOffset={0}>
<M.Group>
{onRename && (
<DropdownMenu.Item dir="ltr" onSelect={onRename} asChild>
<Button className="tlui-menu__button" label="page-menu.submenu.rename" />
<Button type="menu" label="page-menu.submenu.rename" />
</DropdownMenu.Item>
)}
<DropdownMenu.Item
@ -60,23 +60,23 @@ export const PageItemSubmenu = track(function PageItemSubmenu({
disabled={pages.length >= MAX_PAGES}
asChild
>
<Button className="tlui-menu__button" label="page-menu.submenu.duplicate-page" />
<Button type="menu" label="page-menu.submenu.duplicate-page" />
</DropdownMenu.Item>
{index > 0 && (
<DropdownMenu.Item dir="ltr" onSelect={onMoveUp} asChild>
<Button className="tlui-menu__button" label="page-menu.submenu.move-up" />
<Button type="menu" label="page-menu.submenu.move-up" />
</DropdownMenu.Item>
)}
{index < listSize - 1 && (
<DropdownMenu.Item dir="ltr" onSelect={onMoveDown} asChild>
<Button className="tlui-menu__button" label="page-menu.submenu.move-down" />
<Button type="menu" label="page-menu.submenu.move-down" />
</DropdownMenu.Item>
)}
</M.Group>
{listSize > 1 && (
<M.Group>
<DropdownMenu.Item dir="ltr" onSelect={onDelete} asChild>
<Button className="tlui-menu__button" label="page-menu.submenu.delete" />
<Button type="menu" label="page-menu.submenu.delete" />
</DropdownMenu.Item>
</M.Group>
)}

View file

@ -28,7 +28,7 @@ export const PageMenu = function PageMenu() {
const [isOpen, onOpenChange] = useMenuIsOpen('page-menu', handleOpenChange)
const ITEM_HEIGHT = breakpoint < 5 ? 36 : 40
const ITEM_HEIGHT = 36
const rSortableContainer = useRef<HTMLDivElement>(null)
@ -251,12 +251,13 @@ export const PageMenu = function PageMenu() {
}, [editor, msg, isReadonlyMode])
return (
<Popover id="page menu" onOpenChange={onOpenChange} open={isOpen}>
<Popover id="pages" onOpenChange={onOpenChange} open={isOpen}>
<PopoverTrigger>
<Button
className="tlui-page-menu__trigger tlui-menu__trigger"
data-testid="main.page-menu"
icon="chevron-down"
type="menu"
title={currentPage.name}
>
<div className="tlui-page-menu__name">{currentPage.name}</div>
@ -267,14 +268,16 @@ export const PageMenu = function PageMenu() {
<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
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
type="icon"
data-testid="page-menu.create"
icon="plus"
title={msg(
@ -285,7 +288,7 @@ export const PageMenu = function PageMenu() {
disabled={maxPageCountReached}
onClick={handleCreatePageClick}
/>
</>
</div>
)}
</div>
<div
@ -310,6 +313,7 @@ export const PageMenu = function PageMenu() {
}}
>
<Button
type="icon"
tabIndex={-1}
className="tlui-page_menu__item__sortable__handle"
icon="drag-handle-dots"
@ -326,6 +330,7 @@ export const PageMenu = function PageMenu() {
// to be fighting over scroll position. Nothing
// else seems to work!
<Button
type="icon"
className="tlui-page-menu__item__button"
onClick={() => {
const name = window.prompt('Rename page', page.name)
@ -363,6 +368,7 @@ export const PageMenu = function PageMenu() {
className="tlui-page-menu__item"
>
<Button
type="icon"
className="tlui-page-menu__item__button tlui-page-menu__item__button__checkbox"
onClick={() => editor.setCurrentPage(page.id)}
onDoubleClick={toggleEditing}

View file

@ -15,6 +15,7 @@ export const ExitPenMode = track(function ExitPenMode() {
return (
<Button
type="normal"
label={action.label}
iconLeft={action.icon}
onClick={() => action.onSelect('helper-buttons')}

View file

@ -16,6 +16,7 @@ export const RedoButton = memo(function RedoButton() {
<Button
data-testid="main.redo"
icon={redo.icon}
type="icon"
title={`${msg(redo.label!)} ${kbdStr(redo.kbd!)}`}
disabled={!canRedo}
onClick={() => redo.onSelect('quick-actions')}

View file

@ -14,6 +14,7 @@ export const StopFollowing = track(function ExitPenMode() {
return (
<Button
type="normal"
label={action.label}
iconLeft={action.icon}
onClick={() => action.onSelect('people-menu')}

View file

@ -1,6 +1,5 @@
import { Trigger } from '@radix-ui/react-dropdown-menu'
import { SharedStyle, StyleProp, preventDefault } from '@tldraw/editor'
import classNames from 'classnames'
import * as React from 'react'
import { TLUiTranslationKey } from '../../hooks/useTranslation/TLUiTranslationKey'
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
@ -60,6 +59,7 @@ export const DoubleDropdownPicker = React.memo(function DoubleDropdownPicker<T e
<div title={msg(label)} className="tlui-style-panel__double-select-picker-label">
{msg(label)}
</div>
<div className="tlui-buttons__horizontal">
<DropdownMenu.Root id={`style panel ${uiTypeA} A`}>
<Trigger
asChild
@ -67,6 +67,7 @@ export const DoubleDropdownPicker = React.memo(function DoubleDropdownPicker<T e
onTouchEnd={(e) => preventDefault(e)}
>
<Button
type="icon"
data-testid={`style.${uiTypeA}`}
title={
msg(labelA) +
@ -81,16 +82,11 @@ export const DoubleDropdownPicker = React.memo(function DoubleDropdownPicker<T e
/>
</Trigger>
<DropdownMenu.Content side="bottom" align="end" sideOffset={0} alignOffset={-2}>
<div
className={classNames('tlui-button-grid', {
'tlui-button-grid__two': itemsA.length < 4,
'tlui-button-grid__four': itemsA.length >= 4,
})}
>
<div className="tlui-buttons__grid">
{itemsA.map((item) => {
return (
<DropdownMenu.Item
className="tlui-button-grid__button"
type="icon"
title={
msg(labelA) +
' — ' +
@ -114,6 +110,7 @@ export const DoubleDropdownPicker = React.memo(function DoubleDropdownPicker<T e
onTouchEnd={(e) => preventDefault(e)}
>
<Button
type="icon"
data-testid={`style.${uiTypeB}`}
title={
msg(labelB) +
@ -127,16 +124,11 @@ export const DoubleDropdownPicker = React.memo(function DoubleDropdownPicker<T e
/>
</Trigger>
<DropdownMenu.Content side="bottom" align="end" sideOffset={0} alignOffset={-2}>
<div
className={classNames('tlui-button-grid', {
'tlui-button-grid__two': itemsA.length < 4,
'tlui-button-grid__four': itemsA.length >= 4,
})}
>
<div className="tlui-buttons__grid">
{itemsB.map((item) => {
return (
<DropdownMenu.Item
className="tlui-button-grid__button"
type="icon"
title={
msg(labelB) +
' — ' +
@ -153,5 +145,6 @@ export const DoubleDropdownPicker = React.memo(function DoubleDropdownPicker<T e
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
</div>
)
})

View file

@ -1,11 +1,10 @@
import { Trigger } from '@radix-ui/react-dropdown-menu'
import { SharedStyle, StyleProp, preventDefault } 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 { Button } from '../primitives/Button'
import { Button, TLUiButtonProps } from '../primitives/Button'
import * as DropdownMenu from '../primitives/DropdownMenu'
import { StyleValuesForUi } from './styles'
@ -16,6 +15,7 @@ interface DropdownPickerProps<T extends string> {
style: StyleProp<T>
value: SharedStyle<T>
items: StyleValuesForUi<T>
type: TLUiButtonProps['type']
onValueChange: (style: StyleProp<T>, value: T, squashing: boolean) => void
}
@ -25,6 +25,7 @@ export const DropdownPicker = React.memo(function DropdownPicker<T extends strin
uiType,
style,
items,
type,
value,
onValueChange,
}: DropdownPickerProps<T>) {
@ -43,6 +44,7 @@ export const DropdownPicker = React.memo(function DropdownPicker<T extends strin
onTouchEnd={(e) => preventDefault(e)}
>
<Button
type={type}
data-testid={`style.${uiType}`}
title={
value.type === 'mixed'
@ -54,17 +56,11 @@ export const DropdownPicker = React.memo(function DropdownPicker<T extends strin
/>
</Trigger>
<DropdownMenu.Content side="left" align="center" alignOffset={0}>
<div
className={classNames('tlui-button-grid', {
'tlui-button-grid__two': items.length < 3,
'tlui-button-grid__three': items.length == 3,
'tlui-button-grid__four': items.length >= 4,
})}
>
<div className="tlui-buttons__grid">
{items.map((item) => {
return (
<DropdownMenu.Item
className="tlui-button-grid__button"
type="icon"
data-testid={`style.${uiType}.${item.value}`}
title={msg(`${uiType}-style.${item.value}` as TLUiTranslationKey)}
key={item.value}

View file

@ -233,8 +233,10 @@ function TextStylePickerSet({ styles }: { styles: ReadonlySharedStyleMap }) {
value={align}
onValueChange={handleValueChange}
/>
<div className="tlui-style-panel__row__extra-button">
{verticalAlign === undefined ? (
<Button
type="icon"
title={msg('style-panel.vertical-align')}
data-testid="vertical-align"
icon="vertical-align-center"
@ -242,6 +244,7 @@ function TextStylePickerSet({ styles }: { styles: ReadonlySharedStyleMap }) {
/>
) : (
<DropdownPicker
type="icon"
id="geo-vertical-alignment"
uiType="verticalAlign"
style={DefaultVerticalAlignStyle}
@ -251,6 +254,7 @@ function TextStylePickerSet({ styles }: { styles: ReadonlySharedStyleMap }) {
/>
)}
</div>
</div>
)}
</div>
)
@ -267,6 +271,7 @@ function GeoStylePickerSet({ styles }: { styles: ReadonlySharedStyleMap }) {
return (
<DropdownPicker
id="geo"
type="menu"
label={'style-panel.geo'}
uiType="geo"
style={GeoShapeGeoStyle}
@ -288,6 +293,7 @@ function SplineStylePickerSet({ styles }: { styles: ReadonlySharedStyleMap }) {
return (
<DropdownPicker
id="spline"
type="menu"
label={'style-panel.spline'}
uiType="spline"
style={LineShapeSplineStyle}

View file

@ -40,28 +40,22 @@ 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
className={
action.type === 'warn' ? 'tlui-button__warning' : 'tlui-button__primary'
}
>
{action.label}
</Button>
<Button type={action.type}>{action.label}</Button>
</T.Action>
))}
{hasActions && (
<T.Close asChild>
<Button className="tlui-toast__close" style={{ marginLeft: 'auto' }}>
<Button type="normal" className="tlui-toast__close" style={{ marginLeft: 'auto' }}>
{toast.closeLabel ?? msg('toast.close')}
</Button>
</T.Close>
)}
</div>
)}
</div>
{!hasActions && (
<T.Close asChild>
<Button className="tlui-toast__close">{toast.closeLabel ?? msg('toast.close')}</Button>
<Button type="normal" className="tlui-toast__close">
{toast.closeLabel ?? msg('toast.close')}
</Button>
</T.Close>
)}
</T.Root>

View file

@ -30,6 +30,7 @@ export function ToggleToolLockedButton({ activeToolId }: ToggleToolLockedButtonP
return (
<Button
type="normal"
title={msg('action.toggle-tool-lock')}
className={classNames('tlui-toolbar__lock-button', {
'tlui-toolbar__lock-button__mobile': breakpoint < 5,

View file

@ -111,7 +111,7 @@ export const Toolbar = memo(function Toolbar() {
{!isReadonly && (
<div className="tlui-toolbar__extras">
{breakpoint < 6 && !(activeToolId === 'hand' || activeToolId === 'zoom') && (
<div className="tlui-toolbar__extras__controls">
<div className="tlui-toolbar__extras__controls tlui-buttons__horizontal">
<UndoButton />
<RedoButton />
<TrashButton />
@ -149,7 +149,6 @@ export const Toolbar = memo(function Toolbar() {
{showEditingTools && (
<>
{/* Draw / Eraser */}
<div className="tlui-toolbar__divider" />
{toolbarItems.slice(2, 4).map(({ toolItem }) => (
<ToolbarButton
key={toolItem.id}
@ -159,7 +158,6 @@ export const Toolbar = memo(function Toolbar() {
/>
))}
{/* Everything Else */}
<div className="tlui-toolbar__divider" />
{itemsInPanel.map(({ toolItem }) => (
<ToolbarButton
key={toolItem.id}
@ -186,8 +184,9 @@ export const Toolbar = memo(function Toolbar() {
<M.Root id="toolbar overflow" modal={false}>
<M.Trigger>
<Button
className="tlui-toolbar__tools__button tlui-toolbar__overflow"
className="tlui-toolbar__overflow"
icon="chevron-up"
type="tool"
data-testid="tools.more"
title={msg('tool-panel.more')}
/>
@ -220,13 +219,14 @@ const OverflowToolsContent = track(function OverflowToolsContent({
const msg = useTranslation()
return (
<div className="tlui-button-grid__four tlui-button-grid__reverse">
<div className="tlui-buttons__grid">
{toolbarItems.map(({ toolItem: { id, meta, kbd, label, onSelect, icon } }) => {
return (
<M.Item
key={id}
type="icon"
className="tlui-button-grid__button"
data-testid={`tools.${id}`}
data-testid={`tools.more.${id}`}
data-tool={id}
data-geo={meta?.geo ?? ''}
aria-label={label}
@ -251,7 +251,7 @@ function ToolbarButton({
}) {
return (
<Button
className="tlui-toolbar__tools__button"
type="tool"
data-testid={`tools.${item.id}`}
data-tool={item.id}
data-geo={item.meta?.geo ?? ''}

View file

@ -18,6 +18,7 @@ export const TrashButton = track(function TrashButton() {
return (
<Button
icon={action.icon}
type="icon"
onClick={() => action.onSelect('quick-actions')}
disabled={!(editor.isIn('select') && editor.selectedShapeIds.length > 0)}
title={`${msg(action.label!)} ${kbdStr(action.kbd!)}`}

View file

@ -16,6 +16,7 @@ export const UndoButton = memo(function UndoButton() {
<Button
data-testid="main.undo"
icon={undo.icon}
type="icon"
title={`${msg(undo.label!)} ${kbdStr(undo.kbd!)}`}
disabled={!canUndo}
onClick={() => undo.onSelect('quick-actions')}

View file

@ -19,7 +19,7 @@ export interface TLUiButtonProps extends React.HTMLAttributes<HTMLButtonElement>
kbd?: string
isChecked?: boolean
invertIcon?: boolean
type?: 'primary' | 'danger' | 'normal'
type: 'normal' | 'primary' | 'danger' | 'low' | 'icon' | 'tool' | 'menu' | 'help'
}
/** @public */
@ -32,7 +32,7 @@ export const Button = React.forwardRef<HTMLButtonElement, TLUiButtonProps>(funct
smallIcon,
kbd,
isChecked = false,
type = 'normal',
type,
children,
spinner,
...props
@ -51,10 +51,10 @@ export const Button = React.forwardRef<HTMLButtonElement, TLUiButtonProps>(funct
title={props.title ?? labelStr}
className={classnames('tlui-button', `tlui-button__${type}`, props.className)}
>
{iconLeft && <Icon icon={iconLeft} className="tlui-icon-left" small />}
{iconLeft && <Icon icon={iconLeft} className="tlui-button__icon-left" small />}
{children}
{label && (
<span draggable={false}>
<span className="tlui-button__label" draggable={false}>
{labelStr}
{isChecked && <Icon icon="check" />}
</span>

View file

@ -3,7 +3,6 @@ import {
SharedStyle,
StyleProp,
TLDefaultColorStyle,
clamp,
getDefaultColorTheme,
useEditor,
useValue,
@ -24,7 +23,6 @@ export interface ButtonPickerProps<T extends string> {
style: StyleProp<T>
value: SharedStyle<T>
items: StyleValuesForUi<T>
columns?: 2 | 3 | 4
onValueChange: (style: StyleProp<T>, value: T, squashing: boolean) => void
}
@ -35,7 +33,7 @@ function _ButtonPicker<T extends string>(props: ButtonPickerProps<T>) {
title,
style,
value,
columns = clamp(items.length, 2, 4),
// columns = clamp(items.length, 2, 4),
onValueChange,
} = props
const editor = useEditor()
@ -99,15 +97,10 @@ function _ButtonPicker<T extends string>(props: ButtonPickerProps<T>) {
)
return (
<div
className={classNames('tlui-button-grid', {
'tlui-button-grid__two': columns === 2,
'tlui-button-grid__three': columns === 3,
'tlui-button-grid__four': columns === 4,
})}
>
<div className={classNames('tlui-buttons__grid')}>
{items.map((item) => (
<Button
type="icon"
key={item.value}
data-id={item.value}
data-testid={`style.${uiType}.${item.value}`}

View file

@ -22,7 +22,11 @@ export function CloseButton() {
return (
<div className="tlui-dialog__header__close">
<_Dialog.DialogClose data-testid="dialog.close" dir="ltr" asChild>
<Button aria-label="Close" onTouchEnd={(e) => (e.target as HTMLButtonElement).click()}>
<Button
type="icon"
aria-label="Close"
onTouchEnd={(e) => (e.target as HTMLButtonElement).click()}
>
<Icon small icon="cross-2" />
</Button>
</_Dialog.DialogClose>

View file

@ -10,15 +10,17 @@ export function Root({
id,
children,
modal = false,
debugOpen = false,
}: {
id: string
children: any
modal?: boolean
debugOpen?: boolean
}) {
const [open, onOpenChange] = useMenuIsOpen(id)
return (
<DropdownMenu.Root open={open} dir="ltr" modal={modal} onOpenChange={onOpenChange}>
<DropdownMenu.Root open={debugOpen || open} dir="ltr" modal={modal} onOpenChange={onOpenChange}>
{children}
</DropdownMenu.Root>
)
@ -101,7 +103,8 @@ export function SubTrigger({
return (
<DropdownMenu.SubTrigger dir="ltr" data-direction={dataDirection} data-testid={testId} asChild>
<Button
className="tlui-menu__button tlui-menu__submenu__trigger"
type="menu"
className="tlui-menu__submenu__trigger"
label={label}
icon="chevron-right"
/>
@ -171,7 +174,7 @@ export function Item({ noClose, ...props }: DropdownMenuItemProps) {
asChild
onClick={noClose || props.isChecked !== undefined ? preventDefault : undefined}
>
<Button className="tlui-menu__button" {...props} />
<Button {...props} />
</DropdownMenu.Item>
)
}
@ -190,23 +193,14 @@ export function CheckboxItem({ children, onSelect, ...rest }: DropdownMenuCheckb
return (
<DropdownMenu.CheckboxItem
dir="ltr"
className="tlui-button tlui-menu__button tlui-menu__checkbox-item"
className="tlui-button tlui-button__menu tlui-button__checkbox"
onSelect={(e) => {
onSelect?.(e)
preventDefault(e)
}}
{...rest}
>
<div
className="tlui-menu__checkbox-item__check"
style={{
transformOrigin: '75% center',
transform: `scale(${rest.checked ? 1 : 0.5})`,
opacity: rest.checked ? 1 : 0.5,
}}
>
<Icon small icon={rest.checked ? 'check' : 'checkbox-empty'} />
</div>
{children}
</DropdownMenu.CheckboxItem>
)
@ -217,16 +211,18 @@ export function RadioItem({ children, onSelect, ...rest }: DropdownMenuCheckboxI
return (
<DropdownMenu.CheckboxItem
dir="ltr"
className="tlui-button tlui-menu__button tlui-menu__checkbox-item"
className="tlui-button tlui-button__menu tlui-button__checkbox"
onSelect={(e) => {
onSelect?.(e)
preventDefault(e)
}}
{...rest}
>
<DropdownMenu.ItemIndicator dir="ltr" className="tlui-menu__checkbox-item__check">
<Icon icon="check" />
<div className="tlui-button__checkbox__indicator">
<DropdownMenu.ItemIndicator dir="ltr">
<Icon icon="check" small />
</DropdownMenu.ItemIndicator>
</div>
{children}
</DropdownMenu.CheckboxItem>
)

View file

@ -10,11 +10,14 @@ type PopoverProps = {
onOpenChange?: (isOpen: boolean) => void
}
export const Popover: FC<PopoverProps> = ({ id, children, onOpenChange }) => {
export const Popover: FC<PopoverProps> = ({ id, children, onOpenChange, open }) => {
const [isOpen, handleOpenChange] = useMenuIsOpen(id, onOpenChange)
return (
<PopoverPrimitive.Root onOpenChange={handleOpenChange} open={isOpen}>
<PopoverPrimitive.Root
onOpenChange={handleOpenChange}
open={open || isOpen /* allow debugging */}
>
<div className="tlui-popover">{children}</div>
</PopoverPrimitive.Root>
)

View file

@ -35,6 +35,8 @@ export function useMenuIsOpen(id: string, cb?: (isOpen: boolean) => void) {
[editor, id, cb]
)
const isOpen = useValue('is menu open', () => editor.openMenus.includes(id), [editor, id])
useEffect(() => {
// When the effect runs, if the menu is open then
// add it to the open menus list.
@ -67,7 +69,5 @@ export function useMenuIsOpen(id: string, cb?: (isOpen: boolean) => void) {
}
}, [editor, id, trackEvent])
const isOpen = useValue('is menu open', () => editor.openMenus.includes(id), [editor, id])
return [isOpen, onOpenChange] as const
}

View file

@ -1,10 +1,11 @@
import { Editor, uniqueId } from '@tldraw/editor'
import { createContext, useCallback, useContext, useState } from 'react'
import { TLUiIconType } from '../icon-types'
/** @public */
export interface TLUiToast {
id: string
icon?: string
icon?: TLUiIconType
title?: string
description?: string
actions?: TLUiToastAction[]
@ -14,7 +15,7 @@ export interface TLUiToast {
/** @public */
export interface TLUiToastAction {
type: 'primary' | 'secondary' | 'warn'
type: 'primary' | 'danger' | 'normal'
label: string
onClick: () => void
}

View file

@ -302,7 +302,7 @@ export const DEFAULT_TRANSLATION = {
'style-panel.align': 'Align',
'style-panel.vertical-align': 'Vertical align',
'style-panel.position': 'Position',
'style-panel.arrowheads': 'Arrowheads',
'style-panel.arrowheads': 'Arrows',
'style-panel.arrowhead-start': 'Start',
'style-panel.arrowhead-end': 'End',
'style-panel.color': 'Color',

View file

@ -111,3 +111,7 @@ export function useTranslation() {
[translation]
)
}
export function untranslated(string: string) {
return string as TLUiTranslationKey
}