[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
|
||||
disabled?: boolean
|
||||
kbd?: string
|
||||
variant?: 'wide'
|
||||
variant?: 'wide' | 'styleMenu'
|
||||
isSponsor?: boolean
|
||||
isActive?: boolean
|
||||
isWarning?: boolean
|
||||
|
@ -130,6 +130,9 @@ export const StyledRowButton = styled('button', {
|
|||
small: {},
|
||||
},
|
||||
variant: {
|
||||
styleMenu: {
|
||||
margin: '$1 0 $1 0',
|
||||
},
|
||||
wide: {
|
||||
gridColumn: '1 / span 4',
|
||||
},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from 'react'
|
||||
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 {
|
||||
DMCheckboxItem,
|
||||
|
@ -19,37 +19,65 @@ import {
|
|||
SizeSmallIcon,
|
||||
} from '~components/Primitives/icons'
|
||||
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 { breakpoints } from '~components/breakpoints'
|
||||
import { Divider } from '~components/Primitives/Divider'
|
||||
import { preventEvent } from '~components/preventEvent'
|
||||
import {
|
||||
TextAlignCenterIcon,
|
||||
TextAlignJustifyIcon,
|
||||
TextAlignLeftIcon,
|
||||
TextAlignRightIcon,
|
||||
} from '@radix-ui/react-icons'
|
||||
|
||||
const currentStyleSelector = (s: TDSnapshot) => s.appState.currentStyle
|
||||
const selectedIdsSelector = (s: TDSnapshot) =>
|
||||
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.Solid]: <DashSolidIcon />,
|
||||
[DashStyle.Dashed]: <DashDashedIcon />,
|
||||
[DashStyle.Dotted]: <DashDottedIcon />,
|
||||
}
|
||||
|
||||
const SIZES = {
|
||||
const SIZE_ICONS = {
|
||||
[SizeStyle.Small]: <SizeSmallIcon />,
|
||||
[SizeStyle.Medium]: <SizeMediumIcon />,
|
||||
[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 {
|
||||
const app = useTldrawApp()
|
||||
|
||||
const theme = app.useStore(themeSelector)
|
||||
const showTextStyles = app.useStore(showTextStylesSelector)
|
||||
|
||||
const currentStyle = app.useStore(currentStyleSelector)
|
||||
const selectedIds = app.useStore(selectedIdsSelector)
|
||||
|
@ -111,6 +139,14 @@ export const StyleMenu = React.memo(function ColorMenu(): JSX.Element {
|
|||
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 (
|
||||
<DropdownMenu.Root dir="ltr">
|
||||
<DMTriggerIcon>
|
||||
|
@ -126,53 +162,56 @@ export const StyleMenu = React.memo(function ColorMenu(): JSX.Element {
|
|||
fill={fills[theme][displayedStyle.color as ColorStyle]}
|
||||
/>
|
||||
)}
|
||||
{DASHES[displayedStyle.dash]}
|
||||
{DASH_ICONS[displayedStyle.dash]}
|
||||
</OverlapIcons>
|
||||
</DMTriggerIcon>
|
||||
<DMContent>
|
||||
<StyledRow variant="tall">
|
||||
<span>Color</span>
|
||||
<ColorGrid>
|
||||
{Object.keys(strokes.light).map((colorStyle: string) => (
|
||||
<DropdownMenu.Item key={colorStyle} onSelect={preventEvent} asChild>
|
||||
{Object.keys(strokes.light).map((style: string) => (
|
||||
<DropdownMenu.Item key={style} onSelect={preventEvent} asChild>
|
||||
<ToolButton
|
||||
variant="icon"
|
||||
isActive={displayedStyle.color === colorStyle}
|
||||
onClick={() => app.style({ color: colorStyle as ColorStyle })}
|
||||
isActive={displayedStyle.color === style}
|
||||
onClick={() => app.style({ color: style as ColorStyle })}
|
||||
>
|
||||
<CircleIcon
|
||||
size={18}
|
||||
strokeWidth={2.5}
|
||||
fill={
|
||||
displayedStyle.isFilled
|
||||
? fills.light[colorStyle as ColorStyle]
|
||||
: 'transparent'
|
||||
displayedStyle.isFilled ? fills.light[style as ColorStyle] : 'transparent'
|
||||
}
|
||||
stroke={strokes.light[colorStyle as ColorStyle]}
|
||||
stroke={strokes.light[style as ColorStyle]}
|
||||
/>
|
||||
</ToolButton>
|
||||
</DropdownMenu.Item>
|
||||
))}
|
||||
</ColorGrid>
|
||||
</StyledRow>
|
||||
<Divider />
|
||||
<DMCheckboxItem
|
||||
variant="styleMenu"
|
||||
checked={!!displayedStyle.isFilled}
|
||||
onCheckedChange={handleToggleFilled}
|
||||
>
|
||||
Fill
|
||||
</DMCheckboxItem>
|
||||
<StyledRow>
|
||||
Dash
|
||||
<StyledGroup dir="ltr" value={displayedStyle.dash} onValueChange={handleDashChange}>
|
||||
{Object.values(DashStyle).map((dashStyle) => (
|
||||
{Object.values(DashStyle).map((style) => (
|
||||
<DMRadioItem
|
||||
key={dashStyle}
|
||||
isActive={dashStyle === displayedStyle.dash}
|
||||
value={dashStyle}
|
||||
key={style}
|
||||
isActive={style === displayedStyle.dash}
|
||||
value={style}
|
||||
onSelect={preventEvent}
|
||||
bp={breakpoints}
|
||||
>
|
||||
{DASHES[dashStyle as DashStyle]}
|
||||
{DASH_ICONS[style as DashStyle]}
|
||||
</DMRadioItem>
|
||||
))}
|
||||
</StyledGroup>
|
||||
</StyledRow>
|
||||
<Divider />
|
||||
<StyledRow>
|
||||
Size
|
||||
<StyledGroup dir="ltr" value={displayedStyle.size} onValueChange={handleSizeChange}>
|
||||
|
@ -184,15 +223,52 @@ export const StyleMenu = React.memo(function ColorMenu(): JSX.Element {
|
|||
onSelect={preventEvent}
|
||||
bp={breakpoints}
|
||||
>
|
||||
{SIZES[sizeStyle as SizeStyle]}
|
||||
{SIZE_ICONS[sizeStyle as SizeStyle]}
|
||||
</DMRadioItem>
|
||||
))}
|
||||
</StyledGroup>
|
||||
</StyledRow>
|
||||
<Divider />
|
||||
<DMCheckboxItem checked={!!displayedStyle.isFilled} onCheckedChange={handleToggleFilled}>
|
||||
Fill
|
||||
</DMCheckboxItem>
|
||||
{showTextStyles && (
|
||||
<>
|
||||
<Divider />
|
||||
<StyledRow>
|
||||
Font
|
||||
<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>
|
||||
</DropdownMenu.Root>
|
||||
)
|
||||
|
@ -237,7 +313,7 @@ export const StyledRow = styled('div', {
|
|||
fontFamily: '$ui',
|
||||
fontWeight: 400,
|
||||
fontSize: '$1',
|
||||
padding: '0 0 0 $3',
|
||||
padding: '$2 0 $2 $3',
|
||||
borderRadius: 4,
|
||||
userSelect: 'none',
|
||||
margin: 0,
|
||||
|
@ -250,6 +326,7 @@ export const StyledRow = styled('div', {
|
|||
variant: {
|
||||
tall: {
|
||||
alignItems: 'flex-start',
|
||||
padding: '0 0 0 $3',
|
||||
'& > span': {
|
||||
paddingTop: '$4',
|
||||
},
|
||||
|
@ -261,6 +338,7 @@ export const StyledRow = styled('div', {
|
|||
const StyledGroup = styled(DropdownMenu.DropdownMenuRadioGroup, {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: '$1',
|
||||
})
|
||||
|
||||
const OverlapIcons = styled('div', {
|
||||
|
@ -270,3 +348,28 @@ const OverlapIcons = styled('div', {
|
|||
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 { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { TDShapeType } from '~types'
|
||||
import { AlignStyle, TDShapeType } from '~types'
|
||||
import { useFileSystemHandlers, useTldrawApp } from '~hooks'
|
||||
|
||||
export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
||||
|
@ -97,7 +97,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
// Dark Mode
|
||||
|
||||
useHotkeys(
|
||||
'ctrl+shift+d,command+shift+d',
|
||||
'ctrl+shift+d,⌘+shift+d',
|
||||
(e) => {
|
||||
if (!canHandleEvent()) return
|
||||
app.toggleDarkMode()
|
||||
|
@ -110,7 +110,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
// Focus Mode
|
||||
|
||||
useHotkeys(
|
||||
'ctrl+.,command+.',
|
||||
'ctrl+.,⌘+.',
|
||||
() => {
|
||||
if (!canHandleEvent()) return
|
||||
app.toggleFocusMode()
|
||||
|
@ -124,7 +124,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
const { onNewProject, onOpenProject, onSaveProject, onSaveProjectAs } = useFileSystemHandlers()
|
||||
|
||||
useHotkeys(
|
||||
'ctrl+n,command+n',
|
||||
'ctrl+n,⌘+n',
|
||||
(e) => {
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
|
@ -134,7 +134,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
[app]
|
||||
)
|
||||
useHotkeys(
|
||||
'ctrl+s,command+s',
|
||||
'ctrl+s,⌘+s',
|
||||
(e) => {
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
|
@ -145,7 +145,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
)
|
||||
|
||||
useHotkeys(
|
||||
'ctrl+shift+s,command+shift+s',
|
||||
'ctrl+shift+s,⌘+shift+s',
|
||||
(e) => {
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
|
@ -155,7 +155,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
[app]
|
||||
)
|
||||
useHotkeys(
|
||||
'ctrl+o,command+o',
|
||||
'ctrl+o,⌘+o',
|
||||
(e) => {
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
|
@ -168,10 +168,12 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
// Undo Redo
|
||||
|
||||
useHotkeys(
|
||||
'command+z,ctrl+z',
|
||||
'⌘+z,ctrl+z',
|
||||
() => {
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
console.log('Hello')
|
||||
|
||||
if (app.session) {
|
||||
app.cancelSession()
|
||||
} else {
|
||||
|
@ -183,7 +185,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
)
|
||||
|
||||
useHotkeys(
|
||||
'ctrl+shift-z,command+shift+z',
|
||||
'ctrl+shift-z,⌘+shift+z',
|
||||
() => {
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
|
@ -200,7 +202,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
// Undo Redo
|
||||
|
||||
useHotkeys(
|
||||
'command+u,ctrl+u',
|
||||
'⌘+u,ctrl+u',
|
||||
() => {
|
||||
if (!canHandleEvent()) return
|
||||
app.undoSelect()
|
||||
|
@ -210,7 +212,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
)
|
||||
|
||||
useHotkeys(
|
||||
'ctrl+shift-u,command+shift+u',
|
||||
'ctrl+shift-u,⌘+shift+u',
|
||||
() => {
|
||||
if (!canHandleEvent()) return
|
||||
app.redoSelect()
|
||||
|
@ -224,7 +226,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
// Camera
|
||||
|
||||
useHotkeys(
|
||||
'ctrl+=,command+=',
|
||||
'ctrl+=,⌘+=',
|
||||
(e) => {
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
|
@ -236,7 +238,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
)
|
||||
|
||||
useHotkeys(
|
||||
'ctrl+-,command+-',
|
||||
'ctrl+-,⌘+-',
|
||||
(e) => {
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
|
@ -280,7 +282,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
// Duplicate
|
||||
|
||||
useHotkeys(
|
||||
'ctrl+d,command+d',
|
||||
'ctrl+d,⌘+d',
|
||||
(e) => {
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
|
@ -341,7 +343,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
// Select All
|
||||
|
||||
useHotkeys(
|
||||
'command+a,ctrl+a',
|
||||
'⌘+a,ctrl+a',
|
||||
() => {
|
||||
if (!canHandleEvent()) return
|
||||
app.selectAll()
|
||||
|
@ -433,7 +435,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
)
|
||||
|
||||
useHotkeys(
|
||||
'command+shift+l,ctrl+shift+l',
|
||||
'⌘+shift+l,ctrl+shift+l',
|
||||
() => {
|
||||
if (!canHandleEvent()) return
|
||||
app.toggleLocked()
|
||||
|
@ -445,7 +447,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
// Copy, Cut & Paste
|
||||
|
||||
useHotkeys(
|
||||
'command+c,ctrl+c',
|
||||
'⌘+c,ctrl+c',
|
||||
() => {
|
||||
if (!canHandleEvent()) return
|
||||
app.copy()
|
||||
|
@ -455,7 +457,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
)
|
||||
|
||||
useHotkeys(
|
||||
'command+x,ctrl+x',
|
||||
'⌘+x,ctrl+x',
|
||||
() => {
|
||||
if (!canHandleEvent()) return
|
||||
app.cut()
|
||||
|
@ -465,7 +467,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
)
|
||||
|
||||
useHotkeys(
|
||||
'command+v,ctrl+v',
|
||||
'⌘+v,ctrl+v',
|
||||
() => {
|
||||
if (!canHandleEvent()) return
|
||||
app.paste()
|
||||
|
@ -477,7 +479,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
// Group & Ungroup
|
||||
|
||||
useHotkeys(
|
||||
'command+g,ctrl+g',
|
||||
'⌘+g,ctrl+g',
|
||||
(e) => {
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
|
@ -489,7 +491,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
)
|
||||
|
||||
useHotkeys(
|
||||
'command+shift+g,ctrl+shift+g',
|
||||
'⌘+shift+g,ctrl+shift+g',
|
||||
(e) => {
|
||||
if (!canHandleEvent()) return
|
||||
|
||||
|
@ -543,7 +545,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
)
|
||||
|
||||
useHotkeys(
|
||||
'command+shift+backspace',
|
||||
'ctrl+shift+backspace,⌘+shift+backspace',
|
||||
(e) => {
|
||||
if (!canHandleEvent()) return
|
||||
if (app.settings.isDebugMode) {
|
||||
|
@ -554,4 +556,39 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
|
|||
undefined,
|
||||
[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 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() {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { Patch, StateManager } from 'rko'
|
||||
import { StateManager } from 'rko'
|
||||
import { Vec } from '@tldraw/vec'
|
||||
import {
|
||||
TLBoundsEventHandler,
|
||||
|
@ -247,11 +247,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
* @protected
|
||||
* @returns The final state
|
||||
*/
|
||||
protected cleanup = (
|
||||
state: TDSnapshot,
|
||||
prev: TDSnapshot,
|
||||
patch: Patch<TDSnapshot>
|
||||
): TDSnapshot => {
|
||||
protected cleanup = (state: TDSnapshot, prev: TDSnapshot): TDSnapshot => {
|
||||
const next = { ...state }
|
||||
|
||||
// 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])
|
||||
if (shapesToUpdate.length === 0) return this
|
||||
return this.setState(
|
||||
Commands.update(this, shapesToUpdate, this.currentPageId),
|
||||
Commands.updateShapes(this, shapesToUpdate, this.currentPageId),
|
||||
'updated_shapes'
|
||||
)
|
||||
}
|
||||
|
@ -2079,7 +2075,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
const shapesToUpdate = shapes.filter((shape) => pageShapes[shape.id])
|
||||
if (shapesToUpdate.length === 0) return this
|
||||
return this.patchState(
|
||||
Commands.update(this, shapesToUpdate, this.currentPageId).after,
|
||||
Commands.updateShapes(this, shapesToUpdate, this.currentPageId).after,
|
||||
'updated_shapes'
|
||||
)
|
||||
}
|
||||
|
@ -2333,6 +2329,15 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
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.
|
||||
* @param delta The delta in radians.
|
||||
|
@ -2800,12 +2805,12 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
|
||||
getShapeUtil = TLDR.getShapeUtil
|
||||
|
||||
static version = 13
|
||||
static version = 14
|
||||
|
||||
static defaultDocument: TDDocument = {
|
||||
id: 'doc',
|
||||
name: 'New Document',
|
||||
version: 13,
|
||||
version: 14,
|
||||
pages: {
|
||||
page: {
|
||||
id: 'page',
|
||||
|
@ -2843,15 +2848,14 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
|||
showCloneHandles: false,
|
||||
},
|
||||
appState: {
|
||||
status: TDStatus.Idle,
|
||||
activeTool: 'select',
|
||||
hoveredId: undefined,
|
||||
currentPageId: 'page',
|
||||
pages: [{ id: 'page', name: 'page', childIndex: 1 }],
|
||||
currentStyle: defaultStyle,
|
||||
isToolLocked: false,
|
||||
isStyleOpen: false,
|
||||
isEmptyCanvas: false,
|
||||
status: TDStatus.Idle,
|
||||
snapLines: [],
|
||||
},
|
||||
document: TldrawApp.defaultDocument,
|
||||
|
|
|
@ -21,3 +21,4 @@ export * from './toggleShapesProp'
|
|||
export * from './translateShapes'
|
||||
export * from './ungroupShapes'
|
||||
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 type { TldrawApp } from '../../internal'
|
||||
|
||||
export function update(
|
||||
export function updateShapes(
|
||||
app: TldrawApp,
|
||||
updates: ({ id: string } & Partial<TDShape>)[],
|
||||
pageId: string
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
/* 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 {
|
||||
const { version = 0 } = 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
|
||||
if (version <= 13) {
|
||||
Object.values(document.pages).forEach((page) => {
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import * as React from 'react'
|
||||
import { Utils, HTMLContainer, TLBounds } from '@tldraw/core'
|
||||
import { defaultStyle } from '../shared/shape-styles'
|
||||
import { StickyShape, TDMeta, TDShapeType, TransformInfo } from '~types'
|
||||
import { defaultTextStyle } from '../shared/shape-styles'
|
||||
import { AlignStyle, StickyShape, TDMeta, TDShapeType, TransformInfo } from '~types'
|
||||
import { getBoundsRectangle, TextAreaUtils } from '../shared'
|
||||
import { TDShapeUtil } from '../TDShapeUtil'
|
||||
import { getStickyFontStyle, getStickyShapeStyle } from '../shared/shape-styles'
|
||||
import { styled } from '~styles'
|
||||
import Vec from '@tldraw/vec'
|
||||
import { Vec } from '@tldraw/vec'
|
||||
import { GHOSTED_OPACITY } from '~constants'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
|
||||
|
@ -35,7 +35,7 @@ export class StickyUtil extends TDShapeUtil<T, E> {
|
|||
size: [200, 200],
|
||||
text: '',
|
||||
rotation: 0,
|
||||
style: defaultStyle,
|
||||
style: defaultTextStyle,
|
||||
},
|
||||
props
|
||||
)
|
||||
|
@ -165,7 +165,7 @@ export class StickyUtil extends TDShapeUtil<T, E> {
|
|||
isGhost={isGhost}
|
||||
style={{ backgroundColor: fill, ...style }}
|
||||
>
|
||||
<StyledText ref={rText} isEditing={isEditing}>
|
||||
<StyledText ref={rText} isEditing={isEditing} alignment={shape.style.textAlign}>
|
||||
{shape.text}​
|
||||
</StyledText>
|
||||
{isEditing && (
|
||||
|
@ -184,6 +184,7 @@ export class StickyUtil extends TDShapeUtil<T, E> {
|
|||
autoSave="false"
|
||||
autoFocus
|
||||
spellCheck={false}
|
||||
alignment={shape.style.textAlign}
|
||||
/>
|
||||
)}
|
||||
</StyledStickyContainer>
|
||||
|
@ -291,6 +292,20 @@ const StyledText = styled('div', {
|
|||
opacity: 1,
|
||||
},
|
||||
},
|
||||
alignment: {
|
||||
[AlignStyle.Start]: {
|
||||
textAlign: 'left',
|
||||
},
|
||||
[AlignStyle.Middle]: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
[AlignStyle.End]: {
|
||||
textAlign: 'right',
|
||||
},
|
||||
[AlignStyle.Justify]: {
|
||||
textAlign: 'justify',
|
||||
},
|
||||
},
|
||||
},
|
||||
...commonTextWrapping,
|
||||
})
|
||||
|
@ -310,4 +325,20 @@ const StyledTextArea = styled('textarea', {
|
|||
resize: 'none',
|
||||
caretColor: 'black',
|
||||
...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 */
|
||||
import * as React from 'react'
|
||||
import { Utils, HTMLContainer, TLBounds } from '@tldraw/core'
|
||||
import { defaultStyle, getShapeStyle, getFontStyle } from '../shared/shape-styles'
|
||||
import { TextShape, TDMeta, TDShapeType, TransformInfo } from '~types'
|
||||
import { defaultTextStyle, getShapeStyle, getFontStyle } from '../shared/shape-styles'
|
||||
import { TextShape, TDMeta, TDShapeType, TransformInfo, AlignStyle } from '~types'
|
||||
import { TextAreaUtils } from '../shared'
|
||||
import { BINDING_DISTANCE, GHOSTED_OPACITY } from '~constants'
|
||||
import { TDShapeUtil } from '../TDShapeUtil'
|
||||
import { styled } from '~styles'
|
||||
import Vec from '@tldraw/vec'
|
||||
import { Vec } from '@tldraw/vec'
|
||||
import { TLDR } from '~state/TLDR'
|
||||
import { getTextAlign } from '../shared/getTextAlign'
|
||||
|
||||
type T = TextShape
|
||||
type E = HTMLDivElement
|
||||
|
@ -33,7 +34,7 @@ export class TextUtil extends TDShapeUtil<T, E> {
|
|||
point: [0, 0],
|
||||
rotation: 0,
|
||||
text: ' ',
|
||||
style: defaultStyle,
|
||||
style: defaultTextStyle,
|
||||
},
|
||||
props
|
||||
)
|
||||
|
@ -50,7 +51,39 @@ export class TextUtil extends TDShapeUtil<T, E> {
|
|||
|
||||
const handleChange = React.useCallback(
|
||||
(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]
|
||||
)
|
||||
|
@ -126,6 +159,7 @@ export class TextUtil extends TDShapeUtil<T, E> {
|
|||
style={{
|
||||
font,
|
||||
color: styles.stroke,
|
||||
textAlign: getTextAlign(style.textAlign),
|
||||
}}
|
||||
>
|
||||
{isBinding && (
|
||||
|
@ -147,6 +181,7 @@ export class TextUtil extends TDShapeUtil<T, E> {
|
|||
style={{
|
||||
font,
|
||||
color: styles.stroke,
|
||||
textAlign: 'inherit',
|
||||
}}
|
||||
name="text"
|
||||
defaultValue={text}
|
||||
|
|
|
@ -14,9 +14,11 @@ Object {
|
|||
"style": Object {
|
||||
"color": "black",
|
||||
"dash": "draw",
|
||||
"font": "script",
|
||||
"isFilled": false,
|
||||
"scale": 1,
|
||||
"size": "small",
|
||||
"textAlign": "start",
|
||||
},
|
||||
"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 { Theme, ColorStyle, DashStyle, ShapeStyles, SizeStyle } from '~types'
|
||||
import { Theme, ColorStyle, DashStyle, ShapeStyles, SizeStyle, FontStyle, AlignStyle } from '~types'
|
||||
|
||||
const canvasLight = '#fafafa'
|
||||
|
||||
|
@ -84,6 +84,20 @@ const fontSizes = {
|
|||
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 = {
|
||||
[SizeStyle.Small]: 24,
|
||||
[SizeStyle.Medium]: 36,
|
||||
|
@ -95,8 +109,12 @@ export function getStrokeWidth(size: SizeStyle): number {
|
|||
return strokeWidths[size]
|
||||
}
|
||||
|
||||
export function getFontSize(size: SizeStyle): number {
|
||||
return fontSizes[size]
|
||||
export function getFontSize(size: SizeStyle, fontStyle: FontStyle = FontStyle.Script): number {
|
||||
return fontSizes[size] * fontSizeModifiers[fontStyle]
|
||||
}
|
||||
|
||||
export function getFontFace(font: FontStyle = FontStyle.Script): string {
|
||||
return fontFaces[font]
|
||||
}
|
||||
|
||||
export function getStickyFontSize(size: SizeStyle): number {
|
||||
|
@ -104,17 +122,19 @@ export function getStickyFontSize(size: SizeStyle): number {
|
|||
}
|
||||
|
||||
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
|
||||
|
||||
return `${fontSize * scale}px/1.3 "Caveat Brush"`
|
||||
return `${fontSize * scale}px/1.3 ${fontFace}`
|
||||
}
|
||||
|
||||
export function getStickyFontStyle(style: ShapeStyles): string {
|
||||
const fontSize = getStickyFontSize(style.size)
|
||||
const fontFace = getFontFace(style.font)
|
||||
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) {
|
||||
|
@ -158,3 +178,9 @@ export const defaultStyle: ShapeStyles = {
|
|||
dash: DashStyle.Draw,
|
||||
scale: 1,
|
||||
}
|
||||
|
||||
export const defaultTextStyle: ShapeStyles = {
|
||||
...defaultStyle,
|
||||
font: FontStyle.Script,
|
||||
textAlign: AlignStyle.Start,
|
||||
}
|
||||
|
|
|
@ -94,7 +94,6 @@ export interface TDSnapshot {
|
|||
appState: {
|
||||
currentStyle: ShapeStyles
|
||||
currentPageId: string
|
||||
pages: Pick<TLPage<TDShape, TDBinding>, 'id' | 'name' | 'childIndex'>[]
|
||||
hoveredId?: string
|
||||
activeTool: TDToolType
|
||||
isToolLocked: boolean
|
||||
|
@ -401,10 +400,26 @@ export enum FontSize {
|
|||
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 = {
|
||||
color: ColorStyle
|
||||
size: SizeStyle
|
||||
dash: DashStyle
|
||||
font?: FontStyle
|
||||
textAlign?: AlignStyle
|
||||
isFilled?: boolean
|
||||
scale?: number
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue