feat: add help panel (#816)
* feat: add help panel * feat: added all the shortcuts and responsive * improve help panel * add modal for the shortcut * add grid * fix language menu * add responsive grid * Styling keyboard shortcuts / panel * fix links ts issue * Improve styling * Fix translation bug Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
This commit is contained in:
parent
240202bb81
commit
77337b1281
18 changed files with 516 additions and 81 deletions
|
@ -43,8 +43,10 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-alert-dialog": "^0.1.7",
|
"@radix-ui/react-alert-dialog": "^0.1.7",
|
||||||
"@radix-ui/react-context-menu": "^0.1.6",
|
"@radix-ui/react-context-menu": "^0.1.6",
|
||||||
|
"@radix-ui/react-dialog": "^0.1.7",
|
||||||
"@radix-ui/react-dropdown-menu": "^0.1.6",
|
"@radix-ui/react-dropdown-menu": "^0.1.6",
|
||||||
"@radix-ui/react-icons": "^1.1.1",
|
"@radix-ui/react-icons": "^1.1.1",
|
||||||
|
"@radix-ui/react-popover": "^0.1.6",
|
||||||
"@radix-ui/react-tooltip": "^0.1.7",
|
"@radix-ui/react-tooltip": "^0.1.7",
|
||||||
"@stitches/react": "^1.2.8",
|
"@stitches/react": "^1.2.8",
|
||||||
"@tldraw/core": "^1.14.1",
|
"@tldraw/core": "^1.14.1",
|
||||||
|
|
|
@ -9,6 +9,7 @@ export interface DMContentProps {
|
||||||
align?: 'start' | 'center' | 'end'
|
align?: 'start' | 'center' | 'end'
|
||||||
sideOffset?: number
|
sideOffset?: number
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
|
overflow?: boolean
|
||||||
id?: string
|
id?: string
|
||||||
side?: 'top' | 'left' | 'right' | 'bottom' | undefined
|
side?: 'top' | 'left' | 'right' | 'bottom' | undefined
|
||||||
}
|
}
|
||||||
|
@ -19,6 +20,7 @@ export function DMContent({
|
||||||
align,
|
align,
|
||||||
variant,
|
variant,
|
||||||
id,
|
id,
|
||||||
|
overflow = false,
|
||||||
side = 'bottom',
|
side = 'bottom',
|
||||||
}: DMContentProps) {
|
}: DMContentProps) {
|
||||||
return (
|
return (
|
||||||
|
@ -31,7 +33,9 @@ export function DMContent({
|
||||||
id={id}
|
id={id}
|
||||||
side={side}
|
side={side}
|
||||||
>
|
>
|
||||||
<StyledContent variant={variant}>{children}</StyledContent>
|
<StyledContent variant={variant} overflow={overflow}>
|
||||||
|
{children}
|
||||||
|
</StyledContent>
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -40,11 +44,14 @@ export const StyledContent = styled(MenuContent, {
|
||||||
width: 'fit-content',
|
width: 'fit-content',
|
||||||
height: 'fit-content',
|
height: 'fit-content',
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
maxHeight: '75vh',
|
maxHeight: '100vh',
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
'& *': {
|
overflowX: 'hidden',
|
||||||
boxSizing: 'border-box',
|
'&::webkit-scrollbar': {
|
||||||
|
display: 'none',
|
||||||
},
|
},
|
||||||
|
'-ms-overflow-style': 'none' /* for Internet Explorer, Edge */,
|
||||||
|
scrollbarWidth: 'none',
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
horizontal: {
|
horizontal: {
|
||||||
|
@ -54,5 +61,10 @@ export const StyledContent = styled(MenuContent, {
|
||||||
minWidth: 128,
|
minWidth: 128,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
overflow: {
|
||||||
|
true: {
|
||||||
|
maxHeight: '60vh',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -8,10 +8,18 @@ export interface DMSubMenuProps {
|
||||||
size?: 'small'
|
size?: 'small'
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
|
overflow?: boolean
|
||||||
id?: string
|
id?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DMSubMenu({ children, size, disabled = false, label, id }: DMSubMenuProps) {
|
export function DMSubMenu({
|
||||||
|
children,
|
||||||
|
size,
|
||||||
|
overflow = false,
|
||||||
|
disabled = false,
|
||||||
|
label,
|
||||||
|
id,
|
||||||
|
}: DMSubMenuProps) {
|
||||||
return (
|
return (
|
||||||
<span id={id}>
|
<span id={id}>
|
||||||
<Root dir="ltr">
|
<Root dir="ltr">
|
||||||
|
@ -21,7 +29,7 @@ export function DMSubMenu({ children, size, disabled = false, label, id }: DMSub
|
||||||
</RowButton>
|
</RowButton>
|
||||||
</TriggerItem>
|
</TriggerItem>
|
||||||
<Content dir="ltr" asChild sideOffset={2} alignOffset={-2} align="start">
|
<Content dir="ltr" asChild sideOffset={2} alignOffset={-2} align="start">
|
||||||
<MenuContent size={size} overflow>
|
<MenuContent size={size} overflow={overflow}>
|
||||||
{children}
|
{children}
|
||||||
<Arrow offset={13} />
|
<Arrow offset={13} />
|
||||||
</MenuContent>
|
</MenuContent>
|
||||||
|
|
|
@ -14,6 +14,14 @@ export const MenuContent = styled('div', {
|
||||||
padding: '$2 $2',
|
padding: '$2 $2',
|
||||||
borderRadius: '$3',
|
borderRadius: '$3',
|
||||||
font: '$ui',
|
font: '$ui',
|
||||||
|
maxHeight: '100vh',
|
||||||
|
overflowY: 'auto',
|
||||||
|
overflowX: 'hidden',
|
||||||
|
'&::webkit-scrollbar': {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
'-ms-overflow-style': 'none' /* for Internet Explorer, Edge */,
|
||||||
|
scrollbarWidth: 'none',
|
||||||
variants: {
|
variants: {
|
||||||
size: {
|
size: {
|
||||||
small: {
|
small: {
|
||||||
|
@ -23,14 +31,7 @@ export const MenuContent = styled('div', {
|
||||||
overflow: {
|
overflow: {
|
||||||
true: {
|
true: {
|
||||||
maxHeight: '60vh',
|
maxHeight: '60vh',
|
||||||
overflowY: 'auto',
|
|
||||||
overflowX: 'hidden',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'-ms-overflow-style': 'none' /* for Internet Explorer, Edge */,
|
|
||||||
scrollbarWidth: 'none',
|
|
||||||
'&::webkit-scrollbar': {
|
|
||||||
display: 'none',
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
export function QuestionMarkIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="8"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 4 8"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M1.15948 5.85192C1.05082 5.66856 0.972717 5.50556 0.925178 5.36295C0.884431 5.21354 0.864057 5.06074 0.864057 4.90454C0.864057 4.69401 0.921782 4.50046 1.03723 4.32389C1.15269 4.14731 1.2953 3.98093 1.46508 3.82473C1.63486 3.66174 1.80125 3.50214 1.96424 3.34594C2.13402 3.18295 2.27664 3.01317 2.39209 2.8366C2.50754 2.65324 2.56527 2.45629 2.56527 2.24576C2.56527 1.98769 2.48038 1.78056 2.31059 1.62436C2.1476 1.46137 1.93028 1.37988 1.65863 1.37988C1.48206 1.37988 1.32586 1.41044 1.19004 1.47156C1.05421 1.52589 0.911596 1.55305 0.762188 1.55305C0.572033 1.55305 0.429416 1.50551 0.334339 1.41044C0.239261 1.31536 0.191722 1.21009 0.191722 1.09464C0.191722 0.958818 0.252844 0.843366 0.375086 0.748289C0.50412 0.64642 0.701067 0.595485 0.965926 0.595485C1.30549 0.595485 1.6111 0.64642 1.88275 0.748289C2.1544 0.850157 2.38869 0.996169 2.58564 1.18632C2.78938 1.36969 2.94218 1.5904 3.04405 1.84847C3.15271 2.10654 3.20704 2.39517 3.20704 2.71436C3.20704 2.98601 3.14932 3.22031 3.03386 3.41725C2.91841 3.60741 2.7758 3.77719 2.60601 3.9266C2.44302 4.07601 2.27664 4.21862 2.10686 4.35445C1.93708 4.49027 1.79446 4.63628 1.67901 4.79248C1.56356 4.94189 1.50583 5.12186 1.50583 5.33239C1.50583 5.42067 1.50923 5.50896 1.51602 5.59725C1.5296 5.68553 1.54658 5.77042 1.56695 5.85192H1.15948ZM1.3734 7.8078C1.25116 7.8078 1.1391 7.78064 1.03723 7.72631C0.942156 7.67198 0.867452 7.59727 0.813122 7.50219C0.758792 7.40712 0.731627 7.30185 0.731627 7.1864C0.731627 7.06416 0.758792 6.95889 0.813122 6.87061C0.867452 6.77553 0.942156 6.70083 1.03723 6.6465C1.1391 6.59217 1.25116 6.565 1.3734 6.565C1.55677 6.565 1.70957 6.62612 1.83181 6.74837C1.95405 6.86382 2.01518 7.00983 2.01518 7.1864C2.01518 7.36297 1.95405 7.51238 1.83181 7.63462C1.70957 7.75008 1.55677 7.8078 1.3734 7.8078Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
|
@ -15,3 +15,4 @@ export * from './EraserIcon'
|
||||||
export * from './MultiplayerIcon'
|
export * from './MultiplayerIcon'
|
||||||
export * from './DiscordIcon'
|
export * from './DiscordIcon'
|
||||||
export * from './LineIcon'
|
export * from './LineIcon'
|
||||||
|
export * from './QuestionMarkIcon'
|
||||||
|
|
|
@ -51,9 +51,6 @@ const BackToContentContainer = styled(MenuContent, {
|
||||||
pointerEvents: 'all',
|
pointerEvents: 'all',
|
||||||
width: 'fit-content',
|
width: 'fit-content',
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
// gridRow: 1,
|
|
||||||
// flexGrow: 2,
|
|
||||||
// display: 'block',
|
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
})
|
})
|
||||||
|
|
184
packages/tldraw/src/components/ToolsPanel/HelpPanel.tsx
Normal file
184
packages/tldraw/src/components/ToolsPanel/HelpPanel.tsx
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
import * as Popover from '@radix-ui/react-popover'
|
||||||
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||||
|
import { FormattedMessage } from 'react-intl'
|
||||||
|
import { styled } from '~styles'
|
||||||
|
import { useTldrawApp } from '~hooks'
|
||||||
|
import { TDSnapshot } from '~types'
|
||||||
|
import { breakpoints } from '~components/breakpoints'
|
||||||
|
import { GitHubLogoIcon, QuestionMarkIcon, TwitterLogoIcon } from '@radix-ui/react-icons'
|
||||||
|
import { RowButton } from '~components/Primitives/RowButton'
|
||||||
|
import { MenuContent } from '~components/Primitives/MenuContent'
|
||||||
|
import { DMContent, DMDivider } from '~components/Primitives/DropdownMenu'
|
||||||
|
import { SmallIcon } from '~components/Primitives/SmallIcon'
|
||||||
|
import { DiscordIcon } from '~components/Primitives/icons'
|
||||||
|
import { LanguageMenu } from '~components/TopPanel/LanguageMenu/LanguageMenu'
|
||||||
|
import { KeyboardShortcutDialog } from './keyboardShortcutDialog'
|
||||||
|
|
||||||
|
const isDebugModeSelector = (s: TDSnapshot) => s.settings.isDebugMode
|
||||||
|
const dockPositionState = (s: TDSnapshot) => s.settings.dockPosition
|
||||||
|
|
||||||
|
export function HelpPanel() {
|
||||||
|
const app = useTldrawApp()
|
||||||
|
const isDebugMode = app.useStore(isDebugModeSelector)
|
||||||
|
const side = app.useStore(dockPositionState)
|
||||||
|
|
||||||
|
const [isKeyboardShortcutsOpen, setIsKeyboardShortcutsOpen] = React.useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover.Root>
|
||||||
|
<PopoverAnchor dir="ltr">
|
||||||
|
<Popover.Trigger asChild dir="ltr">
|
||||||
|
<HelpButton side={side} debug={isDebugMode} bp={breakpoints}>
|
||||||
|
<QuestionMarkIcon />
|
||||||
|
</HelpButton>
|
||||||
|
</Popover.Trigger>
|
||||||
|
</PopoverAnchor>
|
||||||
|
<Popover.Content dir="ltr">
|
||||||
|
<StyledContent style={{ visibility: isKeyboardShortcutsOpen ? 'hidden' : 'visible' }}>
|
||||||
|
<LanguageMenuDropdown />
|
||||||
|
<KeyboardShortcutDialog onOpenChange={setIsKeyboardShortcutsOpen} />
|
||||||
|
<DMDivider />
|
||||||
|
<Links />
|
||||||
|
</StyledContent>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const LanguageMenuDropdown = () => {
|
||||||
|
return (
|
||||||
|
<DropdownMenu.Root dir="ltr">
|
||||||
|
<DropdownMenu.Trigger asChild>
|
||||||
|
<RowButton variant="wide" hasArrow>
|
||||||
|
<FormattedMessage id="language" />
|
||||||
|
</RowButton>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<LanguageMenu />
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const linksData = [
|
||||||
|
{ id: 'github', title: 'Github', icon: GitHubLogoIcon, url: 'https://github.com/tldraw/tldraw' },
|
||||||
|
{ id: 'twitter', title: 'Twitter', icon: TwitterLogoIcon, url: 'https://twitter.com/tldraw' },
|
||||||
|
{ id: 'discord', title: 'Discord', icon: DiscordIcon, url: 'https://discord.gg/SBBEVCA4PG' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const Links = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{linksData.map((item) => (
|
||||||
|
<a key={item.id} href={item.url} target="_blank" rel="nofollow">
|
||||||
|
<RowButton id={`TD-Link-${item.id}`} variant="wide">
|
||||||
|
{item.title}
|
||||||
|
<SmallIcon>
|
||||||
|
<item.icon />
|
||||||
|
</SmallIcon>
|
||||||
|
</RowButton>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const HelpButton = styled('button', {
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
borderRadius: '100%',
|
||||||
|
position: 'fixed',
|
||||||
|
right: 10,
|
||||||
|
display: 'grid',
|
||||||
|
placeItems: 'center',
|
||||||
|
border: 'none',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
cursor: 'pointer',
|
||||||
|
boxShadow: '$panel',
|
||||||
|
bottom: 10,
|
||||||
|
variants: {
|
||||||
|
debug: {
|
||||||
|
true: {},
|
||||||
|
false: {},
|
||||||
|
},
|
||||||
|
bp: {
|
||||||
|
mobile: {},
|
||||||
|
small: {},
|
||||||
|
medium: {},
|
||||||
|
large: {},
|
||||||
|
},
|
||||||
|
side: {
|
||||||
|
top: {},
|
||||||
|
left: {},
|
||||||
|
right: {},
|
||||||
|
bottom: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
compoundVariants: [
|
||||||
|
{
|
||||||
|
bp: 'mobile',
|
||||||
|
side: 'bottom',
|
||||||
|
debug: false,
|
||||||
|
css: {
|
||||||
|
bottom: 70,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bp: 'mobile',
|
||||||
|
debug: true,
|
||||||
|
css: {
|
||||||
|
bottom: 50, // 40 + 10
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bp: 'mobile',
|
||||||
|
side: 'bottom',
|
||||||
|
debug: true,
|
||||||
|
css: {
|
||||||
|
bottom: 110,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bp: 'small',
|
||||||
|
side: 'bottom',
|
||||||
|
debug: true,
|
||||||
|
css: {
|
||||||
|
bottom: 50,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bp: 'small',
|
||||||
|
debug: false,
|
||||||
|
css: {
|
||||||
|
bottom: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
export const StyledContent = styled(MenuContent, {
|
||||||
|
width: 'fit-content',
|
||||||
|
height: 'fit-content',
|
||||||
|
minWidth: 200,
|
||||||
|
maxHeight: 380,
|
||||||
|
overflowY: 'auto',
|
||||||
|
'& *': {
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
horizontal: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
},
|
||||||
|
menu: {
|
||||||
|
minWidth: 128,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const PopoverAnchor = styled(Popover.Anchor, {
|
||||||
|
position: 'absolute',
|
||||||
|
right: 10,
|
||||||
|
zIndex: 999,
|
||||||
|
bottom: 50,
|
||||||
|
})
|
|
@ -97,7 +97,7 @@ export const ShapesMenu = React.memo(function ShapesMenu({
|
||||||
{shapeShapes.map((shape, i) => (
|
{shapeShapes.map((shape, i) => (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
key={shape}
|
key={shape}
|
||||||
label={intl.formatMessage({ id: shape[0].toUpperCase() + shape.slice(1) })}
|
label={intl.formatMessage({ id: shape })}
|
||||||
kbd={(4 + i).toString()}
|
kbd={(4 + i).toString()}
|
||||||
id={`TD-PrimaryTools-Shapes-${shape}`}
|
id={`TD-PrimaryTools-Shapes-${shape}`}
|
||||||
>
|
>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { PrimaryTools } from './PrimaryTools'
|
||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
import { DeleteButton } from './DeleteButton'
|
import { DeleteButton } from './DeleteButton'
|
||||||
import { breakpoints } from '~components/breakpoints'
|
import { breakpoints } from '~components/breakpoints'
|
||||||
|
import { HelpPanel } from './HelpPanel'
|
||||||
|
|
||||||
const isDebugModeSelector = (s: TDSnapshot) => s.settings.isDebugMode
|
const isDebugModeSelector = (s: TDSnapshot) => s.settings.isDebugMode
|
||||||
const dockPositionState = (s: TDSnapshot) => s.settings.dockPosition
|
const dockPositionState = (s: TDSnapshot) => s.settings.dockPosition
|
||||||
|
@ -35,6 +36,7 @@ export const ToolsPanel = React.memo(function ToolsPanel({ onBlur }: ToolsPanelP
|
||||||
</StyledPrimaryTools>
|
</StyledPrimaryTools>
|
||||||
</StyledCenterWrap>
|
</StyledCenterWrap>
|
||||||
</StyledToolsPanelContainer>
|
</StyledToolsPanelContainer>
|
||||||
|
<HelpPanel />
|
||||||
{isDebugMode && (
|
{isDebugMode && (
|
||||||
<StyledStatusWrap>
|
<StyledStatusWrap>
|
||||||
<StatusBar />
|
<StatusBar />
|
||||||
|
@ -84,7 +86,7 @@ const StyledToolsPanelContainer = styled('div', {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 4,
|
||||||
},
|
},
|
||||||
left: { width: 64, height: '100%', left: 0 },
|
left: { width: 64, height: '100%', left: 0 },
|
||||||
},
|
},
|
||||||
|
@ -101,7 +103,7 @@ const StyledToolsPanelContainer = styled('div', {
|
||||||
side: 'bottom',
|
side: 'bottom',
|
||||||
debug: true,
|
debug: true,
|
||||||
css: {
|
css: {
|
||||||
bottom: '40px',
|
bottom: 44,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -0,0 +1,235 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
import * as Dialog from '@radix-ui/react-dialog'
|
||||||
|
import { Cross2Icon } from '@radix-ui/react-icons'
|
||||||
|
import { FormattedMessage, useIntl } from 'react-intl'
|
||||||
|
import { IconButton } from '~components/Primitives/IconButton'
|
||||||
|
import { RowButton } from '~components/Primitives/RowButton'
|
||||||
|
import { styled } from '~styles'
|
||||||
|
import { breakpoints } from '~components/breakpoints'
|
||||||
|
import { Kbd } from '~components/Primitives/Kbd'
|
||||||
|
|
||||||
|
export function KeyboardShortcutDialog({
|
||||||
|
onOpenChange,
|
||||||
|
}: {
|
||||||
|
onOpenChange?: (open: boolean) => void
|
||||||
|
}) {
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
const shortcuts = {
|
||||||
|
Tools: [
|
||||||
|
{ label: intl.formatMessage({ id: 'select' }), kbd: '1' },
|
||||||
|
{ label: intl.formatMessage({ id: 'draw' }), kbd: '2' },
|
||||||
|
{ label: intl.formatMessage({ id: 'eraser' }), kbd: '3' },
|
||||||
|
{ label: intl.formatMessage({ id: 'rectangle' }), kbd: '4' },
|
||||||
|
{ label: intl.formatMessage({ id: 'ellipse' }), kbd: '5' },
|
||||||
|
{ label: intl.formatMessage({ id: 'triangle' }), kbd: '6' },
|
||||||
|
{ label: intl.formatMessage({ id: 'line' }), kbd: '7' },
|
||||||
|
{ label: intl.formatMessage({ id: 'arrow' }), kbd: '8' },
|
||||||
|
{ label: intl.formatMessage({ id: 'text' }), kbd: '9' },
|
||||||
|
{ label: intl.formatMessage({ id: 'sticky' }), kbd: '0' },
|
||||||
|
],
|
||||||
|
View: [
|
||||||
|
{ label: intl.formatMessage({ id: 'zoom.in' }), kbd: '#+' },
|
||||||
|
{ label: intl.formatMessage({ id: 'zoom.out' }), kbd: '#-' },
|
||||||
|
{ label: `${intl.formatMessage({ id: 'zoom.to' })} 100%`, kbd: '⇧+0' },
|
||||||
|
{ label: intl.formatMessage({ id: 'zoom.to.fit' }), kbd: '⇧+1' },
|
||||||
|
{ label: intl.formatMessage({ id: 'zoom.to.selection' }), kbd: '⇧+2' },
|
||||||
|
{ label: intl.formatMessage({ id: 'preferences.dark.mode' }), kbd: '#⇧D' },
|
||||||
|
{ label: intl.formatMessage({ id: 'preferences.focus.mode' }), kbd: '#.' },
|
||||||
|
{ label: intl.formatMessage({ id: 'preferences.show.grid' }), kbd: '#⇧G' },
|
||||||
|
],
|
||||||
|
Transform: [
|
||||||
|
{ label: intl.formatMessage({ id: 'flip.horizontal' }), kbd: '⇧H' },
|
||||||
|
{ label: intl.formatMessage({ id: 'flip.vertical' }), kbd: '⇧V' },
|
||||||
|
{
|
||||||
|
label: `${intl.formatMessage({ id: 'lock' })} / ${intl.formatMessage({ id: 'unlock' })}`,
|
||||||
|
kbd: '#⇧L',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `${intl.formatMessage({ id: 'move' })} ${intl.formatMessage({ id: 'to.front' })}`,
|
||||||
|
kbd: '⇧]',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `${intl.formatMessage({ id: 'move' })} ${intl.formatMessage({ id: 'forward' })}`,
|
||||||
|
kbd: ']',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `${intl.formatMessage({ id: 'move' })} ${intl.formatMessage({ id: 'backward' })}`,
|
||||||
|
kbd: '[',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `${intl.formatMessage({ id: 'move' })} ${intl.formatMessage({ id: 'back' })}`,
|
||||||
|
kbd: '⇧[',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
File: [
|
||||||
|
{ label: intl.formatMessage({ id: 'new.project' }), kbd: '#N' },
|
||||||
|
{ label: intl.formatMessage({ id: 'open' }), kbd: '#O' },
|
||||||
|
{ label: intl.formatMessage({ id: 'save' }), kbd: '#S' },
|
||||||
|
{ label: intl.formatMessage({ id: 'save.as' }), kbd: '#⇧S' },
|
||||||
|
{ label: intl.formatMessage({ id: 'upload.media' }), kbd: '#U' },
|
||||||
|
],
|
||||||
|
Edit: [
|
||||||
|
{ label: intl.formatMessage({ id: 'undo' }), kbd: '#Z' },
|
||||||
|
{ label: intl.formatMessage({ id: 'redo' }), kbd: '#⇧Z' },
|
||||||
|
{ label: intl.formatMessage({ id: 'cut' }), kbd: '#X' },
|
||||||
|
{ label: intl.formatMessage({ id: 'copy' }), kbd: '#C' },
|
||||||
|
{ label: intl.formatMessage({ id: 'paste' }), kbd: '#V' },
|
||||||
|
{ label: intl.formatMessage({ id: 'select.all' }), kbd: '#A' },
|
||||||
|
{ label: intl.formatMessage({ id: 'delete' }), kbd: '⌫' },
|
||||||
|
{ label: intl.formatMessage({ id: 'duplicate' }), kbd: '#D' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.Root onOpenChange={onOpenChange}>
|
||||||
|
<Dialog.Trigger asChild>
|
||||||
|
<RowButton id="TD-HelpItem-Keyboard" variant="wide">
|
||||||
|
<FormattedMessage id="keyboard.shortcuts" />
|
||||||
|
</RowButton>
|
||||||
|
</Dialog.Trigger>
|
||||||
|
<Dialog.Portal>
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogContent>
|
||||||
|
<DialogTitle>
|
||||||
|
<FormattedMessage id="keyboard.shortcuts" />
|
||||||
|
<Dialog.Close asChild>
|
||||||
|
<DialogIconButton>
|
||||||
|
<Cross2Icon />
|
||||||
|
</DialogIconButton>
|
||||||
|
</Dialog.Close>
|
||||||
|
</DialogTitle>
|
||||||
|
<StyledColumns bp={breakpoints}>
|
||||||
|
{Object.entries(shortcuts).map(([key, value]) => (
|
||||||
|
<StyledSection key={key}>
|
||||||
|
<Label>{key}</Label>
|
||||||
|
<ContentItem>
|
||||||
|
{value.map((shortcut) => (
|
||||||
|
<StyledItem key={shortcut.label}>
|
||||||
|
{shortcut.label}
|
||||||
|
<Kbd variant="menu">{shortcut.kbd}</Kbd>
|
||||||
|
</StyledItem>
|
||||||
|
))}
|
||||||
|
</ContentItem>
|
||||||
|
</StyledSection>
|
||||||
|
))}
|
||||||
|
</StyledColumns>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Label = styled('h3', {
|
||||||
|
fontSize: '$2',
|
||||||
|
color: '$text',
|
||||||
|
fontFamily: '$ui',
|
||||||
|
margin: 0,
|
||||||
|
paddingBottom: '$5',
|
||||||
|
})
|
||||||
|
|
||||||
|
const StyledSection = styled('div', {
|
||||||
|
breakInside: 'avoid',
|
||||||
|
paddingBottom: 24,
|
||||||
|
})
|
||||||
|
|
||||||
|
const ContentItem = styled('ul', {
|
||||||
|
listStyleType: 'none',
|
||||||
|
width: '100%',
|
||||||
|
padding: 0,
|
||||||
|
margin: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
const StyledItem = styled('li', {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
height: 32,
|
||||||
|
minHeight: 32,
|
||||||
|
width: '100%',
|
||||||
|
outline: 'none',
|
||||||
|
color: '$text',
|
||||||
|
fontFamily: '$ui',
|
||||||
|
fontWeight: 400,
|
||||||
|
fontSize: '$1',
|
||||||
|
borderRadius: 4,
|
||||||
|
userSelect: 'none',
|
||||||
|
margin: 0,
|
||||||
|
padding: '0 0',
|
||||||
|
})
|
||||||
|
|
||||||
|
const DialogContent = styled(Dialog.Content, {
|
||||||
|
backgroundColor: 'white',
|
||||||
|
borderRadius: 6,
|
||||||
|
boxShadow: 'hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px',
|
||||||
|
position: 'fixed',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
width: 'fit-content',
|
||||||
|
maxWidth: '90vw',
|
||||||
|
maxHeight: '74vh',
|
||||||
|
overflowY: 'auto',
|
||||||
|
padding: 25,
|
||||||
|
'&:focus': { outline: 'none' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const StyledColumns = styled('div', {
|
||||||
|
maxWidth: '100%',
|
||||||
|
width: 'fit-content',
|
||||||
|
height: 'fit-content',
|
||||||
|
overflowY: 'auto',
|
||||||
|
columnGap: 64,
|
||||||
|
variants: {
|
||||||
|
bp: {
|
||||||
|
mobile: {
|
||||||
|
columns: 1,
|
||||||
|
[`& ${StyledSection}`]: {
|
||||||
|
minWidth: '0px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
small: {
|
||||||
|
columns: 2,
|
||||||
|
[`& ${StyledSection}`]: {
|
||||||
|
minWidth: '200px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
columns: 3,
|
||||||
|
},
|
||||||
|
large: {
|
||||||
|
columns: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const DialogOverlay = styled(Dialog.Overlay, {
|
||||||
|
backgroundColor: '$overlay',
|
||||||
|
position: 'fixed',
|
||||||
|
inset: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
const DialogIconButton = styled(IconButton, {
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
borderRadius: '100%',
|
||||||
|
height: 25,
|
||||||
|
width: 25,
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
color: '$text',
|
||||||
|
cursor: 'pointer',
|
||||||
|
'&:hover': { backgroundColor: '$hover' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const DialogTitle = styled(Dialog.Title, {
|
||||||
|
fontFamily: '$body',
|
||||||
|
fontSize: '$3',
|
||||||
|
color: '$text',
|
||||||
|
paddingBottom: 32,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
margin: 0,
|
||||||
|
})
|
|
@ -1,8 +1,7 @@
|
||||||
import { ExternalLinkIcon } from '@radix-ui/react-icons'
|
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { FormattedMessage, useIntl } from 'react-intl'
|
import { ExternalLinkIcon } from '@radix-ui/react-icons'
|
||||||
import { DMCheckboxItem, DMDivider, DMItem, DMSubMenu } from '~components/Primitives/DropdownMenu'
|
import { FormattedMessage } from 'react-intl'
|
||||||
import { HeartIcon } from '~components/Primitives/icons/HeartIcon'
|
import { DMCheckboxItem, DMContent, DMDivider, DMItem } from '~components/Primitives/DropdownMenu'
|
||||||
import { SmallIcon } from '~components/Primitives/SmallIcon'
|
import { SmallIcon } from '~components/Primitives/SmallIcon'
|
||||||
import { useTldrawApp } from '~hooks'
|
import { useTldrawApp } from '~hooks'
|
||||||
import { TDLanguage, TRANSLATIONS } from '~translations'
|
import { TDLanguage, TRANSLATIONS } from '~translations'
|
||||||
|
@ -13,7 +12,6 @@ const languageSelector = (s: TDSnapshot) => s.settings.language
|
||||||
export function LanguageMenu() {
|
export function LanguageMenu() {
|
||||||
const app = useTldrawApp()
|
const app = useTldrawApp()
|
||||||
const language = app.useStore(languageSelector)
|
const language = app.useStore(languageSelector)
|
||||||
const intl = useIntl()
|
|
||||||
|
|
||||||
const handleChangeLanguage = React.useCallback(
|
const handleChangeLanguage = React.useCallback(
|
||||||
(locale: TDLanguage) => {
|
(locale: TDLanguage) => {
|
||||||
|
@ -23,7 +21,7 @@ export function LanguageMenu() {
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DMSubMenu label={intl.formatMessage({ id: 'language' })}>
|
<DMContent variant="menu" overflow={true} id="language-menu" side="left" sideOffset={8}>
|
||||||
{TRANSLATIONS.map(({ locale, label }) => (
|
{TRANSLATIONS.map(({ locale, label }) => (
|
||||||
<DMCheckboxItem
|
<DMCheckboxItem
|
||||||
key={locale}
|
key={locale}
|
||||||
|
@ -47,6 +45,6 @@ export function LanguageMenu() {
|
||||||
</SmallIcon>
|
</SmallIcon>
|
||||||
</DMItem>
|
</DMItem>
|
||||||
</a>
|
</a>
|
||||||
</DMSubMenu>
|
</DMContent>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,6 @@ import { DiscordIcon } from '~components/Primitives/icons'
|
||||||
import { TDExportType, TDSnapshot } from '~types'
|
import { TDExportType, TDSnapshot } from '~types'
|
||||||
import { Divider } from '~components/Primitives/Divider'
|
import { Divider } from '~components/Primitives/Divider'
|
||||||
import { FormattedMessage, useIntl } from 'react-intl'
|
import { FormattedMessage, useIntl } from 'react-intl'
|
||||||
import { LanguageMenu } from '../LanguageMenu/LanguageMenu'
|
|
||||||
|
|
||||||
interface MenuProps {
|
interface MenuProps {
|
||||||
sponsor: boolean | undefined
|
sponsor: boolean | undefined
|
||||||
|
@ -326,53 +325,6 @@ export const Menu = React.memo(function Menu({ sponsor, readOnly }: MenuProps) {
|
||||||
</DMSubMenu>
|
</DMSubMenu>
|
||||||
<DMDivider dir="ltr" />
|
<DMDivider dir="ltr" />
|
||||||
<PreferencesMenu />
|
<PreferencesMenu />
|
||||||
<DMDivider dir="ltr" />
|
|
||||||
<LanguageMenu />
|
|
||||||
<DMDivider dir="ltr" />
|
|
||||||
<a href="https://github.com/Tldraw/Tldraw" target="_blank" rel="nofollow">
|
|
||||||
<DMItem id="TD-MenuItem-Github">
|
|
||||||
GitHub
|
|
||||||
<SmallIcon>
|
|
||||||
<GitHubLogoIcon />
|
|
||||||
</SmallIcon>
|
|
||||||
</DMItem>
|
|
||||||
</a>
|
|
||||||
<a href="https://twitter.com/Tldraw" target="_blank" rel="nofollow">
|
|
||||||
<DMItem id="TD-MenuItem-Twitter">
|
|
||||||
Twitter
|
|
||||||
<SmallIcon>
|
|
||||||
<TwitterLogoIcon />
|
|
||||||
</SmallIcon>
|
|
||||||
</DMItem>
|
|
||||||
</a>
|
|
||||||
<a href="https://discord.gg/SBBEVCA4PG" target="_blank" rel="nofollow">
|
|
||||||
<DMItem id="TD-MenuItem-Discord">
|
|
||||||
Discord
|
|
||||||
<SmallIcon>
|
|
||||||
<DiscordIcon />
|
|
||||||
</SmallIcon>
|
|
||||||
</DMItem>
|
|
||||||
</a>
|
|
||||||
{sponsor === false && (
|
|
||||||
<a href="https://github.com/sponsors/steveruizok" target="_blank" rel="nofollow">
|
|
||||||
<DMItem isSponsor id="TD-MenuItem-Become_a_Sponsor">
|
|
||||||
<FormattedMessage id="become.a.sponsor" />{' '}
|
|
||||||
<SmallIcon>
|
|
||||||
<HeartIcon />
|
|
||||||
</SmallIcon>
|
|
||||||
</DMItem>
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
{sponsor === true && (
|
|
||||||
<a href="https://github.com/sponsors/steveruizok" target="_blank" rel="nofollow">
|
|
||||||
<DMItem id="TD-MenuItem-is_a_Sponsor">
|
|
||||||
<FormattedMessage id="sponsored" />!
|
|
||||||
<SmallIcon>
|
|
||||||
<HeartFilledIcon />
|
|
||||||
</SmallIcon>
|
|
||||||
</DMItem>
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
{showSignInOutMenu && (
|
{showSignInOutMenu && (
|
||||||
<>
|
<>
|
||||||
<DMDivider dir="ltr" />{' '}
|
<DMDivider dir="ltr" />{' '}
|
||||||
|
|
|
@ -138,7 +138,7 @@ export function PreferencesMenu() {
|
||||||
>
|
>
|
||||||
<FormattedMessage id="preferences.clone.handles" />
|
<FormattedMessage id="preferences.clone.handles" />
|
||||||
</DMCheckboxItem>
|
</DMCheckboxItem>
|
||||||
<DMSubMenu label={intl.formatMessage({ id: 'dock.position' })}>
|
<DMSubMenu label={intl.formatMessage({ id: 'dock.position' })} overflow={false}>
|
||||||
{DockPosition.map((position) => (
|
{DockPosition.map((position) => (
|
||||||
<DMCheckboxItem
|
<DMCheckboxItem
|
||||||
key={position}
|
key={position}
|
||||||
|
|
|
@ -91,6 +91,7 @@
|
||||||
"language": "Langage",
|
"language": "Langage",
|
||||||
"dock.position": "Position du dock",
|
"dock.position": "Position du dock",
|
||||||
"bottom": "En bas",
|
"bottom": "En bas",
|
||||||
|
"keyboard.shortcuts": "Raccourci clavier",
|
||||||
"loading": "Chargement{dots}",
|
"loading": "Chargement{dots}",
|
||||||
"left": "À gauche",
|
"left": "À gauche",
|
||||||
"right": "À droite",
|
"right": "À droite",
|
||||||
|
|
|
@ -64,10 +64,10 @@
|
||||||
"arrow": "Arrow",
|
"arrow": "Arrow",
|
||||||
"text": "Text",
|
"text": "Text",
|
||||||
"sticky": "Sticky",
|
"sticky": "Sticky",
|
||||||
"Rectangle": "Rectangle",
|
"rectangle": "Rectangle",
|
||||||
"Ellipse": "Ellipse",
|
"ellipse": "Ellipse",
|
||||||
"Triangle": "Triangle",
|
"triangle": "Triangle",
|
||||||
"Line": "Line",
|
"line": "Line",
|
||||||
"rotate": "Rotate",
|
"rotate": "Rotate",
|
||||||
"lock.aspect.ratio": "Lock Aspect Ratio",
|
"lock.aspect.ratio": "Lock Aspect Ratio",
|
||||||
"unlock.aspect.ratio": "Unlock Aspect Ratio",
|
"unlock.aspect.ratio": "Unlock Aspect Ratio",
|
||||||
|
@ -96,5 +96,7 @@
|
||||||
"right": "Right",
|
"right": "Right",
|
||||||
"top": "Top",
|
"top": "Top",
|
||||||
"page": "Page",
|
"page": "Page",
|
||||||
|
"keyboard.shortcuts": "Keyboard shortcuts",
|
||||||
|
"search": "Search",
|
||||||
"loading": "Loading{dots}"
|
"loading": "Loading{dots}"
|
||||||
}
|
}
|
23
yarn.lock
23
yarn.lock
|
@ -2482,7 +2482,7 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
|
|
||||||
"@radix-ui/react-dialog@0.1.7":
|
"@radix-ui/react-dialog@0.1.7", "@radix-ui/react-dialog@^0.1.7":
|
||||||
version "0.1.7"
|
version "0.1.7"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-0.1.7.tgz#285414cf66f5bbf42bc9935314e0381abe01e7d0"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-0.1.7.tgz#285414cf66f5bbf42bc9935314e0381abe01e7d0"
|
||||||
integrity sha512-jXt8srGhHBRvEr9jhEAiwwJzWCWZoGRJ030aC9ja/gkRJbZdy0iD3FwXf+Ff4RtsZyLUMHW7VUwFOlz3Ixe1Vw==
|
integrity sha512-jXt8srGhHBRvEr9jhEAiwwJzWCWZoGRJ030aC9ja/gkRJbZdy0iD3FwXf+Ff4RtsZyLUMHW7VUwFOlz3Ixe1Vw==
|
||||||
|
@ -2584,6 +2584,27 @@
|
||||||
aria-hidden "^1.1.1"
|
aria-hidden "^1.1.1"
|
||||||
react-remove-scroll "^2.4.0"
|
react-remove-scroll "^2.4.0"
|
||||||
|
|
||||||
|
"@radix-ui/react-popover@^0.1.6":
|
||||||
|
version "0.1.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-0.1.6.tgz#788e969239d9c55239678e615ab591b6b7ba5cdc"
|
||||||
|
integrity sha512-zQzgUqW4RQDb0ItAL1xNW4K4olUrkfV3jeEPs9rG+nsDQurO+W9TT+YZ9H1mmgAJqlthyv1sBRZGdBm4YjtD6Q==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.13.10"
|
||||||
|
"@radix-ui/primitive" "0.1.0"
|
||||||
|
"@radix-ui/react-compose-refs" "0.1.0"
|
||||||
|
"@radix-ui/react-context" "0.1.1"
|
||||||
|
"@radix-ui/react-dismissable-layer" "0.1.5"
|
||||||
|
"@radix-ui/react-focus-guards" "0.1.0"
|
||||||
|
"@radix-ui/react-focus-scope" "0.1.4"
|
||||||
|
"@radix-ui/react-id" "0.1.5"
|
||||||
|
"@radix-ui/react-popper" "0.1.4"
|
||||||
|
"@radix-ui/react-portal" "0.1.4"
|
||||||
|
"@radix-ui/react-presence" "0.1.2"
|
||||||
|
"@radix-ui/react-primitive" "0.1.4"
|
||||||
|
"@radix-ui/react-use-controllable-state" "0.1.0"
|
||||||
|
aria-hidden "^1.1.1"
|
||||||
|
react-remove-scroll "^2.4.0"
|
||||||
|
|
||||||
"@radix-ui/react-popper@0.1.4":
|
"@radix-ui/react-popper@0.1.4":
|
||||||
version "0.1.4"
|
version "0.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-0.1.4.tgz#dfc055dcd7dfae6a2eff7a70d333141d15a5d029"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-0.1.4.tgz#dfc055dcd7dfae6a2eff7a70d333141d15a5d029"
|
||||||
|
|
Loading…
Reference in a new issue