[feature] fonts (#308)
* adds fonts * Add alignment options * Update useKeyboardShortcuts.tsx * Improve style panel * Alignment for sticky notes * swap fonts
This commit is contained in:
parent
a0891ca3ff
commit
0685ca3871
17 changed files with 420 additions and 83 deletions
|
@ -11,7 +11,7 @@ export interface RowButtonProps {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
kbd?: string
|
kbd?: string
|
||||||
variant?: 'wide'
|
variant?: 'wide' | 'styleMenu'
|
||||||
isSponsor?: boolean
|
isSponsor?: boolean
|
||||||
isActive?: boolean
|
isActive?: boolean
|
||||||
isWarning?: boolean
|
isWarning?: boolean
|
||||||
|
@ -130,6 +130,9 @@ export const StyledRowButton = styled('button', {
|
||||||
small: {},
|
small: {},
|
||||||
},
|
},
|
||||||
variant: {
|
variant: {
|
||||||
|
styleMenu: {
|
||||||
|
margin: '$1 0 $1 0',
|
||||||
|
},
|
||||||
wide: {
|
wide: {
|
||||||
gridColumn: '1 / span 4',
|
gridColumn: '1 / span 4',
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||||
import { strokes, fills, defaultStyle } from '~state/shapes/shared/shape-styles'
|
import { strokes, fills, defaultTextStyle } from '~state/shapes/shared/shape-styles'
|
||||||
import { useTldrawApp } from '~hooks'
|
import { useTldrawApp } from '~hooks'
|
||||||
import {
|
import {
|
||||||
DMCheckboxItem,
|
DMCheckboxItem,
|
||||||
|
@ -19,37 +19,65 @@ import {
|
||||||
SizeSmallIcon,
|
SizeSmallIcon,
|
||||||
} from '~components/Primitives/icons'
|
} from '~components/Primitives/icons'
|
||||||
import { ToolButton } from '~components/Primitives/ToolButton'
|
import { ToolButton } from '~components/Primitives/ToolButton'
|
||||||
import { TDSnapshot, ColorStyle, DashStyle, SizeStyle, ShapeStyles } from '~types'
|
import {
|
||||||
|
TDSnapshot,
|
||||||
|
ColorStyle,
|
||||||
|
DashStyle,
|
||||||
|
SizeStyle,
|
||||||
|
ShapeStyles,
|
||||||
|
FontStyle,
|
||||||
|
AlignStyle,
|
||||||
|
} from '~types'
|
||||||
import { styled } from '~styles'
|
import { styled } from '~styles'
|
||||||
import { breakpoints } from '~components/breakpoints'
|
import { breakpoints } from '~components/breakpoints'
|
||||||
import { Divider } from '~components/Primitives/Divider'
|
import { Divider } from '~components/Primitives/Divider'
|
||||||
import { preventEvent } from '~components/preventEvent'
|
import { preventEvent } from '~components/preventEvent'
|
||||||
|
import {
|
||||||
|
TextAlignCenterIcon,
|
||||||
|
TextAlignJustifyIcon,
|
||||||
|
TextAlignLeftIcon,
|
||||||
|
TextAlignRightIcon,
|
||||||
|
} from '@radix-ui/react-icons'
|
||||||
|
|
||||||
const currentStyleSelector = (s: TDSnapshot) => s.appState.currentStyle
|
const currentStyleSelector = (s: TDSnapshot) => s.appState.currentStyle
|
||||||
const selectedIdsSelector = (s: TDSnapshot) =>
|
const selectedIdsSelector = (s: TDSnapshot) =>
|
||||||
s.document.pageStates[s.appState.currentPageId].selectedIds
|
s.document.pageStates[s.appState.currentPageId].selectedIds
|
||||||
|
|
||||||
const STYLE_KEYS = Object.keys(defaultStyle) as (keyof ShapeStyles)[]
|
const STYLE_KEYS = Object.keys(defaultTextStyle) as (keyof ShapeStyles)[]
|
||||||
|
|
||||||
const DASHES = {
|
const DASH_ICONS = {
|
||||||
[DashStyle.Draw]: <DashDrawIcon />,
|
[DashStyle.Draw]: <DashDrawIcon />,
|
||||||
[DashStyle.Solid]: <DashSolidIcon />,
|
[DashStyle.Solid]: <DashSolidIcon />,
|
||||||
[DashStyle.Dashed]: <DashDashedIcon />,
|
[DashStyle.Dashed]: <DashDashedIcon />,
|
||||||
[DashStyle.Dotted]: <DashDottedIcon />,
|
[DashStyle.Dotted]: <DashDottedIcon />,
|
||||||
}
|
}
|
||||||
|
|
||||||
const SIZES = {
|
const SIZE_ICONS = {
|
||||||
[SizeStyle.Small]: <SizeSmallIcon />,
|
[SizeStyle.Small]: <SizeSmallIcon />,
|
||||||
[SizeStyle.Medium]: <SizeMediumIcon />,
|
[SizeStyle.Medium]: <SizeMediumIcon />,
|
||||||
[SizeStyle.Large]: <SizeLargeIcon />,
|
[SizeStyle.Large]: <SizeLargeIcon />,
|
||||||
}
|
}
|
||||||
|
|
||||||
const themeSelector = (data: TDSnapshot) => (data.settings.isDarkMode ? 'dark' : 'light')
|
const ALIGN_ICONS = {
|
||||||
|
[AlignStyle.Start]: <TextAlignLeftIcon />,
|
||||||
|
[AlignStyle.Middle]: <TextAlignCenterIcon />,
|
||||||
|
[AlignStyle.End]: <TextAlignRightIcon />,
|
||||||
|
[AlignStyle.Justify]: <TextAlignJustifyIcon />,
|
||||||
|
}
|
||||||
|
|
||||||
|
const themeSelector = (s: TDSnapshot) => (s.settings.isDarkMode ? 'dark' : 'light')
|
||||||
|
|
||||||
|
const showTextStylesSelector = (s: TDSnapshot) => {
|
||||||
|
const pageId = s.appState.currentPageId
|
||||||
|
const page = s.document.pages[pageId]
|
||||||
|
return s.document.pageStates[pageId].selectedIds.some((id) => 'text' in page.shapes[id])
|
||||||
|
}
|
||||||
|
|
||||||
export const StyleMenu = React.memo(function ColorMenu(): JSX.Element {
|
export const StyleMenu = React.memo(function ColorMenu(): JSX.Element {
|
||||||
const app = useTldrawApp()
|
const app = useTldrawApp()
|
||||||
|
|
||||||
const theme = app.useStore(themeSelector)
|
const theme = app.useStore(themeSelector)
|
||||||
|
const showTextStyles = app.useStore(showTextStylesSelector)
|
||||||
|
|
||||||
const currentStyle = app.useStore(currentStyleSelector)
|
const currentStyle = app.useStore(currentStyleSelector)
|
||||||
const selectedIds = app.useStore(selectedIdsSelector)
|
const selectedIds = app.useStore(selectedIdsSelector)
|
||||||
|
@ -111,6 +139,14 @@ export const StyleMenu = React.memo(function ColorMenu(): JSX.Element {
|
||||||
app.style({ size: value as SizeStyle })
|
app.style({ size: value as SizeStyle })
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const handleFontChange = React.useCallback((value: string) => {
|
||||||
|
app.style({ font: value as FontStyle })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleTextAlignChange = React.useCallback((value: string) => {
|
||||||
|
app.style({ textAlign: value as AlignStyle })
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.Root dir="ltr">
|
<DropdownMenu.Root dir="ltr">
|
||||||
<DMTriggerIcon>
|
<DMTriggerIcon>
|
||||||
|
@ -126,53 +162,56 @@ export const StyleMenu = React.memo(function ColorMenu(): JSX.Element {
|
||||||
fill={fills[theme][displayedStyle.color as ColorStyle]}
|
fill={fills[theme][displayedStyle.color as ColorStyle]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{DASHES[displayedStyle.dash]}
|
{DASH_ICONS[displayedStyle.dash]}
|
||||||
</OverlapIcons>
|
</OverlapIcons>
|
||||||
</DMTriggerIcon>
|
</DMTriggerIcon>
|
||||||
<DMContent>
|
<DMContent>
|
||||||
<StyledRow variant="tall">
|
<StyledRow variant="tall">
|
||||||
<span>Color</span>
|
<span>Color</span>
|
||||||
<ColorGrid>
|
<ColorGrid>
|
||||||
{Object.keys(strokes.light).map((colorStyle: string) => (
|
{Object.keys(strokes.light).map((style: string) => (
|
||||||
<DropdownMenu.Item key={colorStyle} onSelect={preventEvent} asChild>
|
<DropdownMenu.Item key={style} onSelect={preventEvent} asChild>
|
||||||
<ToolButton
|
<ToolButton
|
||||||
variant="icon"
|
variant="icon"
|
||||||
isActive={displayedStyle.color === colorStyle}
|
isActive={displayedStyle.color === style}
|
||||||
onClick={() => app.style({ color: colorStyle as ColorStyle })}
|
onClick={() => app.style({ color: style as ColorStyle })}
|
||||||
>
|
>
|
||||||
<CircleIcon
|
<CircleIcon
|
||||||
size={18}
|
size={18}
|
||||||
strokeWidth={2.5}
|
strokeWidth={2.5}
|
||||||
fill={
|
fill={
|
||||||
displayedStyle.isFilled
|
displayedStyle.isFilled ? fills.light[style as ColorStyle] : 'transparent'
|
||||||
? fills.light[colorStyle as ColorStyle]
|
|
||||||
: 'transparent'
|
|
||||||
}
|
}
|
||||||
stroke={strokes.light[colorStyle as ColorStyle]}
|
stroke={strokes.light[style as ColorStyle]}
|
||||||
/>
|
/>
|
||||||
</ToolButton>
|
</ToolButton>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
))}
|
))}
|
||||||
</ColorGrid>
|
</ColorGrid>
|
||||||
</StyledRow>
|
</StyledRow>
|
||||||
<Divider />
|
<DMCheckboxItem
|
||||||
|
variant="styleMenu"
|
||||||
|
checked={!!displayedStyle.isFilled}
|
||||||
|
onCheckedChange={handleToggleFilled}
|
||||||
|
>
|
||||||
|
Fill
|
||||||
|
</DMCheckboxItem>
|
||||||
<StyledRow>
|
<StyledRow>
|
||||||
Dash
|
Dash
|
||||||
<StyledGroup dir="ltr" value={displayedStyle.dash} onValueChange={handleDashChange}>
|
<StyledGroup dir="ltr" value={displayedStyle.dash} onValueChange={handleDashChange}>
|
||||||
{Object.values(DashStyle).map((dashStyle) => (
|
{Object.values(DashStyle).map((style) => (
|
||||||
<DMRadioItem
|
<DMRadioItem
|
||||||
key={dashStyle}
|
key={style}
|
||||||
isActive={dashStyle === displayedStyle.dash}
|
isActive={style === displayedStyle.dash}
|
||||||
value={dashStyle}
|
value={style}
|
||||||
onSelect={preventEvent}
|
onSelect={preventEvent}
|
||||||
bp={breakpoints}
|
bp={breakpoints}
|
||||||
>
|
>
|
||||||
{DASHES[dashStyle as DashStyle]}
|
{DASH_ICONS[style as DashStyle]}
|
||||||
</DMRadioItem>
|
</DMRadioItem>
|
||||||
))}
|
))}
|
||||||
</StyledGroup>
|
</StyledGroup>
|
||||||
</StyledRow>
|
</StyledRow>
|
||||||
<Divider />
|
|
||||||
<StyledRow>
|
<StyledRow>
|
||||||
Size
|
Size
|
||||||
<StyledGroup dir="ltr" value={displayedStyle.size} onValueChange={handleSizeChange}>
|
<StyledGroup dir="ltr" value={displayedStyle.size} onValueChange={handleSizeChange}>
|
||||||
|
@ -184,15 +223,52 @@ export const StyleMenu = React.memo(function ColorMenu(): JSX.Element {
|
||||||
onSelect={preventEvent}
|
onSelect={preventEvent}
|
||||||
bp={breakpoints}
|
bp={breakpoints}
|
||||||
>
|
>
|
||||||
{SIZES[sizeStyle as SizeStyle]}
|
{SIZE_ICONS[sizeStyle as SizeStyle]}
|
||||||
</DMRadioItem>
|
</DMRadioItem>
|
||||||
))}
|
))}
|
||||||
</StyledGroup>
|
</StyledGroup>
|
||||||
</StyledRow>
|
</StyledRow>
|
||||||
|
{showTextStyles && (
|
||||||
|
<>
|
||||||
<Divider />
|
<Divider />
|
||||||
<DMCheckboxItem checked={!!displayedStyle.isFilled} onCheckedChange={handleToggleFilled}>
|
<StyledRow>
|
||||||
Fill
|
Font
|
||||||
</DMCheckboxItem>
|
<StyledGroup dir="ltr" value={displayedStyle.font} onValueChange={handleFontChange}>
|
||||||
|
{Object.values(FontStyle).map((fontStyle) => (
|
||||||
|
<DMRadioItem
|
||||||
|
key={fontStyle}
|
||||||
|
isActive={fontStyle === displayedStyle.font}
|
||||||
|
value={fontStyle}
|
||||||
|
onSelect={preventEvent}
|
||||||
|
bp={breakpoints}
|
||||||
|
>
|
||||||
|
<FontIcon fontStyle={fontStyle}>Aa</FontIcon>
|
||||||
|
</DMRadioItem>
|
||||||
|
))}
|
||||||
|
</StyledGroup>
|
||||||
|
</StyledRow>
|
||||||
|
<StyledRow>
|
||||||
|
Align
|
||||||
|
<StyledGroup
|
||||||
|
dir="ltr"
|
||||||
|
value={displayedStyle.textAlign}
|
||||||
|
onValueChange={handleTextAlignChange}
|
||||||
|
>
|
||||||
|
{Object.values(AlignStyle).map((style) => (
|
||||||
|
<DMRadioItem
|
||||||
|
key={style}
|
||||||
|
isActive={style === displayedStyle.textAlign}
|
||||||
|
value={style}
|
||||||
|
onSelect={preventEvent}
|
||||||
|
bp={breakpoints}
|
||||||
|
>
|
||||||
|
{ALIGN_ICONS[style]}
|
||||||
|
</DMRadioItem>
|
||||||
|
))}
|
||||||
|
</StyledGroup>
|
||||||
|
</StyledRow>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</DMContent>
|
</DMContent>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
)
|
)
|
||||||
|
@ -237,7 +313,7 @@ export const StyledRow = styled('div', {
|
||||||
fontFamily: '$ui',
|
fontFamily: '$ui',
|
||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
fontSize: '$1',
|
fontSize: '$1',
|
||||||
padding: '0 0 0 $3',
|
padding: '$2 0 $2 $3',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
margin: 0,
|
margin: 0,
|
||||||
|
@ -250,6 +326,7 @@ export const StyledRow = styled('div', {
|
||||||
variant: {
|
variant: {
|
||||||
tall: {
|
tall: {
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
|
padding: '0 0 0 $3',
|
||||||
'& > span': {
|
'& > span': {
|
||||||
paddingTop: '$4',
|
paddingTop: '$4',
|
||||||
},
|
},
|
||||||
|
@ -261,6 +338,7 @@ export const StyledRow = styled('div', {
|
||||||
const StyledGroup = styled(DropdownMenu.DropdownMenuRadioGroup, {
|
const StyledGroup = styled(DropdownMenu.DropdownMenuRadioGroup, {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
gap: '$1',
|
||||||
})
|
})
|
||||||
|
|
||||||
const OverlapIcons = styled('div', {
|
const OverlapIcons = styled('div', {
|
||||||
|
@ -270,3 +348,28 @@ const OverlapIcons = styled('div', {
|
||||||
gridRow: 1,
|
gridRow: 1,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const FontIcon = styled('div', {
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: '$3',
|
||||||
|
variants: {
|
||||||
|
fontStyle: {
|
||||||
|
[FontStyle.Script]: {
|
||||||
|
fontFamily: 'Caveat Brush',
|
||||||
|
},
|
||||||
|
[FontStyle.Sans]: {
|
||||||
|
fontFamily: 'Recursive',
|
||||||
|
},
|
||||||
|
[FontStyle.Serif]: {
|
||||||
|
fontFamily: 'Georgia',
|
||||||
|
},
|
||||||
|
[FontStyle.Mono]: {
|
||||||
|
fontFamily: 'Recursive Mono',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { TDShapeType } from '~types'
|
import { AlignStyle, TDShapeType } from '~types'
|
||||||
import { useFileSystemHandlers, useTldrawApp } from '~hooks'
|
import { useFileSystemHandlers, useTldrawApp } from '~hooks'
|
||||||
|
|
||||||
export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
|
@ -97,7 +97,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
// Dark Mode
|
// Dark Mode
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'ctrl+shift+d,command+shift+d',
|
'ctrl+shift+d,⌘+shift+d',
|
||||||
(e) => {
|
(e) => {
|
||||||
if (!canHandleEvent()) return
|
if (!canHandleEvent()) return
|
||||||
app.toggleDarkMode()
|
app.toggleDarkMode()
|
||||||
|
@ -110,7 +110,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
// Focus Mode
|
// Focus Mode
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'ctrl+.,command+.',
|
'ctrl+.,⌘+.',
|
||||||
() => {
|
() => {
|
||||||
if (!canHandleEvent()) return
|
if (!canHandleEvent()) return
|
||||||
app.toggleFocusMode()
|
app.toggleFocusMode()
|
||||||
|
@ -124,7 +124,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
const { onNewProject, onOpenProject, onSaveProject, onSaveProjectAs } = useFileSystemHandlers()
|
const { onNewProject, onOpenProject, onSaveProject, onSaveProjectAs } = useFileSystemHandlers()
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'ctrl+n,command+n',
|
'ctrl+n,⌘+n',
|
||||||
(e) => {
|
(e) => {
|
||||||
if (!canHandleEvent()) return
|
if (!canHandleEvent()) return
|
||||||
|
|
||||||
|
@ -134,7 +134,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
[app]
|
[app]
|
||||||
)
|
)
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'ctrl+s,command+s',
|
'ctrl+s,⌘+s',
|
||||||
(e) => {
|
(e) => {
|
||||||
if (!canHandleEvent()) return
|
if (!canHandleEvent()) return
|
||||||
|
|
||||||
|
@ -145,7 +145,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'ctrl+shift+s,command+shift+s',
|
'ctrl+shift+s,⌘+shift+s',
|
||||||
(e) => {
|
(e) => {
|
||||||
if (!canHandleEvent()) return
|
if (!canHandleEvent()) return
|
||||||
|
|
||||||
|
@ -155,7 +155,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
[app]
|
[app]
|
||||||
)
|
)
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'ctrl+o,command+o',
|
'ctrl+o,⌘+o',
|
||||||
(e) => {
|
(e) => {
|
||||||
if (!canHandleEvent()) return
|
if (!canHandleEvent()) return
|
||||||
|
|
||||||
|
@ -168,10 +168,12 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
// Undo Redo
|
// Undo Redo
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'command+z,ctrl+z',
|
'⌘+z,ctrl+z',
|
||||||
() => {
|
() => {
|
||||||
if (!canHandleEvent()) return
|
if (!canHandleEvent()) return
|
||||||
|
|
||||||
|
console.log('Hello')
|
||||||
|
|
||||||
if (app.session) {
|
if (app.session) {
|
||||||
app.cancelSession()
|
app.cancelSession()
|
||||||
} else {
|
} else {
|
||||||
|
@ -183,7 +185,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'ctrl+shift-z,command+shift+z',
|
'ctrl+shift-z,⌘+shift+z',
|
||||||
() => {
|
() => {
|
||||||
if (!canHandleEvent()) return
|
if (!canHandleEvent()) return
|
||||||
|
|
||||||
|
@ -200,7 +202,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
// Undo Redo
|
// Undo Redo
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'command+u,ctrl+u',
|
'⌘+u,ctrl+u',
|
||||||
() => {
|
() => {
|
||||||
if (!canHandleEvent()) return
|
if (!canHandleEvent()) return
|
||||||
app.undoSelect()
|
app.undoSelect()
|
||||||
|
@ -210,7 +212,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'ctrl+shift-u,command+shift+u',
|
'ctrl+shift-u,⌘+shift+u',
|
||||||
() => {
|
() => {
|
||||||
if (!canHandleEvent()) return
|
if (!canHandleEvent()) return
|
||||||
app.redoSelect()
|
app.redoSelect()
|
||||||
|
@ -224,7 +226,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
// Camera
|
// Camera
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'ctrl+=,command+=',
|
'ctrl+=,⌘+=',
|
||||||
(e) => {
|
(e) => {
|
||||||
if (!canHandleEvent()) return
|
if (!canHandleEvent()) return
|
||||||
|
|
||||||
|
@ -236,7 +238,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'ctrl+-,command+-',
|
'ctrl+-,⌘+-',
|
||||||
(e) => {
|
(e) => {
|
||||||
if (!canHandleEvent()) return
|
if (!canHandleEvent()) return
|
||||||
|
|
||||||
|
@ -280,7 +282,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
// Duplicate
|
// Duplicate
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'ctrl+d,command+d',
|
'ctrl+d,⌘+d',
|
||||||
(e) => {
|
(e) => {
|
||||||
if (!canHandleEvent()) return
|
if (!canHandleEvent()) return
|
||||||
|
|
||||||
|
@ -341,7 +343,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
// Select All
|
// Select All
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'command+a,ctrl+a',
|
'⌘+a,ctrl+a',
|
||||||
() => {
|
() => {
|
||||||
if (!canHandleEvent()) return
|
if (!canHandleEvent()) return
|
||||||
app.selectAll()
|
app.selectAll()
|
||||||
|
@ -433,7 +435,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'command+shift+l,ctrl+shift+l',
|
'⌘+shift+l,ctrl+shift+l',
|
||||||
() => {
|
() => {
|
||||||
if (!canHandleEvent()) return
|
if (!canHandleEvent()) return
|
||||||
app.toggleLocked()
|
app.toggleLocked()
|
||||||
|
@ -445,7 +447,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
// Copy, Cut & Paste
|
// Copy, Cut & Paste
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'command+c,ctrl+c',
|
'⌘+c,ctrl+c',
|
||||||
() => {
|
() => {
|
||||||
if (!canHandleEvent()) return
|
if (!canHandleEvent()) return
|
||||||
app.copy()
|
app.copy()
|
||||||
|
@ -455,7 +457,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'command+x,ctrl+x',
|
'⌘+x,ctrl+x',
|
||||||
() => {
|
() => {
|
||||||
if (!canHandleEvent()) return
|
if (!canHandleEvent()) return
|
||||||
app.cut()
|
app.cut()
|
||||||
|
@ -465,7 +467,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'command+v,ctrl+v',
|
'⌘+v,ctrl+v',
|
||||||
() => {
|
() => {
|
||||||
if (!canHandleEvent()) return
|
if (!canHandleEvent()) return
|
||||||
app.paste()
|
app.paste()
|
||||||
|
@ -477,7 +479,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
// Group & Ungroup
|
// Group & Ungroup
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'command+g,ctrl+g',
|
'⌘+g,ctrl+g',
|
||||||
(e) => {
|
(e) => {
|
||||||
if (!canHandleEvent()) return
|
if (!canHandleEvent()) return
|
||||||
|
|
||||||
|
@ -489,7 +491,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'command+shift+g,ctrl+shift+g',
|
'⌘+shift+g,ctrl+shift+g',
|
||||||
(e) => {
|
(e) => {
|
||||||
if (!canHandleEvent()) return
|
if (!canHandleEvent()) return
|
||||||
|
|
||||||
|
@ -543,7 +545,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
)
|
)
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'command+shift+backspace',
|
'ctrl+shift+backspace,⌘+shift+backspace',
|
||||||
(e) => {
|
(e) => {
|
||||||
if (!canHandleEvent()) return
|
if (!canHandleEvent()) return
|
||||||
if (app.settings.isDebugMode) {
|
if (app.settings.isDebugMode) {
|
||||||
|
@ -554,4 +556,39 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||||
undefined,
|
undefined,
|
||||||
[app]
|
[app]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Text Align
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'alt+command+l,alt+ctrl+l',
|
||||||
|
(e) => {
|
||||||
|
if (!canHandleEvent()) return
|
||||||
|
app.style({ textAlign: AlignStyle.Start })
|
||||||
|
e.preventDefault()
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
[app]
|
||||||
|
)
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'alt+command+t,alt+ctrl+t',
|
||||||
|
(e) => {
|
||||||
|
if (!canHandleEvent()) return
|
||||||
|
app.style({ textAlign: AlignStyle.Middle })
|
||||||
|
e.preventDefault()
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
[app]
|
||||||
|
)
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'alt+command+r,alt+ctrl+r',
|
||||||
|
(e) => {
|
||||||
|
if (!canHandleEvent()) return
|
||||||
|
app.style({ textAlign: AlignStyle.End })
|
||||||
|
e.preventDefault()
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
[app]
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ const styles = new Map<string, HTMLStyleElement>()
|
||||||
|
|
||||||
const UID = `Tldraw-fonts`
|
const UID = `Tldraw-fonts`
|
||||||
const CSS = `
|
const CSS = `
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&display=swap')
|
@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&family=Source+Code+Pro&family=Source+Sans+Pro&family=Source+Serif+Pro&display=swap');
|
||||||
`
|
`
|
||||||
|
|
||||||
export function useStylesheet() {
|
export function useStylesheet() {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
import { Patch, StateManager } from 'rko'
|
import { StateManager } from 'rko'
|
||||||
import { Vec } from '@tldraw/vec'
|
import { Vec } from '@tldraw/vec'
|
||||||
import {
|
import {
|
||||||
TLBoundsEventHandler,
|
TLBoundsEventHandler,
|
||||||
|
@ -247,11 +247,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
* @protected
|
* @protected
|
||||||
* @returns The final state
|
* @returns The final state
|
||||||
*/
|
*/
|
||||||
protected cleanup = (
|
protected cleanup = (state: TDSnapshot, prev: TDSnapshot): TDSnapshot => {
|
||||||
state: TDSnapshot,
|
|
||||||
prev: TDSnapshot,
|
|
||||||
patch: Patch<TDSnapshot>
|
|
||||||
): TDSnapshot => {
|
|
||||||
const next = { ...state }
|
const next = { ...state }
|
||||||
|
|
||||||
// Remove deleted shapes and bindings (in Commands, these will be set to undefined)
|
// Remove deleted shapes and bindings (in Commands, these will be set to undefined)
|
||||||
|
@ -2064,7 +2060,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
const shapesToUpdate = shapes.filter((shape) => pageShapes[shape.id])
|
const shapesToUpdate = shapes.filter((shape) => pageShapes[shape.id])
|
||||||
if (shapesToUpdate.length === 0) return this
|
if (shapesToUpdate.length === 0) return this
|
||||||
return this.setState(
|
return this.setState(
|
||||||
Commands.update(this, shapesToUpdate, this.currentPageId),
|
Commands.updateShapes(this, shapesToUpdate, this.currentPageId),
|
||||||
'updated_shapes'
|
'updated_shapes'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2079,7 +2075,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
const shapesToUpdate = shapes.filter((shape) => pageShapes[shape.id])
|
const shapesToUpdate = shapes.filter((shape) => pageShapes[shape.id])
|
||||||
if (shapesToUpdate.length === 0) return this
|
if (shapesToUpdate.length === 0) return this
|
||||||
return this.patchState(
|
return this.patchState(
|
||||||
Commands.update(this, shapesToUpdate, this.currentPageId).after,
|
Commands.updateShapes(this, shapesToUpdate, this.currentPageId).after,
|
||||||
'updated_shapes'
|
'updated_shapes'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2333,6 +2329,15 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
return this.setState(Commands.toggleShapesDecoration(this, ids, handleId))
|
return this.setState(Commands.toggleShapesDecoration(this, ids, handleId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the props of one or more shapes
|
||||||
|
* @param props The props to set on the shapes.
|
||||||
|
* @param ids The ids of the shapes to set props on.
|
||||||
|
*/
|
||||||
|
setShapeProps = <T extends TDShape>(props: Partial<T>, ids = this.selectedIds) => {
|
||||||
|
return this.setState(Commands.setShapesProps(this, ids, props))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rotate one or more shapes by a delta.
|
* Rotate one or more shapes by a delta.
|
||||||
* @param delta The delta in radians.
|
* @param delta The delta in radians.
|
||||||
|
@ -2800,12 +2805,12 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
|
|
||||||
getShapeUtil = TLDR.getShapeUtil
|
getShapeUtil = TLDR.getShapeUtil
|
||||||
|
|
||||||
static version = 13
|
static version = 14
|
||||||
|
|
||||||
static defaultDocument: TDDocument = {
|
static defaultDocument: TDDocument = {
|
||||||
id: 'doc',
|
id: 'doc',
|
||||||
name: 'New Document',
|
name: 'New Document',
|
||||||
version: 13,
|
version: 14,
|
||||||
pages: {
|
pages: {
|
||||||
page: {
|
page: {
|
||||||
id: 'page',
|
id: 'page',
|
||||||
|
@ -2843,15 +2848,14 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||||
showCloneHandles: false,
|
showCloneHandles: false,
|
||||||
},
|
},
|
||||||
appState: {
|
appState: {
|
||||||
|
status: TDStatus.Idle,
|
||||||
activeTool: 'select',
|
activeTool: 'select',
|
||||||
hoveredId: undefined,
|
hoveredId: undefined,
|
||||||
currentPageId: 'page',
|
currentPageId: 'page',
|
||||||
pages: [{ id: 'page', name: 'page', childIndex: 1 }],
|
|
||||||
currentStyle: defaultStyle,
|
currentStyle: defaultStyle,
|
||||||
isToolLocked: false,
|
isToolLocked: false,
|
||||||
isStyleOpen: false,
|
isStyleOpen: false,
|
||||||
isEmptyCanvas: false,
|
isEmptyCanvas: false,
|
||||||
status: TDStatus.Idle,
|
|
||||||
snapLines: [],
|
snapLines: [],
|
||||||
},
|
},
|
||||||
document: TldrawApp.defaultDocument,
|
document: TldrawApp.defaultDocument,
|
||||||
|
|
|
@ -21,3 +21,4 @@ export * from './toggleShapesProp'
|
||||||
export * from './translateShapes'
|
export * from './translateShapes'
|
||||||
export * from './ungroupShapes'
|
export * from './ungroupShapes'
|
||||||
export * from './updateShapes'
|
export * from './updateShapes'
|
||||||
|
export * from './setShapesProps'
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './setShapesProps'
|
|
@ -0,0 +1,3 @@
|
||||||
|
describe('Set shapes props command', () => {
|
||||||
|
it.todo('sets the props of the provided shapes')
|
||||||
|
})
|
|
@ -0,0 +1,56 @@
|
||||||
|
import type { TDShape, TldrawCommand } from '~types'
|
||||||
|
import type { TldrawApp } from '~state'
|
||||||
|
|
||||||
|
export function setShapesProps<T extends TDShape>(
|
||||||
|
app: TldrawApp,
|
||||||
|
ids: string[],
|
||||||
|
partial: Partial<T>
|
||||||
|
): TldrawCommand {
|
||||||
|
const { currentPageId, selectedIds } = app
|
||||||
|
|
||||||
|
const initialShapes = ids
|
||||||
|
.map((id) => app.getShape<T>(id))
|
||||||
|
.filter((shape) => (partial['isLocked'] ? true : !shape.isLocked))
|
||||||
|
|
||||||
|
const before: Record<string, Partial<TDShape>> = {}
|
||||||
|
const after: Record<string, Partial<TDShape>> = {}
|
||||||
|
|
||||||
|
const keys = Object.keys(partial) as (keyof T)[]
|
||||||
|
|
||||||
|
initialShapes.forEach((shape) => {
|
||||||
|
before[shape.id] = Object.fromEntries(keys.map((key) => [key, shape[key]]))
|
||||||
|
after[shape.id] = partial
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: 'set_props',
|
||||||
|
before: {
|
||||||
|
document: {
|
||||||
|
pages: {
|
||||||
|
[currentPageId]: {
|
||||||
|
shapes: before,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pageStates: {
|
||||||
|
[currentPageId]: {
|
||||||
|
selectedIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
after: {
|
||||||
|
document: {
|
||||||
|
pages: {
|
||||||
|
[currentPageId]: {
|
||||||
|
shapes: after,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pageStates: {
|
||||||
|
[currentPageId]: {
|
||||||
|
selectedIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,7 @@ import type { TldrawCommand, TDShape } from '~types'
|
||||||
import { TLDR } from '~state/TLDR'
|
import { TLDR } from '~state/TLDR'
|
||||||
import type { TldrawApp } from '../../internal'
|
import type { TldrawApp } from '../../internal'
|
||||||
|
|
||||||
export function update(
|
export function updateShapes(
|
||||||
app: TldrawApp,
|
app: TldrawApp,
|
||||||
updates: ({ id: string } & Partial<TDShape>)[],
|
updates: ({ id: string } & Partial<TDShape>)[],
|
||||||
pageId: string
|
pageId: string
|
||||||
|
|
|
@ -1,11 +1,19 @@
|
||||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||||
import { Decoration, TDDocument, TDShapeType } from '~types'
|
import { Decoration, FontStyle, TDDocument, TDShapeType, TextShape } from '~types'
|
||||||
|
|
||||||
export function migrate(document: TDDocument, newVersion: number): TDDocument {
|
export function migrate(document: TDDocument, newVersion: number): TDDocument {
|
||||||
const { version = 0 } = document
|
const { version = 0 } = document
|
||||||
|
|
||||||
if (version === newVersion) return document
|
if (version === newVersion) return document
|
||||||
|
|
||||||
|
if (version < 14) {
|
||||||
|
Object.values(document.pages).forEach((page) => {
|
||||||
|
Object.values(page.shapes)
|
||||||
|
.filter((shape) => shape.type === TDShapeType.Text)
|
||||||
|
.forEach((shape) => (shape as TextShape).style.font === FontStyle.Script)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Lowercase styles, move binding meta to binding
|
// Lowercase styles, move binding meta to binding
|
||||||
if (version <= 13) {
|
if (version <= 13) {
|
||||||
Object.values(document.pages).forEach((page) => {
|
Object.values(document.pages).forEach((page) => {
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { Utils, HTMLContainer, TLBounds } from '@tldraw/core'
|
import { Utils, HTMLContainer, TLBounds } from '@tldraw/core'
|
||||||
import { defaultStyle } from '../shared/shape-styles'
|
import { defaultTextStyle } from '../shared/shape-styles'
|
||||||
import { StickyShape, TDMeta, TDShapeType, TransformInfo } from '~types'
|
import { AlignStyle, StickyShape, TDMeta, TDShapeType, TransformInfo } from '~types'
|
||||||
import { getBoundsRectangle, TextAreaUtils } from '../shared'
|
import { getBoundsRectangle, TextAreaUtils } from '../shared'
|
||||||
import { TDShapeUtil } from '../TDShapeUtil'
|
import { TDShapeUtil } from '../TDShapeUtil'
|
||||||
import { getStickyFontStyle, getStickyShapeStyle } from '../shared/shape-styles'
|
import { getStickyFontStyle, getStickyShapeStyle } from '../shared/shape-styles'
|
||||||
import { styled } from '~styles'
|
import { styled } from '~styles'
|
||||||
import Vec from '@tldraw/vec'
|
import { Vec } from '@tldraw/vec'
|
||||||
import { GHOSTED_OPACITY } from '~constants'
|
import { GHOSTED_OPACITY } from '~constants'
|
||||||
import { TLDR } from '~state/TLDR'
|
import { TLDR } from '~state/TLDR'
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ export class StickyUtil extends TDShapeUtil<T, E> {
|
||||||
size: [200, 200],
|
size: [200, 200],
|
||||||
text: '',
|
text: '',
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
style: defaultStyle,
|
style: defaultTextStyle,
|
||||||
},
|
},
|
||||||
props
|
props
|
||||||
)
|
)
|
||||||
|
@ -165,7 +165,7 @@ export class StickyUtil extends TDShapeUtil<T, E> {
|
||||||
isGhost={isGhost}
|
isGhost={isGhost}
|
||||||
style={{ backgroundColor: fill, ...style }}
|
style={{ backgroundColor: fill, ...style }}
|
||||||
>
|
>
|
||||||
<StyledText ref={rText} isEditing={isEditing}>
|
<StyledText ref={rText} isEditing={isEditing} alignment={shape.style.textAlign}>
|
||||||
{shape.text}​
|
{shape.text}​
|
||||||
</StyledText>
|
</StyledText>
|
||||||
{isEditing && (
|
{isEditing && (
|
||||||
|
@ -184,6 +184,7 @@ export class StickyUtil extends TDShapeUtil<T, E> {
|
||||||
autoSave="false"
|
autoSave="false"
|
||||||
autoFocus
|
autoFocus
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
|
alignment={shape.style.textAlign}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</StyledStickyContainer>
|
</StyledStickyContainer>
|
||||||
|
@ -291,6 +292,20 @@ const StyledText = styled('div', {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
alignment: {
|
||||||
|
[AlignStyle.Start]: {
|
||||||
|
textAlign: 'left',
|
||||||
|
},
|
||||||
|
[AlignStyle.Middle]: {
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
[AlignStyle.End]: {
|
||||||
|
textAlign: 'right',
|
||||||
|
},
|
||||||
|
[AlignStyle.Justify]: {
|
||||||
|
textAlign: 'justify',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
...commonTextWrapping,
|
...commonTextWrapping,
|
||||||
})
|
})
|
||||||
|
@ -310,4 +325,20 @@ const StyledTextArea = styled('textarea', {
|
||||||
resize: 'none',
|
resize: 'none',
|
||||||
caretColor: 'black',
|
caretColor: 'black',
|
||||||
...commonTextWrapping,
|
...commonTextWrapping,
|
||||||
|
variants: {
|
||||||
|
alignment: {
|
||||||
|
[AlignStyle.Start]: {
|
||||||
|
textAlign: 'left',
|
||||||
|
},
|
||||||
|
[AlignStyle.Middle]: {
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
[AlignStyle.End]: {
|
||||||
|
textAlign: 'right',
|
||||||
|
},
|
||||||
|
[AlignStyle.Justify]: {
|
||||||
|
textAlign: 'justify',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { Utils, HTMLContainer, TLBounds } from '@tldraw/core'
|
import { Utils, HTMLContainer, TLBounds } from '@tldraw/core'
|
||||||
import { defaultStyle, getShapeStyle, getFontStyle } from '../shared/shape-styles'
|
import { defaultTextStyle, getShapeStyle, getFontStyle } from '../shared/shape-styles'
|
||||||
import { TextShape, TDMeta, TDShapeType, TransformInfo } from '~types'
|
import { TextShape, TDMeta, TDShapeType, TransformInfo, AlignStyle } from '~types'
|
||||||
import { TextAreaUtils } from '../shared'
|
import { TextAreaUtils } from '../shared'
|
||||||
import { BINDING_DISTANCE, GHOSTED_OPACITY } from '~constants'
|
import { BINDING_DISTANCE, GHOSTED_OPACITY } from '~constants'
|
||||||
import { TDShapeUtil } from '../TDShapeUtil'
|
import { TDShapeUtil } from '../TDShapeUtil'
|
||||||
import { styled } from '~styles'
|
import { styled } from '~styles'
|
||||||
import Vec from '@tldraw/vec'
|
import { Vec } from '@tldraw/vec'
|
||||||
import { TLDR } from '~state/TLDR'
|
import { TLDR } from '~state/TLDR'
|
||||||
|
import { getTextAlign } from '../shared/getTextAlign'
|
||||||
|
|
||||||
type T = TextShape
|
type T = TextShape
|
||||||
type E = HTMLDivElement
|
type E = HTMLDivElement
|
||||||
|
@ -33,7 +34,7 @@ export class TextUtil extends TDShapeUtil<T, E> {
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
text: ' ',
|
text: ' ',
|
||||||
style: defaultStyle,
|
style: defaultTextStyle,
|
||||||
},
|
},
|
||||||
props
|
props
|
||||||
)
|
)
|
||||||
|
@ -50,7 +51,39 @@ export class TextUtil extends TDShapeUtil<T, E> {
|
||||||
|
|
||||||
const handleChange = React.useCallback(
|
const handleChange = React.useCallback(
|
||||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
onShapeChange?.({ ...shape, text: TLDR.normalizeText(e.currentTarget.value) })
|
let delta = [0, 0]
|
||||||
|
|
||||||
|
const currentBounds = this.getBounds(shape)
|
||||||
|
|
||||||
|
switch (shape.style.textAlign) {
|
||||||
|
case AlignStyle.Start: {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case AlignStyle.Middle: {
|
||||||
|
const nextBounds = this.getBounds({
|
||||||
|
...shape,
|
||||||
|
text: TLDR.normalizeText(e.currentTarget.value),
|
||||||
|
})
|
||||||
|
|
||||||
|
delta = Vec.div([nextBounds.width - currentBounds.width, 0], 2)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case AlignStyle.End: {
|
||||||
|
const nextBounds = this.getBounds({
|
||||||
|
...shape,
|
||||||
|
text: TLDR.normalizeText(e.currentTarget.value),
|
||||||
|
})
|
||||||
|
|
||||||
|
delta = [nextBounds.width - currentBounds.width, 0]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onShapeChange?.({
|
||||||
|
...shape,
|
||||||
|
point: Vec.sub(shape.point, delta),
|
||||||
|
text: TLDR.normalizeText(e.currentTarget.value),
|
||||||
|
})
|
||||||
},
|
},
|
||||||
[shape]
|
[shape]
|
||||||
)
|
)
|
||||||
|
@ -126,6 +159,7 @@ export class TextUtil extends TDShapeUtil<T, E> {
|
||||||
style={{
|
style={{
|
||||||
font,
|
font,
|
||||||
color: styles.stroke,
|
color: styles.stroke,
|
||||||
|
textAlign: getTextAlign(style.textAlign),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isBinding && (
|
{isBinding && (
|
||||||
|
@ -147,6 +181,7 @@ export class TextUtil extends TDShapeUtil<T, E> {
|
||||||
style={{
|
style={{
|
||||||
font,
|
font,
|
||||||
color: styles.stroke,
|
color: styles.stroke,
|
||||||
|
textAlign: 'inherit',
|
||||||
}}
|
}}
|
||||||
name="text"
|
name="text"
|
||||||
defaultValue={text}
|
defaultValue={text}
|
||||||
|
|
|
@ -14,9 +14,11 @@ Object {
|
||||||
"style": Object {
|
"style": Object {
|
||||||
"color": "black",
|
"color": "black",
|
||||||
"dash": "draw",
|
"dash": "draw",
|
||||||
|
"font": "script",
|
||||||
"isFilled": false,
|
"isFilled": false,
|
||||||
"scale": 1,
|
"scale": 1,
|
||||||
"size": "small",
|
"size": "small",
|
||||||
|
"textAlign": "start",
|
||||||
},
|
},
|
||||||
"text": " ",
|
"text": " ",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
|
|
12
packages/tldraw/src/state/shapes/shared/getTextAlign.ts
Normal file
12
packages/tldraw/src/state/shapes/shared/getTextAlign.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { AlignStyle } from '~types'
|
||||||
|
|
||||||
|
const ALIGN_VALUES = {
|
||||||
|
[AlignStyle.Start]: 'left',
|
||||||
|
[AlignStyle.Middle]: 'center',
|
||||||
|
[AlignStyle.End]: 'right',
|
||||||
|
[AlignStyle.Justify]: 'justify',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export function getTextAlign(alignStyle: AlignStyle = AlignStyle.Start) {
|
||||||
|
return ALIGN_VALUES[alignStyle]
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import { Utils } from '@tldraw/core'
|
import { Utils } from '@tldraw/core'
|
||||||
import { Theme, ColorStyle, DashStyle, ShapeStyles, SizeStyle } from '~types'
|
import { Theme, ColorStyle, DashStyle, ShapeStyles, SizeStyle, FontStyle, AlignStyle } from '~types'
|
||||||
|
|
||||||
const canvasLight = '#fafafa'
|
const canvasLight = '#fafafa'
|
||||||
|
|
||||||
|
@ -84,6 +84,20 @@ const fontSizes = {
|
||||||
auto: 'auto',
|
auto: 'auto',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fontFaces = {
|
||||||
|
[FontStyle.Script]: '"Caveat Brush"',
|
||||||
|
[FontStyle.Sans]: '"Source Sans Pro", sans-serif',
|
||||||
|
[FontStyle.Serif]: '"Source Serif Pro", serif',
|
||||||
|
[FontStyle.Mono]: '"Source Code Pro", monospace',
|
||||||
|
}
|
||||||
|
|
||||||
|
const fontSizeModifiers = {
|
||||||
|
[FontStyle.Script]: 1,
|
||||||
|
[FontStyle.Sans]: 1,
|
||||||
|
[FontStyle.Serif]: 1,
|
||||||
|
[FontStyle.Mono]: 1,
|
||||||
|
}
|
||||||
|
|
||||||
const stickyFontSizes = {
|
const stickyFontSizes = {
|
||||||
[SizeStyle.Small]: 24,
|
[SizeStyle.Small]: 24,
|
||||||
[SizeStyle.Medium]: 36,
|
[SizeStyle.Medium]: 36,
|
||||||
|
@ -95,8 +109,12 @@ export function getStrokeWidth(size: SizeStyle): number {
|
||||||
return strokeWidths[size]
|
return strokeWidths[size]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFontSize(size: SizeStyle): number {
|
export function getFontSize(size: SizeStyle, fontStyle: FontStyle = FontStyle.Script): number {
|
||||||
return fontSizes[size]
|
return fontSizes[size] * fontSizeModifiers[fontStyle]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFontFace(font: FontStyle = FontStyle.Script): string {
|
||||||
|
return fontFaces[font]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getStickyFontSize(size: SizeStyle): number {
|
export function getStickyFontSize(size: SizeStyle): number {
|
||||||
|
@ -104,17 +122,19 @@ export function getStickyFontSize(size: SizeStyle): number {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFontStyle(style: ShapeStyles): string {
|
export function getFontStyle(style: ShapeStyles): string {
|
||||||
const fontSize = getFontSize(style.size)
|
const fontSize = getFontSize(style.size, style.font)
|
||||||
|
const fontFace = getFontFace(style.font)
|
||||||
const { scale = 1 } = style
|
const { scale = 1 } = style
|
||||||
|
|
||||||
return `${fontSize * scale}px/1.3 "Caveat Brush"`
|
return `${fontSize * scale}px/1.3 ${fontFace}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getStickyFontStyle(style: ShapeStyles): string {
|
export function getStickyFontStyle(style: ShapeStyles): string {
|
||||||
const fontSize = getStickyFontSize(style.size)
|
const fontSize = getStickyFontSize(style.size)
|
||||||
|
const fontFace = getFontFace(style.font)
|
||||||
const { scale = 1 } = style
|
const { scale = 1 } = style
|
||||||
|
|
||||||
return `${fontSize * scale}px/1.3 "Caveat Brush"`
|
return `${fontSize * scale}px/1.3 ${fontFace}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getStickyShapeStyle(style: ShapeStyles, isDarkMode = false) {
|
export function getStickyShapeStyle(style: ShapeStyles, isDarkMode = false) {
|
||||||
|
@ -158,3 +178,9 @@ export const defaultStyle: ShapeStyles = {
|
||||||
dash: DashStyle.Draw,
|
dash: DashStyle.Draw,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const defaultTextStyle: ShapeStyles = {
|
||||||
|
...defaultStyle,
|
||||||
|
font: FontStyle.Script,
|
||||||
|
textAlign: AlignStyle.Start,
|
||||||
|
}
|
||||||
|
|
|
@ -94,7 +94,6 @@ export interface TDSnapshot {
|
||||||
appState: {
|
appState: {
|
||||||
currentStyle: ShapeStyles
|
currentStyle: ShapeStyles
|
||||||
currentPageId: string
|
currentPageId: string
|
||||||
pages: Pick<TLPage<TDShape, TDBinding>, 'id' | 'name' | 'childIndex'>[]
|
|
||||||
hoveredId?: string
|
hoveredId?: string
|
||||||
activeTool: TDToolType
|
activeTool: TDToolType
|
||||||
isToolLocked: boolean
|
isToolLocked: boolean
|
||||||
|
@ -401,10 +400,26 @@ export enum FontSize {
|
||||||
ExtraLarge = 'extraLarge',
|
ExtraLarge = 'extraLarge',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum AlignStyle {
|
||||||
|
Start = 'start',
|
||||||
|
Middle = 'middle',
|
||||||
|
End = 'end',
|
||||||
|
Justify = 'justify',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum FontStyle {
|
||||||
|
Script = 'script',
|
||||||
|
Sans = 'sans',
|
||||||
|
Serif = 'erif',
|
||||||
|
Mono = 'mono',
|
||||||
|
}
|
||||||
|
|
||||||
export type ShapeStyles = {
|
export type ShapeStyles = {
|
||||||
color: ColorStyle
|
color: ColorStyle
|
||||||
size: SizeStyle
|
size: SizeStyle
|
||||||
dash: DashStyle
|
dash: DashStyle
|
||||||
|
font?: FontStyle
|
||||||
|
textAlign?: AlignStyle
|
||||||
isFilled?: boolean
|
isFilled?: boolean
|
||||||
scale?: number
|
scale?: number
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue