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(() => { await page.evaluate(() => {
editor.user.updateUserPreferences({ animationSpeed: 0 }) editor.user.updateUserPreferences({ animationSpeed: 0 })
}) })
await page.mouse.move(50, 50)
} }
export async function setupPageWithShapes(page: PlaywrightTestArgs['page']) { export async function setupPageWithShapes(page: PlaywrightTestArgs['page']) {

View file

@ -221,31 +221,27 @@ test.describe('Export snapshots', () => {
}) })
async function snapshotTest(page: Page) { async function snapshotTest(page: Page) {
const downloadEvent = page.waitForEvent('download') page.waitForEvent('download').then(async (download) => {
await page.click('[data-testid="main.menu"]') const path = (await download.path()) as string
await page.click('[data-testid="menu-item.edit"]') assert(path)
await page.click('[data-testid="menu-item.export-as"]') await rename(path, path + '.svg')
await page.click('[data-testid="menu-item.export-as-svg"]') await writeFile(
path + '.html',
`
<!DOCTYPE html>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<img src="${path}.svg" />
`,
'utf-8'
)
const download = await downloadEvent await page.goto(`file://${path}.html`)
const path = (await download.path()) as string const clip = await page.$eval('img', (img) => img.getBoundingClientRect())
assert(path) await expect(page).toHaveScreenshot({
await rename(path, path + '.svg') omitBackground: true,
await writeFile( clip,
path + '.html', })
`
<!DOCTYPE html>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<img src="${path}.svg" />
`,
'utf-8'
)
await page.goto(`file://${path}.html`)
const clip = await page.$eval('img', (img) => img.getBoundingClientRect())
await expect(page).toHaveScreenshot({
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())) { if (!(await page.getByTestId(`tools.more`).isVisible())) {
throw Error(`Tool more is not visible`) throw Error(`Tool more is not visible`)
} }
await page.getByTestId('tools.more').click() 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())) { if (!(await page.getByTestId(`tools.${tool}`).isVisible())) {
@ -106,6 +112,8 @@ test.describe('Shape Tools', () => {
// Find and click the button // Find and click the button
if (!(await page.getByTestId(`tools.${tool}`).isVisible())) { if (!(await page.getByTestId(`tools.${tool}`).isVisible())) {
await page.getByTestId('tools.more').click() 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() await page.getByTestId(`tools.${tool}`).click()
@ -124,7 +132,7 @@ test.describe('Shape Tools', () => {
expect(await getAllShapeTypes(page)).toEqual([shape]) expect(await getAllShapeTypes(page)).toEqual([shape])
// Reset for next time // 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('v') // go to the select tool
await page.keyboard.press('Control+a') await page.keyboard.press('Control+a')
await page.keyboard.press('Backspace') await page.keyboard.press('Backspace')
@ -142,7 +150,10 @@ test.describe('Shape Tools', () => {
// Find and click the button // Find and click the button
if (!(await page.getByTestId(`tools.${tool}`).isVisible())) { if (!(await page.getByTestId(`tools.${tool}`).isVisible())) {
await page.getByTestId('tools.more').click() 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() await page.getByTestId(`tools.${tool}`).click()
// Button should be selected // Button should be selected
@ -163,7 +174,7 @@ test.describe('Shape Tools', () => {
expect(await getAllShapeTypes(page)).toEqual([shape]) expect(await getAllShapeTypes(page)).toEqual([shape])
// Reset for next time // 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('v')
await page.keyboard.press('Control+a') await page.keyboard.press('Control+a')
await page.keyboard.press('Backspace') await page.keyboard.press('Backspace')

View file

@ -71,7 +71,8 @@ test.describe('smoke tests', () => {
expect(await getSelectedShapeColor()).toBe('black') expect(await getSelectedShapeColor()).toBe('black')
// when on a mobile device... // 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) { if (hasMobileMenu) {
// open the style menu // 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 '@tldraw/tldraw/tldraw.css'
import { useEffect } from 'react'
;(window as any).__tldraw_ui_event = { id: 'NOTHING_YET' } ;(window as any).__tldraw_ui_event = { id: 'NOTHING_YET' }
;(window as any).__tldraw_editor_events = [] ;(window as any).__tldraw_editor_events = []
@ -15,7 +16,19 @@ export default function EndToEnd() {
onUiEvent={(name, data) => { onUiEvent={(name, data) => {
;(window as any).__tldraw_ui_event = { name, data } ;(window as any).__tldraw_ui_event = { name, data }
}} }}
/> >
<SneakyExportButton />
</Tldraw>
</div> </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"> <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> </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.align": "Align",
"style-panel.vertical-align": "Vertical align", "style-panel.vertical-align": "Vertical align",
"style-panel.position": "Position", "style-panel.position": "Position",
"style-panel.arrowheads": "Arrowheads", "style-panel.arrowheads": "Arrows",
"style-panel.arrowhead-start": "Start", "style-panel.arrowhead-start": "Start",
"style-panel.arrowhead-end": "End", "style-panel.arrowhead-end": "End",
"style-panel.color": "Color", "style-panel.color": "Color",

View file

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

View file

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

View file

@ -4137,11 +4137,11 @@
"excerptTokens": [ "excerptTokens": [
{ {
"kind": "Content", "kind": "Content",
"text": "export declare function Root({ id, children, modal, }: " "text": "export declare function Root({ id, children, modal, debugOpen, }: "
}, },
{ {
"kind": "Content", "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", "kind": "Content",
@ -4166,7 +4166,7 @@
"overloadIndex": 1, "overloadIndex": 1,
"parameters": [ "parameters": [
{ {
"parameterName": "{ id, children, modal, }", "parameterName": "{ id, children, modal, debugOpen, }",
"parameterTypeTokenRange": { "parameterTypeTokenRange": {
"startIndex": 1, "startIndex": 1,
"endIndex": 2 "endIndex": 2
@ -14545,11 +14545,11 @@
"excerptTokens": [ "excerptTokens": [
{ {
"kind": "Content", "kind": "Content",
"text": "type?: " "text": "type: "
}, },
{ {
"kind": "Content", "kind": "Content",
"text": "'danger' | 'normal' | 'primary'" "text": "'danger' | 'help' | 'icon' | 'low' | 'menu' | 'normal' | 'primary' | 'tool'"
}, },
{ {
"kind": "Content", "kind": "Content",
@ -14557,7 +14557,7 @@
} }
], ],
"isReadonly": false, "isReadonly": false,
"isOptional": true, "isOptional": false,
"releaseTag": "Public", "releaseTag": "Public",
"name": "type", "name": "type",
"propertyTypeTokenRange": { "propertyTypeTokenRange": {
@ -14583,7 +14583,7 @@
"text": "export interface TLUiContextMenuProps " "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", "releaseTag": "Public",
"name": "TLUiContextMenuProps", "name": "TLUiContextMenuProps",
"preserveMemberOrder": false, "preserveMemberOrder": false,
@ -14606,7 +14606,6 @@
"text": ";" "text": ";"
} }
], ],
"fileUrlPath": "packages/tldraw/src/lib/ui/components/ContextMenu.tsx",
"isReadonly": false, "isReadonly": false,
"isOptional": false, "isOptional": false,
"releaseTag": "Public", "releaseTag": "Public",
@ -16746,8 +16745,9 @@
"text": "icon?: " "text": "icon?: "
}, },
{ {
"kind": "Content", "kind": "Reference",
"text": "string" "text": "TLUiIconType",
"canonicalReference": "@tldraw/tldraw!TLUiIconType:type"
}, },
{ {
"kind": "Content", "kind": "Content",
@ -16927,7 +16927,7 @@
}, },
{ {
"kind": "Content", "kind": "Content",
"text": "'primary' | 'secondary' | 'warn'" "text": "'danger' | 'normal' | 'primary'"
}, },
{ {
"kind": "Content", "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 ? ( {isFocusMode ? (
<div className="tlui-layout__top"> <div className="tlui-layout__top">
<Button <Button
type="icon"
className="tlui-focus-button" className="tlui-focus-button"
title={`${msg('focus-mode.toggle-focus-mode')}`} title={`${msg('focus-mode.toggle-focus-mode')}`}
icon="dot" icon="dot"

View file

@ -25,9 +25,9 @@ export const ActionsMenu = memo(function ActionsMenu() {
return ( return (
<Button <Button
key={id} key={id}
className="tlui-button-grid__button"
data-testid={`menu-item.${item.id}`} data-testid={`menu-item.${item.id}`}
icon={icon} icon={icon}
type="icon"
title={ title={
label label
? kbd ? kbd
@ -46,13 +46,14 @@ export const ActionsMenu = memo(function ActionsMenu() {
} }
return ( return (
<Popover id="actions menu"> <Popover id="actions-menu">
<PopoverTrigger> <PopoverTrigger>
<Button <Button
className="tlui-menu__trigger" className="tlui-menu__trigger"
data-testid="main.action-menu" data-testid="main.action-menu"
icon="dots-vertical" icon="dots-vertical"
title={msg('actions-menu.title')} title={msg('actions-menu.title')}
type="icon" // needs to be here because the trigger also passes down type="button"
smallIcon smallIcon
/> />
</PopoverTrigger> </PopoverTrigger>
@ -63,7 +64,7 @@ export const ActionsMenu = memo(function ActionsMenu() {
dir="ltr" dir="ltr"
sideOffset={6} sideOffset={6}
> >
<div className="tlui-actions-menu tlui-button-grid__four"> <div className="tlui-actions-menu tlui-buttons__grid">
{menuSchema.map(getActionMenuItem)} {menuSchema.map(getActionMenuItem)}
</div> </div>
</PopoverPrimitive.Content> </PopoverPrimitive.Content>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -26,23 +26,25 @@ export const NavigationZone = memo(function NavigationZone() {
return ( return (
<div className="tlui-navigation-zone"> <div className="tlui-navigation-zone">
<div className="tlui-navigation-zone__controls"> <div className="tlui-buttons__horizontal">
{breakpoint < 6 ? ( {breakpoint < 6 ? (
<ZoomMenu /> <ZoomMenu />
) : collapsed ? ( ) : collapsed ? (
<> <>
<ZoomMenu /> <ZoomMenu />
<Button <Button
type="icon"
icon={collapsed ? 'chevrons-ne' : 'chevrons-sw'}
data-testid="minimap.toggle"
title={msg('navigation-zone.toggle-minimap')} title={msg('navigation-zone.toggle-minimap')}
className="tlui-navigation-zone__toggle" className="tlui-navigation-zone__toggle"
data-testid="minimap.toggle"
onClick={toggleMinimap} onClick={toggleMinimap}
icon={collapsed ? 'chevrons-ne' : 'chevrons-sw'}
/> />
</> </>
) : ( ) : (
<> <>
<Button <Button
type="icon"
icon="minus" icon="minus"
data-testid="minimap.zoom-out" data-testid="minimap.zoom-out"
title={`${msg(actions['zoom-out'].label!)} ${kbdStr(actions['zoom-out'].kbd!)}`} title={`${msg(actions['zoom-out'].label!)} ${kbdStr(actions['zoom-out'].kbd!)}`}
@ -50,16 +52,19 @@ export const NavigationZone = memo(function NavigationZone() {
/> />
<ZoomMenu /> <ZoomMenu />
<Button <Button
type="icon"
icon="plus" icon="plus"
data-testid="minimap.zoom-in" data-testid="minimap.zoom-in"
title={`${msg(actions['zoom-in'].label!)} ${kbdStr(actions['zoom-in'].kbd!)}`} title={`${msg(actions['zoom-in'].label!)} ${kbdStr(actions['zoom-in'].kbd!)}`}
onClick={() => actions['zoom-in'].onSelect('navigation-zone')} onClick={() => actions['zoom-in'].onSelect('navigation-zone')}
/> />
<Button <Button
type="icon"
icon={collapsed ? 'chevrons-ne' : 'chevrons-sw'}
data-testid="minimap.toggle"
title={msg('navigation-zone.toggle-minimap')} title={msg('navigation-zone.toggle-minimap')}
className="tlui-navigation-zone__toggle" className="tlui-navigation-zone__toggle"
onClick={toggleMinimap} 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.Root id="zoom">
<M.Trigger> <M.Trigger>
<Button <Button
type="icon"
title={`${msg('navigation-zone.zoom')}`} title={`${msg('navigation-zone.zoom')}`}
data-testid="minimap.zoom-menu" data-testid="minimap.zoom-menu"
className={breakpoint < 5 ? 'tlui-zoom-menu__button' : 'tlui-zoom-menu__button__pct'} className={breakpoint < 5 ? 'tlui-zoom-menu__button' : 'tlui-zoom-menu__button__pct'}
@ -74,6 +75,7 @@ function ZoomMenuItem(props: {
return ( return (
<M.Item <M.Item
type="menu"
label={actions[action].label} label={actions[action].label}
kbd={actions[action].kbd} kbd={actions[action].kbd}
data-testid={props['data-testid']} data-testid={props['data-testid']}

View file

@ -45,13 +45,13 @@ export const PageItemSubmenu = track(function PageItemSubmenu({
return ( return (
<M.Root id={`page item submenu ${index}`}> <M.Root id={`page item submenu ${index}`}>
<M.Trigger> <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.Trigger>
<M.Content alignOffset={0}> <M.Content alignOffset={0}>
<M.Group> <M.Group>
{onRename && ( {onRename && (
<DropdownMenu.Item dir="ltr" onSelect={onRename} asChild> <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>
)} )}
<DropdownMenu.Item <DropdownMenu.Item
@ -60,23 +60,23 @@ export const PageItemSubmenu = track(function PageItemSubmenu({
disabled={pages.length >= MAX_PAGES} disabled={pages.length >= MAX_PAGES}
asChild asChild
> >
<Button className="tlui-menu__button" label="page-menu.submenu.duplicate-page" /> <Button type="menu" label="page-menu.submenu.duplicate-page" />
</DropdownMenu.Item> </DropdownMenu.Item>
{index > 0 && ( {index > 0 && (
<DropdownMenu.Item dir="ltr" onSelect={onMoveUp} asChild> <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> </DropdownMenu.Item>
)} )}
{index < listSize - 1 && ( {index < listSize - 1 && (
<DropdownMenu.Item dir="ltr" onSelect={onMoveDown} asChild> <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> </DropdownMenu.Item>
)} )}
</M.Group> </M.Group>
{listSize > 1 && ( {listSize > 1 && (
<M.Group> <M.Group>
<DropdownMenu.Item dir="ltr" onSelect={onDelete} asChild> <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> </DropdownMenu.Item>
</M.Group> </M.Group>
)} )}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,5 @@
import { Trigger } from '@radix-ui/react-dropdown-menu' import { Trigger } from '@radix-ui/react-dropdown-menu'
import { SharedStyle, StyleProp, preventDefault } from '@tldraw/editor' import { SharedStyle, StyleProp, preventDefault } from '@tldraw/editor'
import classNames from 'classnames'
import * as React from 'react' import * as React from 'react'
import { TLUiTranslationKey } from '../../hooks/useTranslation/TLUiTranslationKey' import { TLUiTranslationKey } from '../../hooks/useTranslation/TLUiTranslationKey'
import { useTranslation } from '../../hooks/useTranslation/useTranslation' import { useTranslation } from '../../hooks/useTranslation/useTranslation'
@ -60,98 +59,92 @@ export const DoubleDropdownPicker = React.memo(function DoubleDropdownPicker<T e
<div title={msg(label)} className="tlui-style-panel__double-select-picker-label"> <div title={msg(label)} className="tlui-style-panel__double-select-picker-label">
{msg(label)} {msg(label)}
</div> </div>
<DropdownMenu.Root id={`style panel ${uiTypeA} A`}> <div className="tlui-buttons__horizontal">
<Trigger <DropdownMenu.Root id={`style panel ${uiTypeA} A`}>
asChild <Trigger
// Firefox fix: Stop the dropdown immediately closing after touch asChild
onTouchEnd={(e) => preventDefault(e)} // Firefox fix: Stop the dropdown immediately closing after touch
> onTouchEnd={(e) => preventDefault(e)}
<Button
data-testid={`style.${uiTypeA}`}
title={
msg(labelA) +
' — ' +
(valueA === null
? msg('style-panel.mixed')
: msg(`${uiTypeA}-style.${valueA}` as TLUiTranslationKey))
}
icon={iconA as any}
invertIcon
smallIcon
/>
</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,
})}
> >
{itemsA.map((item) => { <Button
return ( type="icon"
<DropdownMenu.Item data-testid={`style.${uiTypeA}`}
className="tlui-button-grid__button" title={
title={ msg(labelA) +
msg(labelA) + ' — ' +
' — ' + (valueA === null
msg(`${uiTypeA}-style.${item.value}` as TLUiTranslationKey) ? msg('style-panel.mixed')
} : msg(`${uiTypeA}-style.${valueA}` as TLUiTranslationKey))
data-testid={`style.${uiTypeA}.${item.value}`} }
key={item.value} icon={iconA as any}
icon={item.icon as TLUiIconType} invertIcon
onClick={() => onValueChange(styleA, item.value, false)} smallIcon
invertIcon />
/> </Trigger>
) <DropdownMenu.Content side="bottom" align="end" sideOffset={0} alignOffset={-2}>
})} <div className="tlui-buttons__grid">
</div> {itemsA.map((item) => {
</DropdownMenu.Content> return (
</DropdownMenu.Root> <DropdownMenu.Item
<DropdownMenu.Root id={`style panel ${uiTypeB}`}> type="icon"
<Trigger title={
asChild msg(labelA) +
// Firefox fix: Stop the dropdown immediately closing after touch ' — ' +
onTouchEnd={(e) => preventDefault(e)} msg(`${uiTypeA}-style.${item.value}` as TLUiTranslationKey)
> }
<Button data-testid={`style.${uiTypeA}.${item.value}`}
data-testid={`style.${uiTypeB}`} key={item.value}
title={ icon={item.icon as TLUiIconType}
msg(labelB) + onClick={() => onValueChange(styleA, item.value, false)}
' — ' + invertIcon
(valueB === null />
? msg('style-panel.mixed') )
: msg(`${uiTypeB}-style.${valueB}` as TLUiTranslationKey)) })}
} </div>
icon={iconB as any} </DropdownMenu.Content>
smallIcon </DropdownMenu.Root>
/> <DropdownMenu.Root id={`style panel ${uiTypeB}`}>
</Trigger> <Trigger
<DropdownMenu.Content side="bottom" align="end" sideOffset={0} alignOffset={-2}> asChild
<div // Firefox fix: Stop the dropdown immediately closing after touch
className={classNames('tlui-button-grid', { onTouchEnd={(e) => preventDefault(e)}
'tlui-button-grid__two': itemsA.length < 4,
'tlui-button-grid__four': itemsA.length >= 4,
})}
> >
{itemsB.map((item) => { <Button
return ( type="icon"
<DropdownMenu.Item data-testid={`style.${uiTypeB}`}
className="tlui-button-grid__button" title={
title={ msg(labelB) +
msg(labelB) + ' — ' +
' — ' + (valueB === null
msg(`${uiTypeB}-style.${item.value}` as TLUiTranslationKey) ? msg('style-panel.mixed')
} : msg(`${uiTypeB}-style.${valueB}` as TLUiTranslationKey))
data-testid={`style.${uiTypeB}.${item.value}`} }
key={item.value} icon={iconB as any}
icon={item.icon as TLUiIconType} smallIcon
onClick={() => onValueChange(styleB, item.value, false)} />
/> </Trigger>
) <DropdownMenu.Content side="bottom" align="end" sideOffset={0} alignOffset={-2}>
})} <div className="tlui-buttons__grid">
</div> {itemsB.map((item) => {
</DropdownMenu.Content> return (
</DropdownMenu.Root> <DropdownMenu.Item
type="icon"
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)}
/>
)
})}
</div>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
</div> </div>
) )
}) })

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -22,7 +22,11 @@ export function CloseButton() {
return ( return (
<div className="tlui-dialog__header__close"> <div className="tlui-dialog__header__close">
<_Dialog.DialogClose data-testid="dialog.close" dir="ltr" asChild> <_Dialog.DialogClose data-testid="dialog.close" dir="ltr" asChild>
<Button 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" /> <Icon small icon="cross-2" />
</Button> </Button>
</_Dialog.DialogClose> </_Dialog.DialogClose>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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