emojis! 🧑🎨 🎨 ✏️ (#2814)
everyone ❤️'s emojis: https://dropbox.tech/application/dropbox-paper-emojis-and-exformation https://github.com/tldraw/tldraw/assets/469604/8f99f485-de98-44d1-93cb-6eb9c2d87d99 ### Change Type - [x] `minor` — New feature ### Test Plan 1. Test adding lots of emojis! ### Release Notes - Adds emoji picker to text fields.
This commit is contained in:
parent
c4ffa05b12
commit
32f641c1d7
17 changed files with 426 additions and 15 deletions
BIN
.yarn/patches/emoji-mart-npm-5.5.2-10ed58131e.patch
Normal file
BIN
.yarn/patches/emoji-mart-npm-5.5.2-10ed58131e.patch
Normal file
Binary file not shown.
|
@ -1140,6 +1140,8 @@ export class Group2d extends Geometry2d {
|
|||
// (undocumented)
|
||||
getArea(): number;
|
||||
// (undocumented)
|
||||
getLabel(): Geometry2d;
|
||||
// (undocumented)
|
||||
getVertices(): Vec[];
|
||||
// (undocumented)
|
||||
hitTestLineSegment(A: Vec, B: Vec, zoom: number): boolean;
|
||||
|
|
|
@ -22688,6 +22688,38 @@
|
|||
"isAbstract": false,
|
||||
"name": "getArea"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Group2d#getLabel:member(1)",
|
||||
"docComment": "",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "getLabel(): "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "Geometry2d",
|
||||
"canonicalReference": "@tldraw/editor!Geometry2d:class"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isStatic": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 2
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
"overloadIndex": 1,
|
||||
"parameters": [],
|
||||
"isOptional": false,
|
||||
"isAbstract": false,
|
||||
"name": "getLabel"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Group2d#getVertices:member(1)",
|
||||
|
|
|
@ -70,6 +70,10 @@ export class Group2d extends Geometry2d {
|
|||
return this.children[0].area
|
||||
}
|
||||
|
||||
getLabel() {
|
||||
return this.children.filter((c) => c.isLabel)[0]
|
||||
}
|
||||
|
||||
toSimpleSvgPath() {
|
||||
let path = ''
|
||||
for (const child of this.children) {
|
||||
|
|
|
@ -8,7 +8,7 @@ import { Atom, atom, react } from '@tldraw/state'
|
|||
// `true` by default in development and staging, and `false` in production.
|
||||
/** @internal */
|
||||
export const featureFlags: Record<string, DebugFlag<boolean>> = {
|
||||
// canMoveArrowLabel: createFeatureFlag('canMoveArrowLabel'),
|
||||
emojiMenu: createFeatureFlag('emojiMenu'),
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
|
@ -111,16 +111,16 @@ function createDebugValue<T>(
|
|||
})
|
||||
}
|
||||
|
||||
// function createFeatureFlag(
|
||||
// name: string,
|
||||
// defaults: Defaults<boolean> = { all: true, production: false }
|
||||
// ) {
|
||||
// return createDebugValueBase({
|
||||
// name,
|
||||
// defaults,
|
||||
// shouldStoreForSession: true,
|
||||
// })
|
||||
// }
|
||||
function createFeatureFlag(
|
||||
name: string,
|
||||
defaults: Defaults<boolean> = { all: true, production: false }
|
||||
) {
|
||||
return createDebugValueBase({
|
||||
name,
|
||||
defaults,
|
||||
shouldStoreForSession: true,
|
||||
})
|
||||
}
|
||||
|
||||
function createDebugValueBase<T>(def: DebugFlagDef<T>): DebugFlag<T> {
|
||||
const defaultValue = getDefaultValue(def)
|
||||
|
|
|
@ -1420,8 +1420,12 @@ export interface TLUiDialog {
|
|||
// (undocumented)
|
||||
component: (props: TLUiDialogProps) => any;
|
||||
// (undocumented)
|
||||
dialogProps?: any;
|
||||
// (undocumented)
|
||||
id: string;
|
||||
// (undocumented)
|
||||
isCustomDialog?: boolean;
|
||||
// (undocumented)
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
|
|
|
@ -16213,6 +16213,33 @@
|
|||
"endIndex": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "PropertySignature",
|
||||
"canonicalReference": "@tldraw/tldraw!TLUiDialog#dialogProps:member",
|
||||
"docComment": "",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "dialogProps?: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "any"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isReadonly": false,
|
||||
"isOptional": true,
|
||||
"releaseTag": "Public",
|
||||
"name": "dialogProps",
|
||||
"propertyTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "PropertySignature",
|
||||
"canonicalReference": "@tldraw/tldraw!TLUiDialog#id:member",
|
||||
|
@ -16240,6 +16267,33 @@
|
|||
"endIndex": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "PropertySignature",
|
||||
"canonicalReference": "@tldraw/tldraw!TLUiDialog#isCustomDialog:member",
|
||||
"docComment": "",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "isCustomDialog?: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "boolean"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isReadonly": false,
|
||||
"isOptional": true,
|
||||
"releaseTag": "Public",
|
||||
"name": "isCustomDialog",
|
||||
"propertyTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "PropertySignature",
|
||||
"canonicalReference": "@tldraw/tldraw!TLUiDialog#onClose:member",
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
"tldraw.css"
|
||||
],
|
||||
"dependencies": {
|
||||
"@emoji-mart/data": "^1.1.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||
"@radix-ui/react-context-menu": "^2.1.5",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
|
@ -56,6 +57,7 @@
|
|||
"@tldraw/editor": "workspace:*",
|
||||
"canvas-size": "^1.2.6",
|
||||
"classnames": "^2.3.2",
|
||||
"emoji-mart": "patch:emoji-mart@npm%3A5.5.2#~/.yarn/patches/emoji-mart-npm-5.5.2-10ed58131e.patch",
|
||||
"hotkeys-js": "^3.11.2",
|
||||
"lz-string": "^1.4.4"
|
||||
},
|
||||
|
|
|
@ -32,6 +32,7 @@ import { TextLabel } from '../shared/TextLabel'
|
|||
import {
|
||||
FONT_FAMILIES,
|
||||
LABEL_FONT_SIZES,
|
||||
LABEL_PADDING,
|
||||
STROKE_SIZES,
|
||||
TEXT_PROPS,
|
||||
} from '../shared/default-shape-constants'
|
||||
|
@ -59,7 +60,6 @@ import {
|
|||
} from './components/SolidStyleOval'
|
||||
import { SolidStylePolygon, SolidStylePolygonSvg } from './components/SolidStylePolygon'
|
||||
|
||||
const LABEL_PADDING = 16
|
||||
const MIN_SIZE_WITH_LABEL = 17 * 3
|
||||
|
||||
/** @public */
|
||||
|
|
|
@ -58,4 +58,6 @@ export const LABEL_TO_ARROW_PADDING = 20
|
|||
/** @internal */
|
||||
export const ARROW_LABEL_PADDING = 4.25
|
||||
/** @internal */
|
||||
export const LABEL_PADDING = 16
|
||||
/** @internal */
|
||||
export const WAY_TOO_BIG_ARROW_BEND_FACTOR = 10
|
||||
|
|
68
packages/tldraw/src/lib/shapes/shared/emojis/EmojiDialog.tsx
Normal file
68
packages/tldraw/src/lib/shapes/shared/emojis/EmojiDialog.tsx
Normal file
|
@ -0,0 +1,68 @@
|
|||
import { TLEventInfo, track, useEditor } from '@tldraw/editor'
|
||||
import { Picker } from 'emoji-mart'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useDefaultColorTheme } from '../ShapeFill'
|
||||
|
||||
export type EmojiDialogProps = {
|
||||
onClose: () => void
|
||||
text: string
|
||||
top: number
|
||||
left: number
|
||||
onEmojiSelect: (emoji: any) => void
|
||||
onClickOutside: () => void
|
||||
}
|
||||
|
||||
export default track(function EmojiDialog({
|
||||
top,
|
||||
left,
|
||||
onEmojiSelect,
|
||||
onClickOutside,
|
||||
}: EmojiDialogProps) {
|
||||
const editor = useEditor()
|
||||
const theme = useDefaultColorTheme()
|
||||
const ref = useRef(null)
|
||||
const instance = useRef<any>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const eventListener = (event: TLEventInfo) => {
|
||||
if (event.name === 'pointer_down') {
|
||||
onClickOutside()
|
||||
}
|
||||
}
|
||||
editor.on('event', eventListener)
|
||||
|
||||
instance.current = new Picker({
|
||||
maxFrequentRows: 0,
|
||||
onEmojiSelect,
|
||||
onClickOutside,
|
||||
theme: theme.id,
|
||||
searchPosition: 'static',
|
||||
previewPosition: 'none',
|
||||
ref,
|
||||
})
|
||||
EmojiDialogSingleton = instance.current
|
||||
|
||||
return () => {
|
||||
instance.current = null
|
||||
EmojiDialogSingleton = null
|
||||
editor.off('event', eventListener)
|
||||
}
|
||||
}, [editor, theme.id, onEmojiSelect, onClickOutside])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top,
|
||||
left,
|
||||
zIndex: 400,
|
||||
pointerEvents: 'all',
|
||||
transformOrigin: '0 0',
|
||||
transform: 'scale(0.85)',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
export let EmojiDialogSingleton: { component: any } | null = null
|
11
packages/tldraw/src/lib/shapes/shared/emojis/index.tsx
Normal file
11
packages/tldraw/src/lib/shapes/shared/emojis/index.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { Suspense, lazy } from 'react'
|
||||
|
||||
const EmojiDialog = lazy(() => import('./EmojiDialog'))
|
||||
|
||||
export default function EmojiDialogLazy(props: any) {
|
||||
return (
|
||||
<Suspense fallback={<div />}>
|
||||
<EmojiDialog {...props} />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
|
@ -1,8 +1,15 @@
|
|||
/* eslint-disable no-inner-declarations */
|
||||
|
||||
import {
|
||||
Editor,
|
||||
Group2d,
|
||||
Rectangle2d,
|
||||
TLArrowShape,
|
||||
TLGeoShape,
|
||||
TLShape,
|
||||
TLTextShape,
|
||||
TLUnknownShape,
|
||||
featureFlags,
|
||||
getPointerInfo,
|
||||
preventDefault,
|
||||
stopEventPropagation,
|
||||
|
@ -11,6 +18,16 @@ import {
|
|||
} from '@tldraw/editor'
|
||||
import React, { useCallback, useEffect, useRef } from 'react'
|
||||
import { INDENT, TextHelpers } from './TextHelpers'
|
||||
import {
|
||||
ARROW_LABEL_FONT_SIZES,
|
||||
ARROW_LABEL_PADDING,
|
||||
FONT_FAMILIES,
|
||||
FONT_SIZES,
|
||||
LABEL_FONT_SIZES,
|
||||
LABEL_PADDING,
|
||||
TEXT_PROPS,
|
||||
} from './default-shape-constants'
|
||||
import { useEmojis } from './useEmojis'
|
||||
|
||||
export function useEditableText<T extends Extract<TLShape, { props: { text: string } }>>(
|
||||
id: T['id'],
|
||||
|
@ -18,8 +35,12 @@ export function useEditableText<T extends Extract<TLShape, { props: { text: stri
|
|||
text: string
|
||||
) {
|
||||
const editor = useEditor()
|
||||
|
||||
const rInput = useRef<HTMLTextAreaElement>(null)
|
||||
const { onKeyDown: onEmojiKeyDown } = useEmojis(rInput.current, (text: string) => {
|
||||
editor.updateShapes<TLUnknownShape & { props: { text: string } }>([
|
||||
{ id, type, props: { text } },
|
||||
])
|
||||
})
|
||||
const rSkipSelectOnFocus = useRef(false)
|
||||
const rSelectionRanges = useRef<Range[] | null>()
|
||||
|
||||
|
@ -101,6 +122,14 @@ export function useEditableText<T extends Extract<TLShape, { props: { text: stri
|
|||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (!isEditing) return
|
||||
|
||||
if (featureFlags.emojiMenu.get()) {
|
||||
const coords = getCaretPosition(editor, e.target as HTMLTextAreaElement)
|
||||
const isHandledByEmoji = onEmojiKeyDown(e, coords)
|
||||
if (isHandledByEmoji) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
switch (e.key) {
|
||||
case 'Enter': {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
|
@ -119,7 +148,7 @@ export function useEditableText<T extends Extract<TLShape, { props: { text: stri
|
|||
}
|
||||
}
|
||||
},
|
||||
[editor, isEditing]
|
||||
[editor, isEditing, onEmojiKeyDown]
|
||||
)
|
||||
|
||||
// When the text changes, update the text value.
|
||||
|
@ -207,3 +236,46 @@ export function useEditableText<T extends Extract<TLShape, { props: { text: stri
|
|||
isEmpty,
|
||||
}
|
||||
}
|
||||
|
||||
function getCaretPosition(editor: Editor, inputEl: HTMLTextAreaElement | null) {
|
||||
const selectedShape = editor.getOnlySelectedShape() as
|
||||
| TLTextShape
|
||||
| TLArrowShape
|
||||
| TLGeoShape
|
||||
| undefined
|
||||
if (!selectedShape) return null
|
||||
|
||||
let labelX, labelY
|
||||
if (selectedShape.type === 'text') {
|
||||
labelX = selectedShape.x
|
||||
labelY = selectedShape.y
|
||||
} else {
|
||||
const geometry = editor.getShapeGeometry(selectedShape)
|
||||
if (!(geometry instanceof Group2d)) return null
|
||||
const labelGeometry = geometry.getLabel() as Rectangle2d
|
||||
const padding = selectedShape.type === 'arrow' ? ARROW_LABEL_PADDING : LABEL_PADDING
|
||||
labelX = selectedShape.x + labelGeometry.x + padding
|
||||
labelY = selectedShape.y + labelGeometry.y + padding
|
||||
}
|
||||
|
||||
const sizeSet =
|
||||
selectedShape.type === 'arrow'
|
||||
? ARROW_LABEL_FONT_SIZES
|
||||
: selectedShape.type === 'text'
|
||||
? FONT_SIZES
|
||||
: LABEL_FONT_SIZES
|
||||
const substring = !inputEl ? '' : inputEl.value.substring(0, inputEl.selectionStart)
|
||||
const { w, h } = editor.textMeasure.measureText(substring, {
|
||||
...TEXT_PROPS,
|
||||
fontFamily: FONT_FAMILIES[selectedShape.props.font],
|
||||
fontSize: sizeSet[selectedShape.props.size],
|
||||
maxWidth: null,
|
||||
})
|
||||
|
||||
const { x, y } = editor.pageToScreen({ x: labelX, y: labelY })
|
||||
const zoomLevel = editor.getZoomLevel()
|
||||
const top = y + h * zoomLevel
|
||||
const left = x + w * zoomLevel
|
||||
|
||||
return { top, left }
|
||||
}
|
||||
|
|
125
packages/tldraw/src/lib/shapes/shared/useEmojis.ts
Normal file
125
packages/tldraw/src/lib/shapes/shared/useEmojis.ts
Normal file
|
@ -0,0 +1,125 @@
|
|||
import { useState } from 'react'
|
||||
import { useDialogs } from '../../ui/hooks/useDialogsProvider'
|
||||
import EmojiDialog from './emojis'
|
||||
import { EmojiDialogSingleton } from './emojis/EmojiDialog'
|
||||
|
||||
export function useEmojis(inputEl: HTMLTextAreaElement | null, onComplete: (text: string) => void) {
|
||||
const { addDialog, removeDialog } = useDialogs()
|
||||
const [emojiSearchText, setEmojiSearchText] = useState('')
|
||||
const [isEmojiMenuOpen, setIsEmojiMenuOpen] = useState(false)
|
||||
|
||||
const closeMenu = () => {
|
||||
setIsEmojiMenuOpen(false)
|
||||
removeDialog('emoji')
|
||||
setEmojiSearchText('')
|
||||
}
|
||||
|
||||
const onEmojiSelect = (emoji: any) => {
|
||||
if (!inputEl) return
|
||||
|
||||
const searchText = EmojiDialogSingleton?.component.refs.searchInput.current.value
|
||||
inputEl.focus()
|
||||
inputEl.setSelectionRange(
|
||||
inputEl.selectionStart - searchText.length - 1,
|
||||
inputEl.selectionStart
|
||||
)
|
||||
inputEl.setRangeText(emoji.native)
|
||||
inputEl.setSelectionRange(inputEl.selectionStart + 1, inputEl.selectionStart + 1)
|
||||
onComplete(inputEl.value)
|
||||
|
||||
closeMenu()
|
||||
}
|
||||
|
||||
const onKeyDown = (
|
||||
e: React.KeyboardEvent<HTMLTextAreaElement>,
|
||||
coords: { top: number; left: number } | null
|
||||
) => {
|
||||
const emojiPicker = EmojiDialogSingleton?.component
|
||||
|
||||
switch (e.key) {
|
||||
case ':': {
|
||||
if (isEmojiMenuOpen) {
|
||||
closeMenu()
|
||||
return false
|
||||
}
|
||||
|
||||
setEmojiSearchText('')
|
||||
addDialog({
|
||||
id: 'emoji',
|
||||
component: EmojiDialog,
|
||||
isCustomDialog: true,
|
||||
dialogProps: {
|
||||
onEmojiSelect,
|
||||
onClickOutside: closeMenu,
|
||||
top: coords?.top,
|
||||
left: coords?.left,
|
||||
},
|
||||
})
|
||||
setIsEmojiMenuOpen(true)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
case ' ':
|
||||
// fall-through
|
||||
case 'Escape': {
|
||||
if (isEmojiMenuOpen) {
|
||||
closeMenu()
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
case 'Enter':
|
||||
case 'ArrowLeft':
|
||||
case 'ArrowRight':
|
||||
case 'ArrowUp':
|
||||
case 'ArrowDown': {
|
||||
if (isEmojiMenuOpen) {
|
||||
emojiPicker.handleSearchKeyDown({
|
||||
...e,
|
||||
preventDefault: () => {
|
||||
/* shim */
|
||||
},
|
||||
stopImmediatePropagation: () => {
|
||||
/* shim */
|
||||
},
|
||||
})
|
||||
e.preventDefault()
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
case 'Backspace':
|
||||
if (isEmojiMenuOpen) {
|
||||
if (!emojiSearchText) {
|
||||
closeMenu()
|
||||
return true
|
||||
}
|
||||
|
||||
const text = emojiSearchText.slice(0, -1)
|
||||
emojiPicker.refs.searchInput.current.value = text
|
||||
emojiPicker.handleSearchInput()
|
||||
setEmojiSearchText(text)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
default:
|
||||
if (isEmojiMenuOpen && e.key.length === 1 && e.key.match(/[a-zA-Z0-9]/)) {
|
||||
emojiPicker.refs.searchInput.current.value = emojiSearchText + e.key
|
||||
emojiPicker.handleSearchInput()
|
||||
setEmojiSearchText(emojiSearchText + e.key)
|
||||
return true
|
||||
}
|
||||
|
||||
return isEmojiMenuOpen
|
||||
}
|
||||
}
|
||||
|
||||
return { onKeyDown }
|
||||
}
|
|
@ -3,7 +3,13 @@ import { useContainer } from '@tldraw/editor'
|
|||
import React, { useCallback } from 'react'
|
||||
import { TLUiDialog, useDialogs } from '../hooks/useDialogsProvider'
|
||||
|
||||
const Dialog = ({ id, component: ModalContent, onClose }: TLUiDialog) => {
|
||||
const Dialog = ({
|
||||
id,
|
||||
component: ModalContent,
|
||||
onClose,
|
||||
isCustomDialog,
|
||||
dialogProps,
|
||||
}: TLUiDialog) => {
|
||||
const { removeDialog } = useDialogs()
|
||||
|
||||
const container = useContainer()
|
||||
|
@ -24,6 +30,10 @@ const Dialog = ({ id, component: ModalContent, onClose }: TLUiDialog) => {
|
|||
[id, onClose, removeDialog]
|
||||
)
|
||||
|
||||
if (isCustomDialog) {
|
||||
return <ModalContent onClose={() => handleOpenChange(false)} {...dialogProps} />
|
||||
}
|
||||
|
||||
return (
|
||||
<_Dialog.Root onOpenChange={handleOpenChange} defaultOpen>
|
||||
<_Dialog.Portal container={container}>
|
||||
|
|
|
@ -12,6 +12,8 @@ export interface TLUiDialog {
|
|||
id: string
|
||||
onClose?: () => void
|
||||
component: (props: TLUiDialogProps) => any
|
||||
isCustomDialog?: boolean
|
||||
dialogProps?: any
|
||||
}
|
||||
|
||||
/** @public */
|
||||
|
|
23
yarn.lock
23
yarn.lock
|
@ -2608,6 +2608,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@emoji-mart/data@npm:^1.1.2":
|
||||
version: 1.1.2
|
||||
resolution: "@emoji-mart/data@npm:1.1.2"
|
||||
checksum: c285aa159b00b728d37bc2c6a30ad007fd0d468982c80b16bc8ef6a982c615e9811d297a11ff485ee981bd9a41a63988ad34e8d4a65fb807c4a1bf74f3e92443
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@emotion/hash@npm:^0.9.0":
|
||||
version: 0.9.1
|
||||
resolution: "@emotion/hash@npm:0.9.1"
|
||||
|
@ -7477,6 +7484,7 @@ __metadata:
|
|||
version: 0.0.0-use.local
|
||||
resolution: "@tldraw/tldraw@workspace:packages/tldraw"
|
||||
dependencies:
|
||||
"@emoji-mart/data": "npm:^1.1.2"
|
||||
"@peculiar/webcrypto": "npm:^1.4.0"
|
||||
"@radix-ui/react-alert-dialog": "npm:^1.0.5"
|
||||
"@radix-ui/react-context-menu": "npm:^2.1.5"
|
||||
|
@ -7495,6 +7503,7 @@ __metadata:
|
|||
canvas-size: "npm:^1.2.6"
|
||||
chokidar-cli: "npm:^3.0.0"
|
||||
classnames: "npm:^2.3.2"
|
||||
emoji-mart: "patch:emoji-mart@npm%3A5.5.2#~/.yarn/patches/emoji-mart-npm-5.5.2-10ed58131e.patch"
|
||||
hotkeys-js: "npm:^3.11.2"
|
||||
jest-canvas-mock: "npm:^2.5.2"
|
||||
jest-environment-jsdom: "npm:^29.4.3"
|
||||
|
@ -12078,6 +12087,20 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"emoji-mart@npm:5.5.2":
|
||||
version: 5.5.2
|
||||
resolution: "emoji-mart@npm:5.5.2"
|
||||
checksum: 3b891f7940b25fbe83eb784b655bca9fa1b552ddf1c76bf631d51c4b2adef7072834f58b08ed63dc3fccd635d63de7a38f40cd0320fa7c24d97b9ced60dec957
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"emoji-mart@patch:emoji-mart@npm%3A5.5.2#~/.yarn/patches/emoji-mart-npm-5.5.2-10ed58131e.patch":
|
||||
version: 5.5.2
|
||||
resolution: "emoji-mart@patch:emoji-mart@npm%3A5.5.2#~/.yarn/patches/emoji-mart-npm-5.5.2-10ed58131e.patch::version=5.5.2&hash=61a502"
|
||||
checksum: 219b8f4b146f439564d40b0b25bb2090c307e7225890e585aea9d949be405d09526090d80d096fd1eebb778d43f4b354bf3c511ed6919d92780bed8735348d40
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"emoji-regex@npm:^10.3.0":
|
||||
version: 10.3.0
|
||||
resolution: "emoji-regex@npm:10.3.0"
|
||||
|
|
Loading…
Reference in a new issue