Pārlūkot izejas kodu

[feature] fonts (#308)

* adds fonts

* Add alignment options

* Update useKeyboardShortcuts.tsx

* Improve style panel

* Alignment for sticky notes

* swap fonts
main
Steve Ruiz 3 gadus atpakaļ
vecāks
revīzija
0685ca3871
Revīzijas autora e-pasta adrese nav piesaistīta nevienam kontam

+ 4
- 1
packages/tldraw/src/components/Primitives/RowButton/RowButton.tsx Parādīt failu

@@ -11,7 +11,7 @@ export interface RowButtonProps {
11 11
   children: React.ReactNode
12 12
   disabled?: boolean
13 13
   kbd?: string
14
-  variant?: 'wide'
14
+  variant?: 'wide' | 'styleMenu'
15 15
   isSponsor?: boolean
16 16
   isActive?: boolean
17 17
   isWarning?: boolean
@@ -130,6 +130,9 @@ export const StyledRowButton = styled('button', {
130 130
       small: {},
131 131
     },
132 132
     variant: {
133
+      styleMenu: {
134
+        margin: '$1 0 $1 0',
135
+      },
133 136
       wide: {
134 137
         gridColumn: '1 / span 4',
135 138
       },

+ 131
- 28
packages/tldraw/src/components/TopPanel/StyleMenu/StyleMenu.tsx Parādīt failu

@@ -1,6 +1,6 @@
1 1
 import * as React from 'react'
2 2
 import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
3
-import { strokes, fills, defaultStyle } from '~state/shapes/shared/shape-styles'
3
+import { strokes, fills, defaultTextStyle } from '~state/shapes/shared/shape-styles'
4 4
 import { useTldrawApp } from '~hooks'
5 5
 import {
6 6
   DMCheckboxItem,
@@ -19,37 +19,65 @@ import {
19 19
   SizeSmallIcon,
20 20
 } from '~components/Primitives/icons'
21 21
 import { ToolButton } from '~components/Primitives/ToolButton'
22
-import { TDSnapshot, ColorStyle, DashStyle, SizeStyle, ShapeStyles } from '~types'
22
+import {
23
+  TDSnapshot,
24
+  ColorStyle,
25
+  DashStyle,
26
+  SizeStyle,
27
+  ShapeStyles,
28
+  FontStyle,
29
+  AlignStyle,
30
+} from '~types'
23 31
 import { styled } from '~styles'
24 32
 import { breakpoints } from '~components/breakpoints'
25 33
 import { Divider } from '~components/Primitives/Divider'
26 34
 import { preventEvent } from '~components/preventEvent'
35
+import {
36
+  TextAlignCenterIcon,
37
+  TextAlignJustifyIcon,
38
+  TextAlignLeftIcon,
39
+  TextAlignRightIcon,
40
+} from '@radix-ui/react-icons'
27 41
 
28 42
 const currentStyleSelector = (s: TDSnapshot) => s.appState.currentStyle
29 43
 const selectedIdsSelector = (s: TDSnapshot) =>
30 44
   s.document.pageStates[s.appState.currentPageId].selectedIds
31 45
 
32
-const STYLE_KEYS = Object.keys(defaultStyle) as (keyof ShapeStyles)[]
46
+const STYLE_KEYS = Object.keys(defaultTextStyle) as (keyof ShapeStyles)[]
33 47
 
34
-const DASHES = {
48
+const DASH_ICONS = {
35 49
   [DashStyle.Draw]: <DashDrawIcon />,
36 50
   [DashStyle.Solid]: <DashSolidIcon />,
37 51
   [DashStyle.Dashed]: <DashDashedIcon />,
38 52
   [DashStyle.Dotted]: <DashDottedIcon />,
39 53
 }
40 54
 
41
-const SIZES = {
55
+const SIZE_ICONS = {
42 56
   [SizeStyle.Small]: <SizeSmallIcon />,
43 57
   [SizeStyle.Medium]: <SizeMediumIcon />,
44 58
   [SizeStyle.Large]: <SizeLargeIcon />,
45 59
 }
46 60
 
47
-const themeSelector = (data: TDSnapshot) => (data.settings.isDarkMode ? 'dark' : 'light')
61
+const ALIGN_ICONS = {
62
+  [AlignStyle.Start]: <TextAlignLeftIcon />,
63
+  [AlignStyle.Middle]: <TextAlignCenterIcon />,
64
+  [AlignStyle.End]: <TextAlignRightIcon />,
65
+  [AlignStyle.Justify]: <TextAlignJustifyIcon />,
66
+}
67
+
68
+const themeSelector = (s: TDSnapshot) => (s.settings.isDarkMode ? 'dark' : 'light')
69
+
70
+const showTextStylesSelector = (s: TDSnapshot) => {
71
+  const pageId = s.appState.currentPageId
72
+  const page = s.document.pages[pageId]
73
+  return s.document.pageStates[pageId].selectedIds.some((id) => 'text' in page.shapes[id])
74
+}
48 75
 
49 76
 export const StyleMenu = React.memo(function ColorMenu(): JSX.Element {
50 77
   const app = useTldrawApp()
51 78
 
52 79
   const theme = app.useStore(themeSelector)
80
+  const showTextStyles = app.useStore(showTextStylesSelector)
53 81
 
54 82
   const currentStyle = app.useStore(currentStyleSelector)
55 83
   const selectedIds = app.useStore(selectedIdsSelector)
@@ -111,6 +139,14 @@ export const StyleMenu = React.memo(function ColorMenu(): JSX.Element {
111 139
     app.style({ size: value as SizeStyle })
112 140
   }, [])
113 141
 
142
+  const handleFontChange = React.useCallback((value: string) => {
143
+    app.style({ font: value as FontStyle })
144
+  }, [])
145
+
146
+  const handleTextAlignChange = React.useCallback((value: string) => {
147
+    app.style({ textAlign: value as AlignStyle })
148
+  }, [])
149
+
114 150
   return (
115 151
     <DropdownMenu.Root dir="ltr">
116 152
       <DMTriggerIcon>
@@ -126,53 +162,56 @@ export const StyleMenu = React.memo(function ColorMenu(): JSX.Element {
126 162
               fill={fills[theme][displayedStyle.color as ColorStyle]}
127 163
             />
128 164
           )}
129
-          {DASHES[displayedStyle.dash]}
165
+          {DASH_ICONS[displayedStyle.dash]}
130 166
         </OverlapIcons>
131 167
       </DMTriggerIcon>
132 168
       <DMContent>
133 169
         <StyledRow variant="tall">
134 170
           <span>Color</span>
135 171
           <ColorGrid>
136
-            {Object.keys(strokes.light).map((colorStyle: string) => (
137
-              <DropdownMenu.Item key={colorStyle} onSelect={preventEvent} asChild>
172
+            {Object.keys(strokes.light).map((style: string) => (
173
+              <DropdownMenu.Item key={style} onSelect={preventEvent} asChild>
138 174
                 <ToolButton
139 175
                   variant="icon"
140
-                  isActive={displayedStyle.color === colorStyle}
141
-                  onClick={() => app.style({ color: colorStyle as ColorStyle })}
176
+                  isActive={displayedStyle.color === style}
177
+                  onClick={() => app.style({ color: style as ColorStyle })}
142 178
                 >
143 179
                   <CircleIcon
144 180
                     size={18}
145 181
                     strokeWidth={2.5}
146 182
                     fill={
147
-                      displayedStyle.isFilled
148
-                        ? fills.light[colorStyle as ColorStyle]
149
-                        : 'transparent'
183
+                      displayedStyle.isFilled ? fills.light[style as ColorStyle] : 'transparent'
150 184
                     }
151
-                    stroke={strokes.light[colorStyle as ColorStyle]}
185
+                    stroke={strokes.light[style as ColorStyle]}
152 186
                   />
153 187
                 </ToolButton>
154 188
               </DropdownMenu.Item>
155 189
             ))}
156 190
           </ColorGrid>
157 191
         </StyledRow>
158
-        <Divider />
192
+        <DMCheckboxItem
193
+          variant="styleMenu"
194
+          checked={!!displayedStyle.isFilled}
195
+          onCheckedChange={handleToggleFilled}
196
+        >
197
+          Fill
198
+        </DMCheckboxItem>
159 199
         <StyledRow>
160 200
           Dash
161 201
           <StyledGroup dir="ltr" value={displayedStyle.dash} onValueChange={handleDashChange}>
162
-            {Object.values(DashStyle).map((dashStyle) => (
202
+            {Object.values(DashStyle).map((style) => (
163 203
               <DMRadioItem
164
-                key={dashStyle}
165
-                isActive={dashStyle === displayedStyle.dash}
166
-                value={dashStyle}
204
+                key={style}
205
+                isActive={style === displayedStyle.dash}
206
+                value={style}
167 207
                 onSelect={preventEvent}
168 208
                 bp={breakpoints}
169 209
               >
170
-                {DASHES[dashStyle as DashStyle]}
210
+                {DASH_ICONS[style as DashStyle]}
171 211
               </DMRadioItem>
172 212
             ))}
173 213
           </StyledGroup>
174 214
         </StyledRow>
175
-        <Divider />
176 215
         <StyledRow>
177 216
           Size
178 217
           <StyledGroup dir="ltr" value={displayedStyle.size} onValueChange={handleSizeChange}>
@@ -184,15 +223,52 @@ export const StyleMenu = React.memo(function ColorMenu(): JSX.Element {
184 223
                 onSelect={preventEvent}
185 224
                 bp={breakpoints}
186 225
               >
187
-                {SIZES[sizeStyle as SizeStyle]}
226
+                {SIZE_ICONS[sizeStyle as SizeStyle]}
188 227
               </DMRadioItem>
189 228
             ))}
190 229
           </StyledGroup>
191 230
         </StyledRow>
192
-        <Divider />
193
-        <DMCheckboxItem checked={!!displayedStyle.isFilled} onCheckedChange={handleToggleFilled}>
194
-          Fill
195
-        </DMCheckboxItem>
231
+        {showTextStyles && (
232
+          <>
233
+            <Divider />
234
+            <StyledRow>
235
+              Font
236
+              <StyledGroup dir="ltr" value={displayedStyle.font} onValueChange={handleFontChange}>
237
+                {Object.values(FontStyle).map((fontStyle) => (
238
+                  <DMRadioItem
239
+                    key={fontStyle}
240
+                    isActive={fontStyle === displayedStyle.font}
241
+                    value={fontStyle}
242
+                    onSelect={preventEvent}
243
+                    bp={breakpoints}
244
+                  >
245
+                    <FontIcon fontStyle={fontStyle}>Aa</FontIcon>
246
+                  </DMRadioItem>
247
+                ))}
248
+              </StyledGroup>
249
+            </StyledRow>
250
+            <StyledRow>
251
+              Align
252
+              <StyledGroup
253
+                dir="ltr"
254
+                value={displayedStyle.textAlign}
255
+                onValueChange={handleTextAlignChange}
256
+              >
257
+                {Object.values(AlignStyle).map((style) => (
258
+                  <DMRadioItem
259
+                    key={style}
260
+                    isActive={style === displayedStyle.textAlign}
261
+                    value={style}
262
+                    onSelect={preventEvent}
263
+                    bp={breakpoints}
264
+                  >
265
+                    {ALIGN_ICONS[style]}
266
+                  </DMRadioItem>
267
+                ))}
268
+              </StyledGroup>
269
+            </StyledRow>
270
+          </>
271
+        )}
196 272
       </DMContent>
197 273
     </DropdownMenu.Root>
198 274
   )
@@ -237,7 +313,7 @@ export const StyledRow = styled('div', {
237 313
   fontFamily: '$ui',
238 314
   fontWeight: 400,
239 315
   fontSize: '$1',
240
-  padding: '0 0 0 $3',
316
+  padding: '$2 0 $2 $3',
241 317
   borderRadius: 4,
242 318
   userSelect: 'none',
243 319
   margin: 0,
@@ -250,6 +326,7 @@ export const StyledRow = styled('div', {
250 326
     variant: {
251 327
       tall: {
252 328
         alignItems: 'flex-start',
329
+        padding: '0 0 0 $3',
253 330
         '& > span': {
254 331
           paddingTop: '$4',
255 332
         },
@@ -261,6 +338,7 @@ export const StyledRow = styled('div', {
261 338
 const StyledGroup = styled(DropdownMenu.DropdownMenuRadioGroup, {
262 339
   display: 'flex',
263 340
   flexDirection: 'row',
341
+  gap: '$1',
264 342
 })
265 343
 
266 344
 const OverlapIcons = styled('div', {
@@ -270,3 +348,28 @@ const OverlapIcons = styled('div', {
270 348
     gridRow: 1,
271 349
   },
272 350
 })
351
+
352
+const FontIcon = styled('div', {
353
+  width: 32,
354
+  height: 32,
355
+  display: 'flex',
356
+  alignItems: 'center',
357
+  justifyContent: 'center',
358
+  fontSize: '$3',
359
+  variants: {
360
+    fontStyle: {
361
+      [FontStyle.Script]: {
362
+        fontFamily: 'Caveat Brush',
363
+      },
364
+      [FontStyle.Sans]: {
365
+        fontFamily: 'Recursive',
366
+      },
367
+      [FontStyle.Serif]: {
368
+        fontFamily: 'Georgia',
369
+      },
370
+      [FontStyle.Mono]: {
371
+        fontFamily: 'Recursive Mono',
372
+      },
373
+    },
374
+  },
375
+})

+ 59
- 22
packages/tldraw/src/hooks/useKeyboardShortcuts.tsx Parādīt failu

@@ -1,6 +1,6 @@
1 1
 import * as React from 'react'
2 2
 import { useHotkeys } from 'react-hotkeys-hook'
3
-import { TDShapeType } from '~types'
3
+import { AlignStyle, TDShapeType } from '~types'
4 4
 import { useFileSystemHandlers, useTldrawApp } from '~hooks'
5 5
 
6 6
 export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
@@ -97,7 +97,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
97 97
   // Dark Mode
98 98
 
99 99
   useHotkeys(
100
-    'ctrl+shift+d,command+shift+d',
100
+    'ctrl+shift+d,+shift+d',
101 101
     (e) => {
102 102
       if (!canHandleEvent()) return
103 103
       app.toggleDarkMode()
@@ -110,7 +110,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
110 110
   // Focus Mode
111 111
 
112 112
   useHotkeys(
113
-    'ctrl+.,command+.',
113
+    'ctrl+.,+.',
114 114
     () => {
115 115
       if (!canHandleEvent()) return
116 116
       app.toggleFocusMode()
@@ -124,7 +124,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
124 124
   const { onNewProject, onOpenProject, onSaveProject, onSaveProjectAs } = useFileSystemHandlers()
125 125
 
126 126
   useHotkeys(
127
-    'ctrl+n,command+n',
127
+    'ctrl+n,+n',
128 128
     (e) => {
129 129
       if (!canHandleEvent()) return
130 130
 
@@ -134,7 +134,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
134 134
     [app]
135 135
   )
136 136
   useHotkeys(
137
-    'ctrl+s,command+s',
137
+    'ctrl+s,+s',
138 138
     (e) => {
139 139
       if (!canHandleEvent()) return
140 140
 
@@ -145,7 +145,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
145 145
   )
146 146
 
147 147
   useHotkeys(
148
-    'ctrl+shift+s,command+shift+s',
148
+    'ctrl+shift+s,+shift+s',
149 149
     (e) => {
150 150
       if (!canHandleEvent()) return
151 151
 
@@ -155,7 +155,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
155 155
     [app]
156 156
   )
157 157
   useHotkeys(
158
-    'ctrl+o,command+o',
158
+    'ctrl+o,+o',
159 159
     (e) => {
160 160
       if (!canHandleEvent()) return
161 161
 
@@ -168,10 +168,12 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
168 168
   // Undo Redo
169 169
 
170 170
   useHotkeys(
171
-    'command+z,ctrl+z',
171
+    '+z,ctrl+z',
172 172
     () => {
173 173
       if (!canHandleEvent()) return
174 174
 
175
+      console.log('Hello')
176
+
175 177
       if (app.session) {
176 178
         app.cancelSession()
177 179
       } else {
@@ -183,7 +185,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
183 185
   )
184 186
 
185 187
   useHotkeys(
186
-    'ctrl+shift-z,command+shift+z',
188
+    'ctrl+shift-z,+shift+z',
187 189
     () => {
188 190
       if (!canHandleEvent()) return
189 191
 
@@ -200,7 +202,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
200 202
   // Undo Redo
201 203
 
202 204
   useHotkeys(
203
-    'command+u,ctrl+u',
205
+    '+u,ctrl+u',
204 206
     () => {
205 207
       if (!canHandleEvent()) return
206 208
       app.undoSelect()
@@ -210,7 +212,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
210 212
   )
211 213
 
212 214
   useHotkeys(
213
-    'ctrl+shift-u,command+shift+u',
215
+    'ctrl+shift-u,+shift+u',
214 216
     () => {
215 217
       if (!canHandleEvent()) return
216 218
       app.redoSelect()
@@ -224,7 +226,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
224 226
   // Camera
225 227
 
226 228
   useHotkeys(
227
-    'ctrl+=,command+=',
229
+    'ctrl+=,+=',
228 230
     (e) => {
229 231
       if (!canHandleEvent()) return
230 232
 
@@ -236,7 +238,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
236 238
   )
237 239
 
238 240
   useHotkeys(
239
-    'ctrl+-,command+-',
241
+    'ctrl+-,+-',
240 242
     (e) => {
241 243
       if (!canHandleEvent()) return
242 244
 
@@ -280,7 +282,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
280 282
   // Duplicate
281 283
 
282 284
   useHotkeys(
283
-    'ctrl+d,command+d',
285
+    'ctrl+d,+d',
284 286
     (e) => {
285 287
       if (!canHandleEvent()) return
286 288
 
@@ -341,7 +343,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
341 343
   // Select All
342 344
 
343 345
   useHotkeys(
344
-    'command+a,ctrl+a',
346
+    '+a,ctrl+a',
345 347
     () => {
346 348
       if (!canHandleEvent()) return
347 349
       app.selectAll()
@@ -433,7 +435,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
433 435
   )
434 436
 
435 437
   useHotkeys(
436
-    'command+shift+l,ctrl+shift+l',
438
+    '+shift+l,ctrl+shift+l',
437 439
     () => {
438 440
       if (!canHandleEvent()) return
439 441
       app.toggleLocked()
@@ -445,7 +447,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
445 447
   // Copy, Cut & Paste
446 448
 
447 449
   useHotkeys(
448
-    'command+c,ctrl+c',
450
+    '+c,ctrl+c',
449 451
     () => {
450 452
       if (!canHandleEvent()) return
451 453
       app.copy()
@@ -455,7 +457,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
455 457
   )
456 458
 
457 459
   useHotkeys(
458
-    'command+x,ctrl+x',
460
+    '+x,ctrl+x',
459 461
     () => {
460 462
       if (!canHandleEvent()) return
461 463
       app.cut()
@@ -465,7 +467,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
465 467
   )
466 468
 
467 469
   useHotkeys(
468
-    'command+v,ctrl+v',
470
+    '+v,ctrl+v',
469 471
     () => {
470 472
       if (!canHandleEvent()) return
471 473
       app.paste()
@@ -477,7 +479,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
477 479
   // Group & Ungroup
478 480
 
479 481
   useHotkeys(
480
-    'command+g,ctrl+g',
482
+    '+g,ctrl+g',
481 483
     (e) => {
482 484
       if (!canHandleEvent()) return
483 485
 
@@ -489,7 +491,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
489 491
   )
490 492
 
491 493
   useHotkeys(
492
-    'command+shift+g,ctrl+shift+g',
494
+    '+shift+g,ctrl+shift+g',
493 495
     (e) => {
494 496
       if (!canHandleEvent()) return
495 497
 
@@ -543,7 +545,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
543 545
   )
544 546
 
545 547
   useHotkeys(
546
-    'command+shift+backspace',
548
+    'ctrl+shift+backspace,⌘+shift+backspace',
547 549
     (e) => {
548 550
       if (!canHandleEvent()) return
549 551
       if (app.settings.isDebugMode) {
@@ -554,4 +556,39 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
554 556
     undefined,
555 557
     [app]
556 558
   )
559
+
560
+  // Text Align
561
+
562
+  useHotkeys(
563
+    'alt+command+l,alt+ctrl+l',
564
+    (e) => {
565
+      if (!canHandleEvent()) return
566
+      app.style({ textAlign: AlignStyle.Start })
567
+      e.preventDefault()
568
+    },
569
+    undefined,
570
+    [app]
571
+  )
572
+
573
+  useHotkeys(
574
+    'alt+command+t,alt+ctrl+t',
575
+    (e) => {
576
+      if (!canHandleEvent()) return
577
+      app.style({ textAlign: AlignStyle.Middle })
578
+      e.preventDefault()
579
+    },
580
+    undefined,
581
+    [app]
582
+  )
583
+
584
+  useHotkeys(
585
+    'alt+command+r,alt+ctrl+r',
586
+    (e) => {
587
+      if (!canHandleEvent()) return
588
+      app.style({ textAlign: AlignStyle.End })
589
+      e.preventDefault()
590
+    },
591
+    undefined,
592
+    [app]
593
+  )
557 594
 }

+ 1
- 1
packages/tldraw/src/hooks/useStylesheet.ts Parādīt failu

@@ -4,7 +4,7 @@ const styles = new Map<string, HTMLStyleElement>()
4 4
 
5 5
 const UID = `Tldraw-fonts`
6 6
 const CSS = `
7
-@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&display=swap')
7
+@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&family=Source+Code+Pro&family=Source+Sans+Pro&family=Source+Serif+Pro&display=swap');
8 8
 `
9 9
 
10 10
 export function useStylesheet() {

+ 16
- 12
packages/tldraw/src/state/TldrawApp.ts Parādīt failu

@@ -1,7 +1,7 @@
1 1
 /* eslint-disable @typescript-eslint/no-explicit-any */
2 2
 /* eslint-disable @typescript-eslint/ban-ts-comment */
3 3
 /* eslint-disable @typescript-eslint/no-non-null-assertion */
4
-import { Patch, StateManager } from 'rko'
4
+import { StateManager } from 'rko'
5 5
 import { Vec } from '@tldraw/vec'
6 6
 import {
7 7
   TLBoundsEventHandler,
@@ -247,11 +247,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
247 247
    * @protected
248 248
    * @returns The final state
249 249
    */
250
-  protected cleanup = (
251
-    state: TDSnapshot,
252
-    prev: TDSnapshot,
253
-    patch: Patch<TDSnapshot>
254
-  ): TDSnapshot => {
250
+  protected cleanup = (state: TDSnapshot, prev: TDSnapshot): TDSnapshot => {
255 251
     const next = { ...state }
256 252
 
257 253
     // Remove deleted shapes and bindings (in Commands, these will be set to undefined)
@@ -2064,7 +2060,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
2064 2060
     const shapesToUpdate = shapes.filter((shape) => pageShapes[shape.id])
2065 2061
     if (shapesToUpdate.length === 0) return this
2066 2062
     return this.setState(
2067
-      Commands.update(this, shapesToUpdate, this.currentPageId),
2063
+      Commands.updateShapes(this, shapesToUpdate, this.currentPageId),
2068 2064
       'updated_shapes'
2069 2065
     )
2070 2066
   }
@@ -2079,7 +2075,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
2079 2075
     const shapesToUpdate = shapes.filter((shape) => pageShapes[shape.id])
2080 2076
     if (shapesToUpdate.length === 0) return this
2081 2077
     return this.patchState(
2082
-      Commands.update(this, shapesToUpdate, this.currentPageId).after,
2078
+      Commands.updateShapes(this, shapesToUpdate, this.currentPageId).after,
2083 2079
       'updated_shapes'
2084 2080
     )
2085 2081
   }
@@ -2333,6 +2329,15 @@ export class TldrawApp extends StateManager<TDSnapshot> {
2333 2329
     return this.setState(Commands.toggleShapesDecoration(this, ids, handleId))
2334 2330
   }
2335 2331
 
2332
+  /**
2333
+   * Set the props of one or more shapes
2334
+   * @param props The props to set on the shapes.
2335
+   * @param ids The ids of the shapes to set props on.
2336
+   */
2337
+  setShapeProps = <T extends TDShape>(props: Partial<T>, ids = this.selectedIds) => {
2338
+    return this.setState(Commands.setShapesProps(this, ids, props))
2339
+  }
2340
+
2336 2341
   /**
2337 2342
    * Rotate one or more shapes by a delta.
2338 2343
    * @param delta The delta in radians.
@@ -2800,12 +2805,12 @@ export class TldrawApp extends StateManager<TDSnapshot> {
2800 2805
 
2801 2806
   getShapeUtil = TLDR.getShapeUtil
2802 2807
 
2803
-  static version = 13
2808
+  static version = 14
2804 2809
 
2805 2810
   static defaultDocument: TDDocument = {
2806 2811
     id: 'doc',
2807 2812
     name: 'New Document',
2808
-    version: 13,
2813
+    version: 14,
2809 2814
     pages: {
2810 2815
       page: {
2811 2816
         id: 'page',
@@ -2843,15 +2848,14 @@ export class TldrawApp extends StateManager<TDSnapshot> {
2843 2848
       showCloneHandles: false,
2844 2849
     },
2845 2850
     appState: {
2851
+      status: TDStatus.Idle,
2846 2852
       activeTool: 'select',
2847 2853
       hoveredId: undefined,
2848 2854
       currentPageId: 'page',
2849
-      pages: [{ id: 'page', name: 'page', childIndex: 1 }],
2850 2855
       currentStyle: defaultStyle,
2851 2856
       isToolLocked: false,
2852 2857
       isStyleOpen: false,
2853 2858
       isEmptyCanvas: false,
2854
-      status: TDStatus.Idle,
2855 2859
       snapLines: [],
2856 2860
     },
2857 2861
     document: TldrawApp.defaultDocument,

+ 1
- 0
packages/tldraw/src/state/commands/index.ts Parādīt failu

@@ -21,3 +21,4 @@ export * from './toggleShapesProp'
21 21
 export * from './translateShapes'
22 22
 export * from './ungroupShapes'
23 23
 export * from './updateShapes'
24
+export * from './setShapesProps'

+ 1
- 0
packages/tldraw/src/state/commands/setShapesProps/index.ts Parādīt failu

@@ -0,0 +1 @@
1
+export * from './setShapesProps'

+ 3
- 0
packages/tldraw/src/state/commands/setShapesProps/setShapesProps.spec.ts Parādīt failu

@@ -0,0 +1,3 @@
1
+describe('Set shapes props command', () => {
2
+  it.todo('sets the props of the provided shapes')
3
+})

+ 56
- 0
packages/tldraw/src/state/commands/setShapesProps/setShapesProps.ts Parādīt failu

@@ -0,0 +1,56 @@
1
+import type { TDShape, TldrawCommand } from '~types'
2
+import type { TldrawApp } from '~state'
3
+
4
+export function setShapesProps<T extends TDShape>(
5
+  app: TldrawApp,
6
+  ids: string[],
7
+  partial: Partial<T>
8
+): TldrawCommand {
9
+  const { currentPageId, selectedIds } = app
10
+
11
+  const initialShapes = ids
12
+    .map((id) => app.getShape<T>(id))
13
+    .filter((shape) => (partial['isLocked'] ? true : !shape.isLocked))
14
+
15
+  const before: Record<string, Partial<TDShape>> = {}
16
+  const after: Record<string, Partial<TDShape>> = {}
17
+
18
+  const keys = Object.keys(partial) as (keyof T)[]
19
+
20
+  initialShapes.forEach((shape) => {
21
+    before[shape.id] = Object.fromEntries(keys.map((key) => [key, shape[key]]))
22
+    after[shape.id] = partial
23
+  })
24
+
25
+  return {
26
+    id: 'set_props',
27
+    before: {
28
+      document: {
29
+        pages: {
30
+          [currentPageId]: {
31
+            shapes: before,
32
+          },
33
+        },
34
+        pageStates: {
35
+          [currentPageId]: {
36
+            selectedIds,
37
+          },
38
+        },
39
+      },
40
+    },
41
+    after: {
42
+      document: {
43
+        pages: {
44
+          [currentPageId]: {
45
+            shapes: after,
46
+          },
47
+        },
48
+        pageStates: {
49
+          [currentPageId]: {
50
+            selectedIds,
51
+          },
52
+        },
53
+      },
54
+    },
55
+  }
56
+}

+ 1
- 1
packages/tldraw/src/state/commands/updateShapes/updateShapes.ts Parādīt failu

@@ -2,7 +2,7 @@ import type { TldrawCommand, TDShape } from '~types'
2 2
 import { TLDR } from '~state/TLDR'
3 3
 import type { TldrawApp } from '../../internal'
4 4
 
5
-export function update(
5
+export function updateShapes(
6 6
   app: TldrawApp,
7 7
   updates: ({ id: string } & Partial<TDShape>)[],
8 8
   pageId: string

+ 9
- 1
packages/tldraw/src/state/data/migrate.ts Parādīt failu

@@ -1,11 +1,19 @@
1 1
 /* eslint-disable @typescript-eslint/ban-ts-comment */
2
-import { Decoration, TDDocument, TDShapeType } from '~types'
2
+import { Decoration, FontStyle, TDDocument, TDShapeType, TextShape } from '~types'
3 3
 
4 4
 export function migrate(document: TDDocument, newVersion: number): TDDocument {
5 5
   const { version = 0 } = document
6 6
 
7 7
   if (version === newVersion) return document
8 8
 
9
+  if (version < 14) {
10
+    Object.values(document.pages).forEach((page) => {
11
+      Object.values(page.shapes)
12
+        .filter((shape) => shape.type === TDShapeType.Text)
13
+        .forEach((shape) => (shape as TextShape).style.font === FontStyle.Script)
14
+    })
15
+  }
16
+
9 17
   // Lowercase styles, move binding meta to binding
10 18
   if (version <= 13) {
11 19
     Object.values(document.pages).forEach((page) => {

+ 36
- 5
packages/tldraw/src/state/shapes/StickyUtil/StickyUtil.tsx Parādīt failu

@@ -1,13 +1,13 @@
1 1
 /* eslint-disable @typescript-eslint/no-non-null-assertion */
2 2
 import * as React from 'react'
3 3
 import { Utils, HTMLContainer, TLBounds } from '@tldraw/core'
4
-import { defaultStyle } from '../shared/shape-styles'
5
-import { StickyShape, TDMeta, TDShapeType, TransformInfo } from '~types'
4
+import { defaultTextStyle } from '../shared/shape-styles'
5
+import { AlignStyle, StickyShape, TDMeta, TDShapeType, TransformInfo } from '~types'
6 6
 import { getBoundsRectangle, TextAreaUtils } from '../shared'
7 7
 import { TDShapeUtil } from '../TDShapeUtil'
8 8
 import { getStickyFontStyle, getStickyShapeStyle } from '../shared/shape-styles'
9 9
 import { styled } from '~styles'
10
-import Vec from '@tldraw/vec'
10
+import { Vec } from '@tldraw/vec'
11 11
 import { GHOSTED_OPACITY } from '~constants'
12 12
 import { TLDR } from '~state/TLDR'
13 13
 
@@ -35,7 +35,7 @@ export class StickyUtil extends TDShapeUtil<T, E> {
35 35
         size: [200, 200],
36 36
         text: '',
37 37
         rotation: 0,
38
-        style: defaultStyle,
38
+        style: defaultTextStyle,
39 39
       },
40 40
       props
41 41
     )
@@ -165,7 +165,7 @@ export class StickyUtil extends TDShapeUtil<T, E> {
165 165
             isGhost={isGhost}
166 166
             style={{ backgroundColor: fill, ...style }}
167 167
           >
168
-            <StyledText ref={rText} isEditing={isEditing}>
168
+            <StyledText ref={rText} isEditing={isEditing} alignment={shape.style.textAlign}>
169 169
               {shape.text}&#8203;
170 170
             </StyledText>
171 171
             {isEditing && (
@@ -184,6 +184,7 @@ export class StickyUtil extends TDShapeUtil<T, E> {
184 184
                 autoSave="false"
185 185
                 autoFocus
186 186
                 spellCheck={false}
187
+                alignment={shape.style.textAlign}
187 188
               />
188 189
             )}
189 190
           </StyledStickyContainer>
@@ -291,6 +292,20 @@ const StyledText = styled('div', {
291 292
         opacity: 1,
292 293
       },
293 294
     },
295
+    alignment: {
296
+      [AlignStyle.Start]: {
297
+        textAlign: 'left',
298
+      },
299
+      [AlignStyle.Middle]: {
300
+        textAlign: 'center',
301
+      },
302
+      [AlignStyle.End]: {
303
+        textAlign: 'right',
304
+      },
305
+      [AlignStyle.Justify]: {
306
+        textAlign: 'justify',
307
+      },
308
+    },
294 309
   },
295 310
   ...commonTextWrapping,
296 311
 })
@@ -310,4 +325,20 @@ const StyledTextArea = styled('textarea', {
310 325
   resize: 'none',
311 326
   caretColor: 'black',
312 327
   ...commonTextWrapping,
328
+  variants: {
329
+    alignment: {
330
+      [AlignStyle.Start]: {
331
+        textAlign: 'left',
332
+      },
333
+      [AlignStyle.Middle]: {
334
+        textAlign: 'center',
335
+      },
336
+      [AlignStyle.End]: {
337
+        textAlign: 'right',
338
+      },
339
+      [AlignStyle.Justify]: {
340
+        textAlign: 'justify',
341
+      },
342
+    },
343
+  },
313 344
 })

+ 40
- 5
packages/tldraw/src/state/shapes/TextUtil/TextUtil.tsx Parādīt failu

@@ -1,14 +1,15 @@
1 1
 /* eslint-disable @typescript-eslint/no-non-null-assertion */
2 2
 import * as React from 'react'
3 3
 import { Utils, HTMLContainer, TLBounds } from '@tldraw/core'
4
-import { defaultStyle, getShapeStyle, getFontStyle } from '../shared/shape-styles'
5
-import { TextShape, TDMeta, TDShapeType, TransformInfo } from '~types'
4
+import { defaultTextStyle, getShapeStyle, getFontStyle } from '../shared/shape-styles'
5
+import { TextShape, TDMeta, TDShapeType, TransformInfo, AlignStyle } from '~types'
6 6
 import { TextAreaUtils } from '../shared'
7 7
 import { BINDING_DISTANCE, GHOSTED_OPACITY } from '~constants'
8 8
 import { TDShapeUtil } from '../TDShapeUtil'
9 9
 import { styled } from '~styles'
10
-import Vec from '@tldraw/vec'
10
+import { Vec } from '@tldraw/vec'
11 11
 import { TLDR } from '~state/TLDR'
12
+import { getTextAlign } from '../shared/getTextAlign'
12 13
 
13 14
 type T = TextShape
14 15
 type E = HTMLDivElement
@@ -33,7 +34,7 @@ export class TextUtil extends TDShapeUtil<T, E> {
33 34
         point: [0, 0],
34 35
         rotation: 0,
35 36
         text: ' ',
36
-        style: defaultStyle,
37
+        style: defaultTextStyle,
37 38
       },
38 39
       props
39 40
     )
@@ -50,7 +51,39 @@ export class TextUtil extends TDShapeUtil<T, E> {
50 51
 
51 52
       const handleChange = React.useCallback(
52 53
         (e: React.ChangeEvent<HTMLTextAreaElement>) => {
53
-          onShapeChange?.({ ...shape, text: TLDR.normalizeText(e.currentTarget.value) })
54
+          let delta = [0, 0]
55
+
56
+          const currentBounds = this.getBounds(shape)
57
+
58
+          switch (shape.style.textAlign) {
59
+            case AlignStyle.Start: {
60
+              break
61
+            }
62
+            case AlignStyle.Middle: {
63
+              const nextBounds = this.getBounds({
64
+                ...shape,
65
+                text: TLDR.normalizeText(e.currentTarget.value),
66
+              })
67
+
68
+              delta = Vec.div([nextBounds.width - currentBounds.width, 0], 2)
69
+              break
70
+            }
71
+            case AlignStyle.End: {
72
+              const nextBounds = this.getBounds({
73
+                ...shape,
74
+                text: TLDR.normalizeText(e.currentTarget.value),
75
+              })
76
+
77
+              delta = [nextBounds.width - currentBounds.width, 0]
78
+              break
79
+            }
80
+          }
81
+
82
+          onShapeChange?.({
83
+            ...shape,
84
+            point: Vec.sub(shape.point, delta),
85
+            text: TLDR.normalizeText(e.currentTarget.value),
86
+          })
54 87
         },
55 88
         [shape]
56 89
       )
@@ -126,6 +159,7 @@ export class TextUtil extends TDShapeUtil<T, E> {
126 159
               style={{
127 160
                 font,
128 161
                 color: styles.stroke,
162
+                textAlign: getTextAlign(style.textAlign),
129 163
               }}
130 164
             >
131 165
               {isBinding && (
@@ -147,6 +181,7 @@ export class TextUtil extends TDShapeUtil<T, E> {
147 181
                   style={{
148 182
                     font,
149 183
                     color: styles.stroke,
184
+                    textAlign: 'inherit',
150 185
                   }}
151 186
                   name="text"
152 187
                   defaultValue={text}

+ 2
- 0
packages/tldraw/src/state/shapes/TextUtil/__snapshots__/TextUtil.spec.tsx.snap Parādīt failu

@@ -14,9 +14,11 @@ Object {
14 14
   "style": Object {
15 15
     "color": "black",
16 16
     "dash": "draw",
17
+    "font": "script",
17 18
     "isFilled": false,
18 19
     "scale": 1,
19 20
     "size": "small",
21
+    "textAlign": "start",
20 22
   },
21 23
   "text": " ",
22 24
   "type": "text",

+ 12
- 0
packages/tldraw/src/state/shapes/shared/getTextAlign.ts Parādīt failu

@@ -0,0 +1,12 @@
1
+import { AlignStyle } from '~types'
2
+
3
+const ALIGN_VALUES = {
4
+  [AlignStyle.Start]: 'left',
5
+  [AlignStyle.Middle]: 'center',
6
+  [AlignStyle.End]: 'right',
7
+  [AlignStyle.Justify]: 'justify',
8
+} as const
9
+
10
+export function getTextAlign(alignStyle: AlignStyle = AlignStyle.Start) {
11
+  return ALIGN_VALUES[alignStyle]
12
+}

+ 32
- 6
packages/tldraw/src/state/shapes/shared/shape-styles.ts Parādīt failu

@@ -1,5 +1,5 @@
1 1
 import { Utils } from '@tldraw/core'
2
-import { Theme, ColorStyle, DashStyle, ShapeStyles, SizeStyle } from '~types'
2
+import { Theme, ColorStyle, DashStyle, ShapeStyles, SizeStyle, FontStyle, AlignStyle } from '~types'
3 3
 
4 4
 const canvasLight = '#fafafa'
5 5
 
@@ -84,6 +84,20 @@ const fontSizes = {
84 84
   auto: 'auto',
85 85
 }
86 86
 
87
+const fontFaces = {
88
+  [FontStyle.Script]: '"Caveat Brush"',
89
+  [FontStyle.Sans]: '"Source Sans Pro", sans-serif',
90
+  [FontStyle.Serif]: '"Source Serif Pro", serif',
91
+  [FontStyle.Mono]: '"Source Code Pro", monospace',
92
+}
93
+
94
+const fontSizeModifiers = {
95
+  [FontStyle.Script]: 1,
96
+  [FontStyle.Sans]: 1,
97
+  [FontStyle.Serif]: 1,
98
+  [FontStyle.Mono]: 1,
99
+}
100
+
87 101
 const stickyFontSizes = {
88 102
   [SizeStyle.Small]: 24,
89 103
   [SizeStyle.Medium]: 36,
@@ -95,8 +109,12 @@ export function getStrokeWidth(size: SizeStyle): number {
95 109
   return strokeWidths[size]
96 110
 }
97 111
 
98
-export function getFontSize(size: SizeStyle): number {
99
-  return fontSizes[size]
112
+export function getFontSize(size: SizeStyle, fontStyle: FontStyle = FontStyle.Script): number {
113
+  return fontSizes[size] * fontSizeModifiers[fontStyle]
114
+}
115
+
116
+export function getFontFace(font: FontStyle = FontStyle.Script): string {
117
+  return fontFaces[font]
100 118
 }
101 119
 
102 120
 export function getStickyFontSize(size: SizeStyle): number {
@@ -104,17 +122,19 @@ export function getStickyFontSize(size: SizeStyle): number {
104 122
 }
105 123
 
106 124
 export function getFontStyle(style: ShapeStyles): string {
107
-  const fontSize = getFontSize(style.size)
125
+  const fontSize = getFontSize(style.size, style.font)
126
+  const fontFace = getFontFace(style.font)
108 127
   const { scale = 1 } = style
109 128
 
110
-  return `${fontSize * scale}px/1.3 "Caveat Brush"`
129
+  return `${fontSize * scale}px/1.3 ${fontFace}`
111 130
 }
112 131
 
113 132
 export function getStickyFontStyle(style: ShapeStyles): string {
114 133
   const fontSize = getStickyFontSize(style.size)
134
+  const fontFace = getFontFace(style.font)
115 135
   const { scale = 1 } = style
116 136
 
117
-  return `${fontSize * scale}px/1.3 "Caveat Brush"`
137
+  return `${fontSize * scale}px/1.3 ${fontFace}`
118 138
 }
119 139
 
120 140
 export function getStickyShapeStyle(style: ShapeStyles, isDarkMode = false) {
@@ -158,3 +178,9 @@ export const defaultStyle: ShapeStyles = {
158 178
   dash: DashStyle.Draw,
159 179
   scale: 1,
160 180
 }
181
+
182
+export const defaultTextStyle: ShapeStyles = {
183
+  ...defaultStyle,
184
+  font: FontStyle.Script,
185
+  textAlign: AlignStyle.Start,
186
+}

+ 16
- 1
packages/tldraw/src/types.ts Parādīt failu

@@ -94,7 +94,6 @@ export interface TDSnapshot {
94 94
   appState: {
95 95
     currentStyle: ShapeStyles
96 96
     currentPageId: string
97
-    pages: Pick<TLPage<TDShape, TDBinding>, 'id' | 'name' | 'childIndex'>[]
98 97
     hoveredId?: string
99 98
     activeTool: TDToolType
100 99
     isToolLocked: boolean
@@ -401,10 +400,26 @@ export enum FontSize {
401 400
   ExtraLarge = 'extraLarge',
402 401
 }
403 402
 
403
+export enum AlignStyle {
404
+  Start = 'start',
405
+  Middle = 'middle',
406
+  End = 'end',
407
+  Justify = 'justify',
408
+}
409
+
410
+export enum FontStyle {
411
+  Script = 'script',
412
+  Sans = 'sans',
413
+  Serif = 'erif',
414
+  Mono = 'mono',
415
+}
416
+
404 417
 export type ShapeStyles = {
405 418
   color: ColorStyle
406 419
   size: SizeStyle
407 420
   dash: DashStyle
421
+  font?: FontStyle
422
+  textAlign?: AlignStyle
408 423
   isFilled?: boolean
409 424
   scale?: number
410 425
 }

Notiek ielāde…
Atcelt
Saglabāt