tldraw 3.15.0-next.39f008bfb627 → 3.15.0-next.82ffd490a4f1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/dist-cjs/index.d.ts +16 -8
  2. package/dist-cjs/index.js +2 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawImage.js +5 -2
  5. package/dist-cjs/lib/TldrawImage.js.map +3 -3
  6. package/dist-cjs/lib/canvas/TldrawCropHandles.js +1 -1
  7. package/dist-cjs/lib/canvas/TldrawCropHandles.js.map +2 -2
  8. package/dist-cjs/lib/canvas/TldrawHandles.js +1 -1
  9. package/dist-cjs/lib/canvas/TldrawHandles.js.map +2 -2
  10. package/dist-cjs/lib/canvas/TldrawOverlays.js +1 -1
  11. package/dist-cjs/lib/canvas/TldrawOverlays.js.map +2 -2
  12. package/dist-cjs/lib/canvas/TldrawSelectionForeground.js +279 -271
  13. package/dist-cjs/lib/canvas/TldrawSelectionForeground.js.map +2 -2
  14. package/dist-cjs/lib/shapes/shared/PathBuilder.js +21 -3
  15. package/dist-cjs/lib/shapes/shared/PathBuilder.js.map +2 -2
  16. package/dist-cjs/lib/shapes/shared/PlainTextLabel.js +1 -0
  17. package/dist-cjs/lib/shapes/shared/PlainTextLabel.js.map +2 -2
  18. package/dist-cjs/lib/shapes/shared/RichTextLabel.js +1 -0
  19. package/dist-cjs/lib/shapes/shared/RichTextLabel.js.map +2 -2
  20. package/dist-cjs/lib/styles.js.map +2 -2
  21. package/dist-cjs/lib/ui/components/NavigationPanel/DefaultNavigationPanel.js +3 -4
  22. package/dist-cjs/lib/ui/components/NavigationPanel/DefaultNavigationPanel.js.map +2 -2
  23. package/dist-cjs/lib/ui/components/Spinner.js +2 -25
  24. package/dist-cjs/lib/ui/components/Spinner.js.map +2 -2
  25. package/dist-cjs/lib/ui/components/StylePanel/DefaultStylePanelContent.js +2 -1
  26. package/dist-cjs/lib/ui/components/StylePanel/DefaultStylePanelContent.js.map +2 -2
  27. package/dist-cjs/lib/ui/components/primitives/Button/TldrawUiButtonIcon.js.map +2 -2
  28. package/dist-cjs/lib/ui/components/primitives/TldrawUiDialog.js +1 -1
  29. package/dist-cjs/lib/ui/components/primitives/TldrawUiDialog.js.map +2 -2
  30. package/dist-cjs/lib/ui/components/primitives/TldrawUiIcon.js +35 -1
  31. package/dist-cjs/lib/ui/components/primitives/TldrawUiIcon.js.map +2 -2
  32. package/dist-cjs/lib/ui/components/primitives/TldrawUiSlider.js +5 -2
  33. package/dist-cjs/lib/ui/components/primitives/TldrawUiSlider.js.map +2 -2
  34. package/dist-cjs/lib/ui/components/primitives/TldrawUiToolbar.js +1 -0
  35. package/dist-cjs/lib/ui/components/primitives/TldrawUiToolbar.js.map +2 -2
  36. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuCheckboxItem.js.map +2 -2
  37. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuItem.js.map +2 -2
  38. package/dist-cjs/lib/ui/context/actions.js.map +1 -1
  39. package/dist-cjs/lib/ui/hooks/useClipboardEvents.js +24 -7
  40. package/dist-cjs/lib/ui/hooks/useClipboardEvents.js.map +2 -2
  41. package/dist-cjs/lib/ui/hooks/useTools.js.map +2 -2
  42. package/dist-cjs/lib/ui/version.js +3 -3
  43. package/dist-cjs/lib/ui/version.js.map +1 -1
  44. package/dist-esm/index.d.mts +16 -8
  45. package/dist-esm/index.mjs +6 -2
  46. package/dist-esm/index.mjs.map +2 -2
  47. package/dist-esm/lib/TldrawImage.mjs +5 -2
  48. package/dist-esm/lib/TldrawImage.mjs.map +2 -2
  49. package/dist-esm/lib/canvas/TldrawCropHandles.mjs +1 -1
  50. package/dist-esm/lib/canvas/TldrawCropHandles.mjs.map +2 -2
  51. package/dist-esm/lib/canvas/TldrawHandles.mjs +1 -1
  52. package/dist-esm/lib/canvas/TldrawHandles.mjs.map +2 -2
  53. package/dist-esm/lib/canvas/TldrawOverlays.mjs +1 -1
  54. package/dist-esm/lib/canvas/TldrawOverlays.mjs.map +2 -2
  55. package/dist-esm/lib/canvas/TldrawSelectionForeground.mjs +279 -271
  56. package/dist-esm/lib/canvas/TldrawSelectionForeground.mjs.map +2 -2
  57. package/dist-esm/lib/shapes/shared/PathBuilder.mjs +22 -3
  58. package/dist-esm/lib/shapes/shared/PathBuilder.mjs.map +2 -2
  59. package/dist-esm/lib/shapes/shared/PlainTextLabel.mjs +1 -0
  60. package/dist-esm/lib/shapes/shared/PlainTextLabel.mjs.map +2 -2
  61. package/dist-esm/lib/shapes/shared/RichTextLabel.mjs +1 -0
  62. package/dist-esm/lib/shapes/shared/RichTextLabel.mjs.map +2 -2
  63. package/dist-esm/lib/styles.mjs.map +2 -2
  64. package/dist-esm/lib/ui/components/NavigationPanel/DefaultNavigationPanel.mjs +3 -4
  65. package/dist-esm/lib/ui/components/NavigationPanel/DefaultNavigationPanel.mjs.map +2 -2
  66. package/dist-esm/lib/ui/components/Spinner.mjs +3 -26
  67. package/dist-esm/lib/ui/components/Spinner.mjs.map +2 -2
  68. package/dist-esm/lib/ui/components/StylePanel/DefaultStylePanelContent.mjs +2 -1
  69. package/dist-esm/lib/ui/components/StylePanel/DefaultStylePanelContent.mjs.map +2 -2
  70. package/dist-esm/lib/ui/components/primitives/Button/TldrawUiButtonIcon.mjs.map +2 -2
  71. package/dist-esm/lib/ui/components/primitives/TldrawUiDialog.mjs +1 -1
  72. package/dist-esm/lib/ui/components/primitives/TldrawUiDialog.mjs.map +2 -2
  73. package/dist-esm/lib/ui/components/primitives/TldrawUiIcon.mjs +36 -2
  74. package/dist-esm/lib/ui/components/primitives/TldrawUiIcon.mjs.map +2 -2
  75. package/dist-esm/lib/ui/components/primitives/TldrawUiSlider.mjs +5 -2
  76. package/dist-esm/lib/ui/components/primitives/TldrawUiSlider.mjs.map +2 -2
  77. package/dist-esm/lib/ui/components/primitives/TldrawUiToolbar.mjs +1 -0
  78. package/dist-esm/lib/ui/components/primitives/TldrawUiToolbar.mjs.map +2 -2
  79. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuCheckboxItem.mjs.map +2 -2
  80. package/dist-esm/lib/ui/components/primitives/menus/TldrawUiMenuItem.mjs.map +2 -2
  81. package/dist-esm/lib/ui/context/actions.mjs.map +1 -1
  82. package/dist-esm/lib/ui/hooks/useClipboardEvents.mjs +24 -7
  83. package/dist-esm/lib/ui/hooks/useClipboardEvents.mjs.map +2 -2
  84. package/dist-esm/lib/ui/hooks/useTools.mjs.map +2 -2
  85. package/dist-esm/lib/ui/version.mjs +3 -3
  86. package/dist-esm/lib/ui/version.mjs.map +1 -1
  87. package/package.json +4 -3
  88. package/src/index.ts +6 -1
  89. package/src/lib/TldrawImage.tsx +6 -2
  90. package/src/lib/canvas/TldrawCropHandles.tsx +1 -1
  91. package/src/lib/canvas/TldrawHandles.tsx +5 -1
  92. package/src/lib/canvas/TldrawOverlays.tsx +1 -1
  93. package/src/lib/canvas/TldrawSelectionForeground.tsx +5 -1
  94. package/src/lib/shapes/shared/PathBuilder.test.tsx +1 -1
  95. package/src/lib/shapes/shared/PathBuilder.tsx +35 -1
  96. package/src/lib/shapes/shared/PlainTextLabel.tsx +1 -0
  97. package/src/lib/shapes/shared/RichTextLabel.tsx +1 -0
  98. package/src/lib/styles.tsx +3 -1
  99. package/src/lib/ui/components/NavigationPanel/DefaultNavigationPanel.tsx +3 -4
  100. package/src/lib/ui/components/Spinner.tsx +2 -24
  101. package/src/lib/ui/components/StylePanel/DefaultStylePanelContent.tsx +1 -0
  102. package/src/lib/ui/components/primitives/Button/TldrawUiButtonIcon.tsx +2 -2
  103. package/src/lib/ui/components/primitives/TldrawUiDialog.tsx +1 -1
  104. package/src/lib/ui/components/primitives/TldrawUiIcon.tsx +41 -3
  105. package/src/lib/ui/components/primitives/TldrawUiSlider.tsx +5 -1
  106. package/src/lib/ui/components/primitives/TldrawUiToolbar.tsx +4 -0
  107. package/src/lib/ui/components/primitives/menus/TldrawUiMenuCheckboxItem.tsx +2 -2
  108. package/src/lib/ui/components/primitives/menus/TldrawUiMenuItem.tsx +3 -2
  109. package/src/lib/ui/context/actions.tsx +1 -1
  110. package/src/lib/ui/hooks/useClipboardEvents.ts +31 -10
  111. package/src/lib/ui/hooks/useTools.tsx +2 -1
  112. package/src/lib/ui/version.ts +3 -3
  113. package/src/lib/ui.css +8 -22
  114. package/src/test/navigation.test.ts +254 -0
  115. package/tldraw.css +25 -26
@@ -451,6 +451,7 @@ export function OpacitySlider() {
451
451
  steps={tldrawSupportedOpacities.length - 1}
452
452
  title={msg('style-panel.opacity')}
453
453
  onHistoryMark={onHistoryMark}
454
+ ariaValueModifier={25}
454
455
  />
455
456
  )
456
457
  }
@@ -1,8 +1,8 @@
1
- import { TldrawUiIcon } from '../TldrawUiIcon'
1
+ import { TldrawUiIcon, TLUiIconJsx } from '../TldrawUiIcon'
2
2
 
3
3
  /** @public */
4
4
  export interface TLUiButtonIconProps {
5
- icon: string
5
+ icon: string | TLUiIconJsx
6
6
  small?: boolean
7
7
  invertIcon?: boolean
8
8
  }
@@ -65,7 +65,7 @@ export interface TLUiDialogBodyProps {
65
65
  /** @public @react */
66
66
  export function TldrawUiDialogBody({ className, children, style }: TLUiDialogBodyProps) {
67
67
  return (
68
- <div className={classNames('tlui-dialog__body', className)} style={style}>
68
+ <div className={classNames('tlui-dialog__body', className)} style={style} tabIndex={0}>
69
69
  {children}
70
70
  </div>
71
71
  )
@@ -1,11 +1,14 @@
1
1
  import classNames from 'classnames'
2
- import { memo, useLayoutEffect, useRef } from 'react'
2
+ import { cloneElement, memo, ReactElement, useLayoutEffect, useRef } from 'react'
3
3
  import { useAssetUrls } from '../../context/asset-urls'
4
4
  import { TLUiIconType } from '../../icon-types'
5
5
 
6
+ /** @public */
7
+ export type TLUiIconJsx = ReactElement<React.HTMLAttributes<HTMLDivElement>>
8
+
6
9
  /** @public */
7
10
  export interface TLUiIconProps extends React.HTMLAttributes<HTMLDivElement> {
8
- icon: TLUiIconType | Exclude<string, TLUiIconType>
11
+ icon: TLUiIconType | Exclude<string, TLUiIconType> | TLUiIconJsx
9
12
  label: string
10
13
  small?: boolean
11
14
  color?: string
@@ -24,6 +27,41 @@ export const TldrawUiIcon = memo(function TldrawUiIcon({
24
27
  className,
25
28
  ...props
26
29
  }: TLUiIconProps) {
30
+ if (typeof icon === 'string') {
31
+ return (
32
+ <TldrawUIIconInner
33
+ label={label}
34
+ small={small}
35
+ invertIcon={invertIcon}
36
+ icon={icon}
37
+ color={color}
38
+ className={className}
39
+ {...props}
40
+ />
41
+ )
42
+ }
43
+
44
+ return cloneElement(icon, {
45
+ ...props,
46
+ className: classNames({ 'tlui-icon__small': small }, className, icon.props.className),
47
+ 'aria-label': label,
48
+ style: {
49
+ color,
50
+ transform: invertIcon ? 'scale(-1, 1)' : undefined,
51
+ ...icon.props.style,
52
+ },
53
+ })
54
+ })
55
+
56
+ function TldrawUIIconInner({
57
+ label,
58
+ small,
59
+ invertIcon,
60
+ icon,
61
+ color,
62
+ className,
63
+ ...props
64
+ }: TLUiIconProps & { icon: TLUiIconType | Exclude<string, TLUiIconType> }) {
27
65
  const assetUrls = useAssetUrls()
28
66
  const asset = assetUrls.icons[icon as TLUiIconType] ?? assetUrls.icons['question-mark-circle']
29
67
  const ref = useRef<HTMLDivElement>(null)
@@ -69,4 +107,4 @@ export const TldrawUiIcon = memo(function TldrawUiIcon({
69
107
  }}
70
108
  />
71
109
  )
72
- })
110
+ }
@@ -13,6 +13,7 @@ export interface TLUiSliderProps {
13
13
  onValueChange(value: number): void
14
14
  onHistoryMark(id: string): void
15
15
  'data-testid'?: string
16
+ ariaValueModifier?: number
16
17
  }
17
18
 
18
19
  /** @public @react */
@@ -26,6 +27,7 @@ export const TldrawUiSlider = React.forwardRef<HTMLDivElement, TLUiSliderProps>(
26
27
  label,
27
28
  onValueChange,
28
29
  ['data-testid']: testId,
30
+ ariaValueModifier = 1,
29
31
  }: TLUiSliderProps,
30
32
  ref
31
33
  ) {
@@ -81,7 +83,9 @@ export const TldrawUiSlider = React.forwardRef<HTMLDivElement, TLUiSliderProps>(
81
83
  </_Slider.Track>
82
84
  {value !== null && (
83
85
  <_Slider.Thumb
84
- aria-label={msg('style-panel.opacity')}
86
+ aria-valuemin={(min ?? 0) * ariaValueModifier}
87
+ aria-valuenow={value * ariaValueModifier}
88
+ aria-valuemax={steps * ariaValueModifier}
85
89
  className="tlui-slider__thumb"
86
90
  dir="ltr"
87
91
  ref={ref}
@@ -76,6 +76,10 @@ export const TldrawUiToolbarToggleGroup = ({
76
76
  <_Toolbar.ToggleGroup
77
77
  type={type}
78
78
  {...props}
79
+ // TODO: this fixes a bug in Radix until they fix it.
80
+ // https://github.com/radix-ui/primitives/issues/3188
81
+ // https://github.com/radix-ui/primitives/pull/3189
82
+ role="radiogroup"
79
83
  className={classnames('tlui-toolbar-toggle-group', className)}
80
84
  >
81
85
  {children}
@@ -5,7 +5,7 @@ import { TLUiEventSource } from '../../../context/events'
5
5
  import { useReadonly } from '../../../hooks/useReadonly'
6
6
  import { TLUiTranslationKey } from '../../../hooks/useTranslation/TLUiTranslationKey'
7
7
  import { useTranslation } from '../../../hooks/useTranslation/useTranslation'
8
- import { TldrawUiIcon } from '../TldrawUiIcon'
8
+ import { TldrawUiIcon, TLUiIconJsx } from '../TldrawUiIcon'
9
9
  import { TldrawUiKbd } from '../TldrawUiKbd'
10
10
  import { useTldrawUiMenuContext } from './TldrawUiMenuContext'
11
11
 
@@ -14,7 +14,7 @@ export interface TLUiMenuCheckboxItemProps<
14
14
  TranslationKey extends string = string,
15
15
  IconType extends string = string,
16
16
  > {
17
- icon?: IconType
17
+ icon?: IconType | TLUiIconJsx
18
18
  id: string
19
19
  kbd?: string
20
20
  title?: string
@@ -12,6 +12,7 @@ import { TldrawUiButton } from '../Button/TldrawUiButton'
12
12
  import { TldrawUiButtonIcon } from '../Button/TldrawUiButtonIcon'
13
13
  import { TldrawUiButtonLabel } from '../Button/TldrawUiButtonLabel'
14
14
  import { TldrawUiDropdownMenuItem } from '../TldrawUiDropdownMenu'
15
+ import { TLUiIconJsx } from '../TldrawUiIcon'
15
16
  import { TldrawUiKbd } from '../TldrawUiKbd'
16
17
  import { TldrawUiToolbarButton } from '../TldrawUiToolbar'
17
18
  import { useTldrawUiMenuContext } from './TldrawUiMenuContext'
@@ -25,11 +26,11 @@ export interface TLUiMenuItemProps<
25
26
  /**
26
27
  * The icon to display on the item. Icons are only shown in certain menu types.
27
28
  */
28
- icon?: IconType
29
+ icon?: IconType | TLUiIconJsx
29
30
  /**
30
31
  * An icon to display to the left of the menu item.
31
32
  */
32
- iconLeft?: IconType
33
+ iconLeft?: IconType | TLUiIconJsx
33
34
  /**
34
35
  * The keyboard shortcut to display on the item.
35
36
  */
@@ -43,7 +43,7 @@ export interface TLUiActionItem<
43
43
  TransationKey extends string = string,
44
44
  IconType extends string = string,
45
45
  > {
46
- icon?: IconType
46
+ icon?: IconType | React.ReactElement
47
47
  id: string
48
48
  kbd?: string
49
49
  label?: TransationKey | { [key: string]: TransationKey }
@@ -352,7 +352,7 @@ async function handleClipboardThings(editor: Editor, things: ClipboardThing[], p
352
352
 
353
353
  if (tldrawHtmlComment) {
354
354
  try {
355
- // First try parsing as plain JSON (version 2 format)
355
+ // First try parsing as plain JSON (version 2/3 formats)
356
356
  let json
357
357
  try {
358
358
  json = JSON.parse(tldrawHtmlComment)
@@ -380,19 +380,32 @@ async function handleClipboardThings(editor: Editor, things: ClipboardThing[], p
380
380
  }
381
381
 
382
382
  // Handle versioned clipboard format
383
- if (json.version === 2) {
384
- // Version 2: Assets are plain, decompress only other data
383
+ if (json.version === 3) {
384
+ // Version 3: Assets are plain, decompress only other data
385
385
  try {
386
- r({ type: 'tldraw', data: json.data })
386
+ const otherData = JSON.parse(
387
+ lz.decompressFromBase64(json.data.otherCompressed) || '{}'
388
+ )
389
+ const reconstructedData = {
390
+ assets: json.data.assets || [],
391
+ ...otherData,
392
+ }
393
+
394
+ r({ type: 'tldraw', data: reconstructedData })
387
395
  return
388
396
  } catch (error) {
389
397
  r({
390
398
  type: 'error',
391
399
  data: json,
392
- reason: `failed to parse version 2 clipboard data: ${error}`,
400
+ reason: `failed to decompress version 2 clipboard data: ${error}`,
393
401
  })
394
402
  return
395
403
  }
404
+ }
405
+ if (json.version === 2) {
406
+ // Version 2: Everything is plain, this had issues with encoding... :-/
407
+ // TODO: nix this support after some time.
408
+ r({ type: 'tldraw', data: json.data })
396
409
  } else {
397
410
  // Version 1 or no version: Legacy format
398
411
  if (typeof json.data === 'string') {
@@ -584,13 +597,21 @@ const handleNativeOrMenuCopy = async (editor: Editor) => {
584
597
  return
585
598
  }
586
599
 
587
- // Version 2: Don't compress anything.
588
- const stringifiedClipboard = JSON.stringify({
600
+ // Use versioned clipboard format for better compression
601
+ // Version 3: Don't compress assets, only compress other data
602
+ const { assets, ...otherData } = content
603
+ const clipboardData = {
589
604
  type: 'application/tldraw',
590
605
  kind: 'content',
591
- version: 2,
592
- data: content,
593
- })
606
+ version: 3,
607
+ data: {
608
+ assets: assets || [], // Plain JSON, no compression
609
+ otherCompressed: lz.compressToBase64(JSON.stringify(otherData)), // Only compress non-asset data
610
+ },
611
+ }
612
+
613
+ // Don't compress the final structure - just use plain JSON
614
+ const stringifiedClipboard = JSON.stringify(clipboardData)
594
615
 
595
616
  if (typeof navigator === 'undefined') {
596
617
  return
@@ -1,6 +1,7 @@
1
1
  import { Editor, GeoShapeGeoStyle, useMaybeEditor } from '@tldraw/editor'
2
2
  import * as React from 'react'
3
3
  import { EmbedDialog } from '../components/EmbedDialog'
4
+ import { TLUiIconJsx } from '../components/primitives/TldrawUiIcon'
4
5
  import { useA11y } from '../context/a11y'
5
6
  import { TLUiEventSource, useUiEvents } from '../context/events'
6
7
  import { TLUiIconType } from '../icon-types'
@@ -16,7 +17,7 @@ export interface TLUiToolItem<
16
17
  id: string
17
18
  label: TranslationKey
18
19
  shortcutsLabel?: TranslationKey
19
- icon: IconType
20
+ icon: IconType | TLUiIconJsx
20
21
  onSelect(source: TLUiEventSource): void
21
22
  /**
22
23
  * The keyboard shortcut for this tool. This is a string that can be a single key,
@@ -1,9 +1,9 @@
1
1
  // This file is automatically generated by internal/scripts/refresh-assets.ts.
2
2
  // Do not edit manually. Or do, I'm a comment, not a cop.
3
3
 
4
- export const version = '3.15.0-next.39f008bfb627'
4
+ export const version = '3.15.0-next.82ffd490a4f1'
5
5
  export const publishDates = {
6
6
  major: '2024-09-13T14:36:29.063Z',
7
- minor: '2025-07-10T06:54:15.368Z',
8
- patch: '2025-07-10T06:54:15.368Z',
7
+ minor: '2025-07-23T09:26:18.147Z',
8
+ patch: '2025-07-23T09:26:18.147Z',
9
9
  }
package/src/lib/ui.css CHANGED
@@ -964,7 +964,7 @@
964
964
  justify-content: center;
965
965
  border-radius: 99px;
966
966
  opacity: 0;
967
- animation: fade-in;
967
+ animation: tl-fade-in;
968
968
  animation-duration: 0.12s;
969
969
  animation-delay: 2s;
970
970
  animation-fill-mode: forwards;
@@ -1108,20 +1108,6 @@
1108
1108
  display: none;
1109
1109
  }
1110
1110
 
1111
- .tlui-navigation-panel__toggle .tlui-icon {
1112
- opacity: 0.24;
1113
- }
1114
-
1115
- .tlui-navigation-panel__toggle:active .tlui-icon {
1116
- opacity: 1;
1117
- }
1118
-
1119
- @media (hover: hover) {
1120
- .tlui-navigation-panel__toggle:hover .tlui-icon {
1121
- opacity: 1;
1122
- }
1123
- }
1124
-
1125
1111
  /* Minimap */
1126
1112
 
1127
1113
  .tlui-minimap {
@@ -1379,11 +1365,11 @@
1379
1365
 
1380
1366
  @media (prefers-reduced-motion: no-preference) {
1381
1367
  .tlui-toast__container[data-state='open'] {
1382
- animation: slide-in 200ms cubic-bezier(0.785, 0.135, 0.15, 0.86);
1368
+ animation: tlui-slide-in 200ms cubic-bezier(0.785, 0.135, 0.15, 0.86);
1383
1369
  }
1384
1370
 
1385
1371
  .tlui-toast__container[data-state='closed'] {
1386
- animation: hide 100ms ease-in;
1372
+ animation: tlui-fade-out 100ms ease-in;
1387
1373
  }
1388
1374
 
1389
1375
  .tlui-toast__container[data-swipe='move'] {
@@ -1396,7 +1382,7 @@
1396
1382
  }
1397
1383
 
1398
1384
  .tlui-toast__container[data-swipe='end'] {
1399
- animation: swipe-out 100ms ease-out;
1385
+ animation: tlui-slide-out 100ms ease-out;
1400
1386
  }
1401
1387
  }
1402
1388
 
@@ -1411,7 +1397,7 @@
1411
1397
  z-index: var(--layer-canvas-overlays);
1412
1398
  background-color: var(--color-overlay);
1413
1399
  pointer-events: all;
1414
- animation: fadeIn 0.12s ease-out;
1400
+ animation: tl-fade-in 0.12s ease-out;
1415
1401
  display: grid;
1416
1402
  place-items: center;
1417
1403
  overflow-y: auto;
@@ -1978,7 +1964,7 @@
1978
1964
  }
1979
1965
 
1980
1966
  /* ------------------- Animations ------------------- */
1981
- @keyframes hide {
1967
+ @keyframes tlui-fade-out {
1982
1968
  0% {
1983
1969
  opacity: 1;
1984
1970
  }
@@ -1987,7 +1973,7 @@
1987
1973
  }
1988
1974
  }
1989
1975
 
1990
- @keyframes slide-in {
1976
+ @keyframes tlui-slide-in {
1991
1977
  from {
1992
1978
  transform: translateX(calc(100% + var(--space-3)));
1993
1979
  }
@@ -1996,7 +1982,7 @@
1996
1982
  }
1997
1983
  }
1998
1984
 
1999
- @keyframes swipe-out {
1985
+ @keyframes tlui-slide-out {
2000
1986
  from {
2001
1987
  transform: translateX(var(--radix-toast-swipe-end-x));
2002
1988
  }
@@ -414,6 +414,260 @@ describe('Shape navigation', () => {
414
414
  expect(editor.getSelectedShapeIds()).toEqual([])
415
415
  })
416
416
 
417
+ it('respects container boundaries when navigating with left/right', () => {
418
+ // Create a frame with shapes inside and shapes outside
419
+ editor.createShapes([
420
+ {
421
+ id: ids.frame1,
422
+ type: 'frame',
423
+ x: 0,
424
+ y: 0,
425
+ props: {
426
+ w: 200,
427
+ h: 200,
428
+ },
429
+ },
430
+ // Shapes inside frame
431
+ {
432
+ id: ids.box1,
433
+ type: 'geo',
434
+ x: 10,
435
+ y: 100,
436
+ parentId: ids.frame1,
437
+ props: {
438
+ w: 30,
439
+ h: 30,
440
+ },
441
+ },
442
+ {
443
+ id: ids.box2,
444
+ type: 'geo',
445
+ x: 50,
446
+ y: 100,
447
+ parentId: ids.frame1,
448
+ props: {
449
+ w: 30,
450
+ h: 30,
451
+ },
452
+ },
453
+ {
454
+ id: ids.box3,
455
+ type: 'geo',
456
+ x: 90,
457
+ y: 100,
458
+ parentId: ids.frame1,
459
+ props: {
460
+ w: 30,
461
+ h: 30,
462
+ },
463
+ },
464
+ // Shapes outside frame
465
+ {
466
+ id: ids.box4,
467
+ type: 'geo',
468
+ x: 300,
469
+ y: 100,
470
+ props: {
471
+ w: 30,
472
+ h: 30,
473
+ },
474
+ },
475
+ {
476
+ id: ids.box5,
477
+ type: 'geo',
478
+ x: 350,
479
+ y: 100,
480
+ props: {
481
+ w: 30,
482
+ h: 30,
483
+ },
484
+ },
485
+ ])
486
+
487
+ // Setup shape centers for consistent testing
488
+ jest.spyOn(editor, 'getShapePageBounds').mockImplementation((shape: any) => {
489
+ const positions = {
490
+ [ids.box1]: { x: 25, y: 115 },
491
+ [ids.box2]: { x: 65, y: 115 },
492
+ [ids.box3]: { x: 105, y: 115 },
493
+ [ids.box4]: { x: 315, y: 115 },
494
+ [ids.box5]: { x: 365, y: 115 },
495
+ }
496
+ const pos = positions[shape?.id as keyof typeof positions]
497
+ return pos ? ({ center: pos } as any) : ({ center: { x: 0, y: 0 } } as any)
498
+ })
499
+
500
+ // Select a shape inside the frame
501
+ editor.select(ids.box1)
502
+ expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
503
+
504
+ // Navigate right - should stay within the frame
505
+ editor.selectAdjacentShape('right')
506
+ expect(editor.getSelectedShapeIds()).toEqual([ids.box2])
507
+
508
+ // Continue navigating right - should still stay within the frame
509
+ editor.selectAdjacentShape('right')
510
+ expect(editor.getSelectedShapeIds()).toEqual([ids.box3])
511
+
512
+ // Navigate right again - should not leave the frame to go to box4
513
+ editor.selectAdjacentShape('right')
514
+ expect(editor.getSelectedShapeIds()).toEqual([ids.box3]) // Should stay at box3
515
+
516
+ // Now navigate left to test the other direction
517
+ editor.selectAdjacentShape('left')
518
+ expect(editor.getSelectedShapeIds()).toEqual([ids.box2])
519
+
520
+ editor.selectAdjacentShape('left')
521
+ expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
522
+
523
+ // Navigate left again - should not leave the frame
524
+ editor.selectAdjacentShape('left')
525
+ expect(editor.getSelectedShapeIds()).toEqual([ids.box1]) // Should stay at box1
526
+
527
+ // Now test navigation outside the frame
528
+ editor.select(ids.box4)
529
+ expect(editor.getSelectedShapeIds()).toEqual([ids.box4])
530
+
531
+ // Navigate right - should move to box5
532
+ editor.selectAdjacentShape('right')
533
+ expect(editor.getSelectedShapeIds()).toEqual([ids.box5])
534
+
535
+ // Navigate left - should move back to box4
536
+ editor.selectAdjacentShape('left')
537
+ expect(editor.getSelectedShapeIds()).toEqual([ids.box4])
538
+
539
+ // Navigate left again - should select the frame (nearest shape to the left)
540
+ editor.selectAdjacentShape('left')
541
+ expect(editor.getSelectedShapeIds()).toEqual([ids.frame1]) // Should select frame1
542
+ })
543
+
544
+ it('respects container boundaries when navigating with up/down', () => {
545
+ // Create a frame with shapes inside and shapes outside
546
+ editor.createShapes([
547
+ {
548
+ id: ids.frame1,
549
+ type: 'frame',
550
+ x: 0,
551
+ y: 0,
552
+ props: {
553
+ w: 200,
554
+ h: 200,
555
+ },
556
+ },
557
+ // Shapes inside frame - vertically arranged
558
+ {
559
+ id: ids.box1,
560
+ type: 'geo',
561
+ x: 100,
562
+ y: 10,
563
+ parentId: ids.frame1,
564
+ props: {
565
+ w: 30,
566
+ h: 30,
567
+ },
568
+ },
569
+ {
570
+ id: ids.box2,
571
+ type: 'geo',
572
+ x: 100,
573
+ y: 50,
574
+ parentId: ids.frame1,
575
+ props: {
576
+ w: 30,
577
+ h: 30,
578
+ },
579
+ },
580
+ {
581
+ id: ids.box3,
582
+ type: 'geo',
583
+ x: 100,
584
+ y: 90,
585
+ parentId: ids.frame1,
586
+ props: {
587
+ w: 30,
588
+ h: 30,
589
+ },
590
+ },
591
+ // Shapes outside frame - vertically arranged
592
+ {
593
+ id: ids.box4,
594
+ type: 'geo',
595
+ x: 300,
596
+ y: 10,
597
+ props: {
598
+ w: 30,
599
+ h: 30,
600
+ },
601
+ },
602
+ {
603
+ id: ids.box5,
604
+ type: 'geo',
605
+ x: 300,
606
+ y: 50,
607
+ props: {
608
+ w: 30,
609
+ h: 30,
610
+ },
611
+ },
612
+ ])
613
+
614
+ // Setup shape centers for consistent testing
615
+ jest.spyOn(editor, 'getShapePageBounds').mockImplementation((shape: any) => {
616
+ const positions = {
617
+ [ids.box1]: { x: 115, y: 25 },
618
+ [ids.box2]: { x: 115, y: 65 },
619
+ [ids.box3]: { x: 115, y: 105 },
620
+ [ids.box4]: { x: 315, y: 25 },
621
+ [ids.box5]: { x: 315, y: 65 },
622
+ }
623
+ const pos = positions[shape?.id as keyof typeof positions]
624
+ return pos ? ({ center: pos } as any) : ({ center: { x: 0, y: 0 } } as any)
625
+ })
626
+
627
+ // Select a shape inside the frame
628
+ editor.select(ids.box1)
629
+ expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
630
+
631
+ // Navigate down - should stay within the frame
632
+ editor.selectAdjacentShape('down')
633
+ expect(editor.getSelectedShapeIds()).toEqual([ids.box2])
634
+
635
+ // Continue navigating down - should still stay within the frame
636
+ editor.selectAdjacentShape('down')
637
+ expect(editor.getSelectedShapeIds()).toEqual([ids.box3])
638
+
639
+ // Navigate down again - should not leave the frame
640
+ editor.selectAdjacentShape('down')
641
+ expect(editor.getSelectedShapeIds()).toEqual([ids.box3]) // Should stay at box3
642
+
643
+ // Now navigate up to test the other direction
644
+ editor.selectAdjacentShape('up')
645
+ expect(editor.getSelectedShapeIds()).toEqual([ids.box2])
646
+
647
+ editor.selectAdjacentShape('up')
648
+ expect(editor.getSelectedShapeIds()).toEqual([ids.box1])
649
+
650
+ // Navigate up again - should not leave the frame
651
+ editor.selectAdjacentShape('up')
652
+ expect(editor.getSelectedShapeIds()).toEqual([ids.box1]) // Should stay at box1
653
+
654
+ // Now test navigation outside the frame
655
+ editor.select(ids.box4)
656
+ expect(editor.getSelectedShapeIds()).toEqual([ids.box4])
657
+
658
+ // Navigate down - should move to box5
659
+ editor.selectAdjacentShape('down')
660
+ expect(editor.getSelectedShapeIds()).toEqual([ids.box5])
661
+
662
+ // Navigate up - should move back to box4
663
+ editor.selectAdjacentShape('up')
664
+ expect(editor.getSelectedShapeIds()).toEqual([ids.box4])
665
+
666
+ // Navigate up again - should not enter the frame
667
+ editor.selectAdjacentShape('up')
668
+ expect(editor.getSelectedShapeIds()).toEqual([ids.box4]) // Should stay at box4
669
+ })
670
+
417
671
  it('respects container boundaries when navigating with Tab', () => {
418
672
  // Create a frame with shapes inside and shapes outside
419
673
  editor.createShapes([