>((props, ref) => {
+ let createComponentProps = props;
+ if (prepareProps) {
+ createComponentProps = prepareProps(props);
+ }
+
+ return ;
+ });
+};
diff --git a/src/web/WebShape.ts b/src/web/WebShape.ts
deleted file mode 100644
index 9e00d2a99..000000000
--- a/src/web/WebShape.ts
+++ /dev/null
@@ -1,121 +0,0 @@
-import React from 'react';
-import {
- GestureResponderEvent,
- // @ts-ignore it is not seen in exports
- unstable_createElement as createElement,
-} from 'react-native';
-
-import { BaseProps } from './types';
-import { prepare } from './utils/prepare';
-import { convertInt32ColorToRGBA } from './utils/convertInt32Color';
-import { camelCaseToDashed, hasTouchableProperty, remeasure } from './utils';
-import SvgTouchableMixin from '../lib/SvgTouchableMixin';
-
-export class WebShape<
- P extends BaseProps = BaseProps,
-> extends React.Component {
- [x: string]: unknown;
- protected tag?: React.ElementType;
- protected prepareProps(props: P) {
- return props;
- }
-
- elementRef =
- React.createRef() as React.MutableRefObject;
-
- lastMergedProps: Partial = {};
-
- /**
- * disclaimer: I am not sure why the props are wrapped in a `style` attribute here, but that's how reanimated calls it
- */
- setNativeProps(props: { style: P }) {
- const merged = Object.assign(
- {},
- this.props,
- this.lastMergedProps,
- props.style
- );
- this.lastMergedProps = merged;
- const clean = prepare(this, this.prepareProps(merged));
- const current = this.elementRef.current;
- if (current) {
- for (const cleanAttribute of Object.keys(clean)) {
- const cleanValue = clean[cleanAttribute as keyof typeof clean];
- switch (cleanAttribute) {
- case 'ref':
- case 'children':
- break;
- case 'style':
- // style can be an object here or an array, so we convert it to an array and assign each element
- for (const partialStyle of ([] as unknown[]).concat(
- clean.style ?? []
- )) {
- Object.assign(current.style, partialStyle);
- }
- break;
- case 'fill':
- if (cleanValue && typeof cleanValue === 'object') {
- const value = cleanValue as { payload: number };
- current.setAttribute(
- 'fill',
- convertInt32ColorToRGBA(value.payload)
- );
- }
- break;
- case 'stroke':
- if (cleanValue && typeof cleanValue === 'object') {
- const value = cleanValue as { payload: number };
- current.setAttribute(
- 'stroke',
- convertInt32ColorToRGBA(value.payload)
- );
- }
- break;
- default:
- // apply all other incoming prop updates as attributes on the node
- // same logic as in https://github.com/software-mansion/react-native-reanimated/blob/d04720c82f5941532991b235787285d36d717247/src/reanimated2/js-reanimated/index.ts#L38-L39
- // @ts-expect-error TODO: fix this
- current.setAttribute(camelCaseToDashed(cleanAttribute), cleanValue);
- break;
- }
- }
- }
- }
-
- _remeasureMetricsOnActivation: () => void;
- touchableHandleStartShouldSetResponder?: (
- e: GestureResponderEvent
- ) => boolean;
-
- touchableHandleResponderMove?: (e: GestureResponderEvent) => void;
- touchableHandleResponderGrant?: (e: GestureResponderEvent) => void;
- touchableHandleResponderRelease?: (e: GestureResponderEvent) => void;
- touchableHandleResponderTerminate?: (e: GestureResponderEvent) => void;
- touchableHandleResponderTerminationRequest?: (
- e: GestureResponderEvent
- ) => boolean;
-
- constructor(props: P) {
- super(props);
-
- // Do not attach touchable mixin handlers if SVG element doesn't have a touchable prop
- if (hasTouchableProperty(props)) {
- SvgTouchableMixin(this);
- }
-
- this._remeasureMetricsOnActivation = remeasure.bind(this);
- }
-
- render(): JSX.Element {
- if (!this.tag) {
- throw new Error(
- 'When extending `WebShape` you need to overwrite either `tag` or `render`!'
- );
- }
- this.lastMergedProps = {};
- return createElement(
- this.tag,
- prepare(this, this.prepareProps(this.props))
- );
- }
-}
diff --git a/src/web/WebShape.tsx b/src/web/WebShape.tsx
new file mode 100644
index 000000000..edca5c4ff
--- /dev/null
+++ b/src/web/WebShape.tsx
@@ -0,0 +1,84 @@
+import React, { useImperativeHandle, useRef } from 'react';
+import {
+ // @ts-ignore it is not seen in exports
+ unstable_createElement as createElement,
+} from 'react-native';
+import { useMergeRefs } from './webUtils.web';
+import type { CreateComponentProps } from '../types';
+import { prepare } from './utils/prepare';
+import { useHandleEvents } from './hooks/useHandleEvents';
+import { setNativeProps } from './utils/setNativeProps';
+import { encodeSvg, getBoundingClientRect } from './utils';
+
+export const WebShape = (
+ props: CreateComponentProps,
+ forwardedRef: React.Ref
+) => {
+ const { tag: Tag } = props;
+ const currentRef = useRef(null);
+ const { elementRef, rest, onClick } = useHandleEvents(currentRef, props);
+ const lastMergedProps = useRef>({});
+
+ setNativeProps(
+ rest,
+ elementRef as React.MutableRefObject,
+ lastMergedProps
+ );
+ const prep = prepare({ ...rest, elementRef });
+ const setRef = useMergeRefs(prep.ref, forwardedRef);
+
+ if (Tag === 'svg') {
+ useImperativeHandle(
+ forwardedRef as React.MutableRefObject,
+ () => {
+ return {
+ toDataURL(
+ callback: (data: string) => void,
+ options: { width?: number; height?: number } = {}
+ ) {
+ const ref = elementRef.current as SVGElement;
+
+ if (ref === null) {
+ return;
+ }
+ const rect = getBoundingClientRect(ref);
+
+ const width = Number(options.width) || rect.width;
+ const height = Number(options.height) || rect.height;
+
+ const svg = document.createElementNS(
+ 'http://www.w3.org/2000/svg',
+ 'svg'
+ );
+ svg.setAttribute('viewBox', `0 0 ${rect.width} ${rect.height}`);
+ svg.setAttribute('width', String(width));
+ svg.setAttribute('height', String(height));
+ svg.appendChild(ref.cloneNode(true));
+
+ const img = new window.Image();
+ img.onload = () => {
+ const canvas = document.createElement('canvas');
+ canvas.width = width;
+ canvas.height = height;
+ const context = canvas.getContext('2d');
+ context?.drawImage(img, 0, 0);
+ callback(
+ canvas.toDataURL().replace('data:image/png;base64,', '')
+ );
+ };
+ img.src = `data:image/svg+xml;utf8,${encodeSvg(
+ new window.XMLSerializer().serializeToString(svg)
+ )}`;
+ },
+ };
+ },
+ []
+ );
+ }
+ return createElement(Tag, {
+ ...prep,
+ onClick,
+ collapsable: undefined,
+ ref: setRef,
+ });
+};
diff --git a/src/web/hooks/types.ts b/src/web/hooks/types.ts
new file mode 100644
index 000000000..1cacd6779
--- /dev/null
+++ b/src/web/hooks/types.ts
@@ -0,0 +1,66 @@
+type ClickEvent = any;
+type ResponderEvent = any;
+type KeyboardEvent = any;
+
+export type PressResponderConfig = Readonly<{
+ // The gesture can be interrupted by a parent gesture, e.g., scroll.
+ // Defaults to true.
+ cancelable?: boolean | null | undefined;
+ // Whether to disable initialization of the press gesture.
+ disabled?: boolean | null | undefined;
+ // Duration (in addition to `delayPressStart`) after which a press gesture is
+ // considered a long press gesture. Defaults to 500 (milliseconds).
+ delayLongPress?: number | null | undefined;
+ // Duration to wait after press down before calling `onPressStart`.
+ delayPressStart?: number | null | undefined;
+ // Duration to wait after letting up before calling `onPressEnd`.
+ delayPressEnd?: number | null | undefined;
+ // Called when a long press gesture has been triggered.
+ onLongPress?: (event: ResponderEvent) => void | null | undefined;
+ // Called when a press gestute has been triggered.
+ onPress?: (event: ClickEvent) => void | null | undefined;
+ // Called when the press is activated to provide visual feedback.
+ onPressChange?: (event: ResponderEvent) => void | null | undefined;
+ // Called when the press is activated to provide visual feedback.
+ onPressStart?: (event: ResponderEvent) => void | null | undefined;
+ // Called when the press location moves. (This should rarely be used.)
+ onPressMove?: (event: ResponderEvent) => void | null | undefined;
+ // Called when the press is deactivated to undo visual feedback.
+ onPressEnd?: (event: ResponderEvent) => void | null | undefined;
+}>;
+
+export type EventHandlers = Readonly<{
+ onClick: (event: ClickEvent) => void;
+ onContextMenu: (event: ClickEvent) => void;
+ onKeyDown: (event: KeyboardEvent) => void;
+ onResponderGrant: (event: ResponderEvent) => void;
+ onResponderMove: (event: ResponderEvent) => void;
+ onResponderRelease: (event: ResponderEvent) => void;
+ onResponderTerminate: (event: ResponderEvent) => void;
+ onResponderTerminationRequest: (event: ResponderEvent) => boolean;
+ onStartShouldSetResponder: (event: ResponderEvent) => boolean;
+}>;
+
+export type ResponderConfig = {
+ // Direct responder events dispatched directly to responder. Do not bubble.
+ onResponderEnd?: (e: ResponderEvent) => void;
+ onResponderGrant?: (e: ResponderEvent) => void | boolean;
+ onResponderMove?: (e: ResponderEvent) => void;
+ onResponderRelease?: (e: ResponderEvent) => void;
+ onResponderReject?: (e: ResponderEvent) => void;
+ onResponderStart?: (e: ResponderEvent) => void;
+ onResponderTerminate?: (e: ResponderEvent) => void;
+ onResponderTerminationRequest?: (e: ResponderEvent) => boolean;
+ // On pointer down, should this element become the responder?
+ onStartShouldSetResponder?: (e: ResponderEvent) => boolean;
+ onStartShouldSetResponderCapture?: (e: ResponderEvent) => boolean;
+ // On pointer move, should this element become the responder?
+ onMoveShouldSetResponder?: (e: ResponderEvent) => boolean;
+ onMoveShouldSetResponderCapture?: (e: ResponderEvent) => boolean;
+ // On scroll, should this element become the responder? Do no bubble
+ onScrollShouldSetResponder?: (e: ResponderEvent) => boolean;
+ onScrollShouldSetResponderCapture?: (e: ResponderEvent) => boolean;
+ // On text selection change, should this element become the responder?
+ onSelectionChangeShouldSetResponder?: (e: ResponderEvent) => boolean;
+ onSelectionChangeShouldSetResponderCapture?: (e: ResponderEvent) => boolean;
+};
diff --git a/src/web/hooks/useHandleEvents.ts b/src/web/hooks/useHandleEvents.ts
new file mode 100644
index 000000000..51d49fe39
--- /dev/null
+++ b/src/web/hooks/useHandleEvents.ts
@@ -0,0 +1,89 @@
+import { MutableRefObject, useMemo } from 'react';
+import { usePressEvents, useResponderEvents } from '../webUtils.web';
+import { hasResponderEvents, hasTouchableProperty } from '../utils/hasProperty';
+import type { CreateComponentProps } from '../../types';
+import { EventHandlers } from './types';
+
+export function useHandleEvents(
+ elementRef: MutableRefObject,
+ props: CreateComponentProps
+) {
+ const {
+ onMoveShouldSetResponder,
+ onMoveShouldSetResponderCapture,
+ onResponderEnd,
+ onResponderGrant,
+ onResponderMove,
+ onResponderReject,
+ onResponderRelease,
+ onResponderStart,
+ onResponderTerminate,
+ onResponderTerminationRequest,
+ onScrollShouldSetResponder,
+ onScrollShouldSetResponderCapture,
+ onSelectionChangeShouldSetResponder,
+ onSelectionChangeShouldSetResponderCapture,
+ onStartShouldSetResponder,
+ onStartShouldSetResponderCapture,
+ onPress,
+ onLongPress,
+ onPressIn,
+ onPressOut,
+ onPressMove,
+ delayPressIn,
+ delayPressOut,
+ delayLongPress,
+ ...rest
+ } = props;
+
+ let pressEventHandlers: EventHandlers | undefined;
+ if (hasTouchableProperty(props)) {
+ const pressConfig = useMemo(
+ () => ({
+ onLongPress,
+ onPress,
+ onPressMove,
+ delayLongPress,
+ delayPressStart: delayPressIn,
+ delayPressEnd: delayPressOut,
+ onPressStart: onPressIn,
+ onPressEnd: onPressOut,
+ }),
+ [
+ delayPressIn,
+ delayPressOut,
+ onLongPress,
+ delayLongPress,
+ onPress,
+ onPressIn,
+ onPressOut,
+ ]
+ );
+ pressEventHandlers = usePressEvents(elementRef, pressConfig);
+ useResponderEvents(elementRef, pressEventHandlers);
+ } else if (hasResponderEvents(props)) {
+ useResponderEvents(elementRef, {
+ onMoveShouldSetResponder,
+ onMoveShouldSetResponderCapture,
+ onResponderEnd,
+ onResponderGrant,
+ onResponderMove,
+ onResponderReject,
+ onResponderRelease,
+ onResponderStart,
+ onResponderTerminate,
+ onResponderTerminationRequest,
+ onScrollShouldSetResponder,
+ onScrollShouldSetResponderCapture,
+ onSelectionChangeShouldSetResponder,
+ onSelectionChangeShouldSetResponderCapture,
+ onStartShouldSetResponder,
+ onStartShouldSetResponderCapture,
+ });
+ }
+ return {
+ elementRef,
+ rest,
+ onClick: pressEventHandlers?.onClick,
+ };
+}
diff --git a/src/web/react-native-web.d.ts b/src/web/react-native-web.d.ts
new file mode 100644
index 000000000..b94085cf6
--- /dev/null
+++ b/src/web/react-native-web.d.ts
@@ -0,0 +1,4 @@
+'use strict';
+declare module 'react-native-web/dist/modules/usePressEvents';
+declare module 'react-native-web/dist/modules/useResponderEvents';
+declare module 'react-native-web/dist/modules/useMergeRefs';
diff --git a/src/web/types.ts b/src/web/types.ts
index 81add91b1..f5490d341 100644
--- a/src/web/types.ts
+++ b/src/web/types.ts
@@ -60,7 +60,7 @@ export interface BaseProps {
| React.MutableRefObject;
style?: Iterable;
- // different tranform props
+ // different transform props
gradientTransform?: TransformProps['transform'];
patternTransform?: TransformProps['transform'];
}
diff --git a/src/web/utils/hasProperty.ts b/src/web/utils/hasProperty.ts
new file mode 100644
index 000000000..c3347b9a5
--- /dev/null
+++ b/src/web/utils/hasProperty.ts
@@ -0,0 +1,33 @@
+import { BaseProps } from '../../types';
+
+export function hasTouchableProperty(props: BaseProps): boolean {
+ return !!(
+ props.onPress ||
+ props.onPressIn ||
+ props.onPressOut ||
+ props.onLongPress
+ );
+}
+
+export function hasResponderEvents(props: BaseProps): boolean {
+ const responderEvents = [
+ 'onResponderGrant',
+ 'onResponderMove',
+ 'onResponderRelease',
+ 'onResponderTerminate',
+ 'onResponderTerminationRequest',
+ 'onMoveShouldSetResponder',
+ 'onMoveShouldSetResponderCapture',
+ 'onResponderEnd',
+ 'onResponderReject',
+ 'onResponderStart',
+ 'onScrollShouldSetResponder',
+ 'onScrollShouldSetResponderCapture',
+ 'onSelectionChangeShouldSetResponder',
+ 'onSelectionChangeShouldSetResponderCapture',
+ 'onStartShouldSetResponder',
+ 'onStartShouldSetResponderCapture',
+ ];
+
+ return responderEvents.some((event) => !!props[event as keyof BaseProps]);
+}
diff --git a/src/web/utils/index.ts b/src/web/utils/index.ts
index 0feb34ad9..b533f31aa 100644
--- a/src/web/utils/index.ts
+++ b/src/web/utils/index.ts
@@ -1,74 +1,7 @@
-import { BaseProps } from '../types';
-import type { TransformProps } from '../../lib/extract/types';
-import {
- transformsArrayToProps,
- TransformsStyleArray,
-} from '../../lib/extract/extractTransform';
-
-export const hasTouchableProperty = (props: BaseProps) =>
- props.onPress || props.onPressIn || props.onPressOut || props.onLongPress;
-
export const camelCaseToDashed = (camelCase: string) => {
return camelCase.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
};
-function stringifyTransformProps(transformProps: TransformProps) {
- const transformArray = [];
- if (transformProps.translate != null) {
- transformArray.push(`translate(${transformProps.translate})`);
- }
- if (transformProps.translateX != null || transformProps.translateY != null) {
- transformArray.push(
- `translate(${transformProps.translateX || 0}, ${
- transformProps.translateY || 0
- })`
- );
- }
- if (transformProps.scale != null) {
- transformArray.push(`scale(${transformProps.scale})`);
- }
- if (transformProps.scaleX != null || transformProps.scaleY != null) {
- transformArray.push(
- `scale(${transformProps.scaleX || 1}, ${transformProps.scaleY || 1})`
- );
- }
- // rotation maps to rotate, not to collide with the text rotate attribute (which acts per glyph rather than block)
- if (transformProps.rotation != null) {
- transformArray.push(`rotate(${transformProps.rotation})`);
- }
- if (transformProps.skewX != null) {
- transformArray.push(`skewX(${transformProps.skewX})`);
- }
- if (transformProps.skewY != null) {
- transformArray.push(`skewY(${transformProps.skewY})`);
- }
- return transformArray;
-}
-
-export function parseTransformProp(
- transform: TransformProps['transform'],
- props?: BaseProps
-) {
- const transformArray: string[] = [];
-
- props && transformArray.push(...stringifyTransformProps(props));
-
- if (Array.isArray(transform)) {
- if (typeof transform[0] === 'number') {
- transformArray.push(`matrix(${transform.join(' ')})`);
- } else {
- const stringifiedProps = transformsArrayToProps(
- transform as TransformsStyleArray
- );
- transformArray.push(...stringifyTransformProps(stringifiedProps));
- }
- } else if (typeof transform === 'string') {
- transformArray.push(transform);
- }
-
- return transformArray.length ? transformArray.join(' ') : undefined;
-}
-
export const getBoundingClientRect = (node: SVGElement) => {
if (node) {
const isElement = node.nodeType === 1; /* Node.ELEMENT_NODE */
@@ -79,39 +12,6 @@ export const getBoundingClientRect = (node: SVGElement) => {
throw new Error('Can not get boundingClientRect of ' + node || 'undefined');
};
-const measureLayout = (
- node: SVGElement,
- callback: (
- x: number,
- y: number,
- width: number,
- height: number,
- left: number,
- top: number
- ) => void
-) => {
- const relativeNode = node?.parentNode;
- if (relativeNode) {
- setTimeout(() => {
- // @ts-expect-error TODO: handle it better
- const relativeRect = getBoundingClientRect(relativeNode);
- const { height, left, top, width } = getBoundingClientRect(node);
- const x = left - relativeRect.left;
- const y = top - relativeRect.top;
- callback(x, y, width, height, left, top);
- }, 0);
- }
-};
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export function remeasure(this: any) {
- const tag = this.state.touchable.responderID;
- if (tag === null) {
- return;
- }
- measureLayout(tag, this._handleQueryLayout);
-}
-
/* Taken from here: https://gist.github.com/jennyknuth/222825e315d45a738ed9d6e04c7a88d0 */
export function encodeSvg(svgString: string) {
return svgString
diff --git a/src/web/utils/parseTransform.ts b/src/web/utils/parseTransform.ts
new file mode 100644
index 000000000..e9da9c40e
--- /dev/null
+++ b/src/web/utils/parseTransform.ts
@@ -0,0 +1,63 @@
+import type { TransformProps } from '../../lib/extract/types';
+import {
+ transformsArrayToProps,
+ TransformsStyleArray,
+} from '../../lib/extract/extractTransform';
+import type { BaseProps } from '../../types';
+
+export function parseTransformProp(
+ transform: TransformProps['transform'],
+ props?: BaseProps
+) {
+ const transformArray: string[] = [];
+
+ props && transformArray.push(...stringifyTransformProps(props));
+
+ if (Array.isArray(transform)) {
+ if (typeof transform[0] === 'number') {
+ transformArray.push(`matrix(${transform.join(' ')})`);
+ } else {
+ const stringifiedProps = transformsArrayToProps(
+ transform as TransformsStyleArray
+ );
+ transformArray.push(...stringifyTransformProps(stringifiedProps));
+ }
+ } else if (typeof transform === 'string') {
+ transformArray.push(transform);
+ }
+
+ return transformArray.length ? transformArray.join(' ') : undefined;
+}
+
+export function stringifyTransformProps(transformProps: TransformProps) {
+ const transformArray = [];
+ if (transformProps.translate != null) {
+ transformArray.push(`translate(${transformProps.translate})`);
+ }
+ if (transformProps.translateX != null || transformProps.translateY != null) {
+ transformArray.push(
+ `translate(${transformProps.translateX || 0}, ${
+ transformProps.translateY || 0
+ })`
+ );
+ }
+ if (transformProps.scale != null) {
+ transformArray.push(`scale(${transformProps.scale})`);
+ }
+ if (transformProps.scaleX != null || transformProps.scaleY != null) {
+ transformArray.push(
+ `scale(${transformProps.scaleX || 1}, ${transformProps.scaleY || 1})`
+ );
+ }
+ // rotation maps to rotate, not to collide with the text rotate attribute (which acts per glyph rather than block)
+ if (transformProps.rotation != null) {
+ transformArray.push(`rotate(${transformProps.rotation})`);
+ }
+ if (transformProps.skewX != null) {
+ transformArray.push(`skewX(${transformProps.skewX})`);
+ }
+ if (transformProps.skewY != null) {
+ transformArray.push(`skewY(${transformProps.skewY})`);
+ }
+ return transformArray;
+}
diff --git a/src/web/utils/prepare.ts b/src/web/utils/prepare.ts
index 0679b5513..c59b05589 100644
--- a/src/web/utils/prepare.ts
+++ b/src/web/utils/prepare.ts
@@ -1,26 +1,19 @@
-import {
- GestureResponderEvent,
- type ImageProps as RNImageProps,
-} from 'react-native';
-import { BaseProps } from '../types';
-import { WebShape } from '../WebShape';
-import { hasTouchableProperty, parseTransformProp } from '.';
+import type { NumberProp } from '../../lib/extract/types';
+import type { CreateComponentProps } from '../../types';
import { resolve } from '../../lib/resolve';
-import { NumberProp } from '../../lib/extract/types';
import { resolveAssetUri } from '../../lib/resolveAssetUri';
+import { parseTransformProp } from './parseTransform';
+
/**
* `react-native-svg` supports additional props that aren't defined in the spec.
* This function replaces them in a spec conforming manner.
- *
- * @param {WebShape} self Instance given to us.
- * @param {Object?} props Optional overridden props given to us.
- * @returns {Object} Cleaned props object.
- * @private
*/
-export const prepare = (
- self: WebShape,
- props = self.props
-) => {
+interface PreparedComponentProps extends CreateComponentProps {
+ 'transform-origin'?: string;
+ ref?: unknown;
+}
+
+export const prepare = (props: CreateComponentProps) => {
const {
transform,
origin,
@@ -34,48 +27,21 @@ export const prepare = (
forwardedRef,
gradientTransform,
patternTransform,
- onPress,
+ elementRef,
...rest
} = props;
- const clean: {
- onStartShouldSetResponder?: (e: GestureResponderEvent) => boolean;
- onResponderMove?: (e: GestureResponderEvent) => void;
- onResponderGrant?: (e: GestureResponderEvent) => void;
- onResponderRelease?: (e: GestureResponderEvent) => void;
- onResponderTerminate?: (e: GestureResponderEvent) => void;
- onResponderTerminationRequest?: (e: GestureResponderEvent) => boolean;
- onClick?: (e: GestureResponderEvent) => void;
- transform?: string;
- gradientTransform?: string;
- patternTransform?: string;
- 'transform-origin'?: string;
- href?: RNImageProps['source'] | string | null;
- style?: object;
- ref?: unknown;
- } = {
- ...(hasTouchableProperty(props)
- ? {
- onStartShouldSetResponder:
- self.touchableHandleStartShouldSetResponder,
- onResponderTerminationRequest:
- self.touchableHandleResponderTerminationRequest,
- onResponderGrant: self.touchableHandleResponderGrant,
- onResponderMove: self.touchableHandleResponderMove,
- onResponderRelease: self.touchableHandleResponderRelease,
- onResponderTerminate: self.touchableHandleResponderTerminate,
- }
- : null),
- ...rest,
- };
+ const clean: PreparedComponentProps = rest;
- if (origin != null) {
+ if (origin !== null && origin !== undefined) {
clean['transform-origin'] = origin.toString().replace(',', ' ');
- } else if (originX != null || originY != null) {
+ } else if (
+ (originX !== null && originX !== undefined) ||
+ (originY !== null && originY !== undefined)
+ ) {
clean['transform-origin'] = `${originX || 0} ${originY || 0}`;
}
- // we do it like this because setting transform as undefined causes error in web
const parsedTransform = parseTransformProp(transform, props);
if (parsedTransform) {
clean.transform = parsedTransform;
@@ -90,11 +56,13 @@ export const prepare = (
}
clean.ref = (el: SVGElement | null) => {
- self.elementRef.current = el;
+ if (elementRef) {
+ elementRef.current = el;
+ }
if (typeof forwardedRef === 'function') {
forwardedRef(el);
} else if (forwardedRef) {
- forwardedRef.current = el;
+ (forwardedRef as React.MutableRefObject).current = el;
}
};
@@ -105,24 +73,21 @@ export const prepare = (
fontWeight?: NumberProp;
} = {};
- if (fontFamily != null) {
+ if (fontFamily !== null && fontFamily !== undefined) {
styles.fontFamily = fontFamily;
}
- if (fontSize != null) {
+ if (fontSize !== null && fontSize !== undefined) {
styles.fontSize = fontSize;
}
- if (fontWeight != null) {
+ if (fontWeight !== null && fontWeight !== undefined) {
styles.fontWeight = fontWeight;
}
- if (fontStyle != null) {
+ if (fontStyle !== null && fontStyle !== undefined) {
styles.fontStyle = fontStyle;
}
- clean.style = resolve(style, styles);
- if (onPress !== null) {
- clean.onClick = props.onPress;
- }
if (props.href !== null && props.href !== undefined) {
clean.href = resolveAssetUri(props.href)?.uri;
}
+ clean.style = resolve(style, styles);
return clean;
};
diff --git a/src/web/utils/setNativeProps.ts b/src/web/utils/setNativeProps.ts
new file mode 100644
index 000000000..c1e3bbaef
--- /dev/null
+++ b/src/web/utils/setNativeProps.ts
@@ -0,0 +1,62 @@
+import { BaseProps, CreateComponentProps } from '../../types';
+import { camelCaseToDashed } from '.';
+import { convertInt32ColorToRGBA } from './convertInt32Color';
+
+export const setNativeProps = (
+ nativeProps: BaseProps,
+ elementRef: React.MutableRefObject,
+ lastMergedProps: React.MutableRefObject>
+) => {
+ const { style, ...rest } = nativeProps;
+ if (elementRef.current) {
+ const current = elementRef.current;
+
+ // Set attributes and styles
+ Object.keys(rest).forEach((cleanAttribute) => {
+ const cleanValue = rest[cleanAttribute as keyof typeof rest];
+ switch (cleanAttribute) {
+ case 'ref':
+ case 'children':
+ break;
+ case 'style':
+ Object.assign(current.style, cleanValue);
+ break;
+ case 'fill':
+ if (cleanValue && typeof cleanValue === 'object') {
+ current.setAttribute(
+ 'fill',
+ convertInt32ColorToRGBA(
+ (cleanValue as { payload: number }).payload
+ )
+ );
+ }
+ break;
+ case 'stroke':
+ if (cleanValue && typeof cleanValue === 'object') {
+ current.setAttribute(
+ 'stroke',
+ convertInt32ColorToRGBA(
+ (cleanValue as { payload: number }).payload
+ )
+ );
+ }
+ break;
+ default:
+ // apply all other incoming prop updates as attributes on the node
+ // same logic as in https://github.com/software-mansion/react-native-reanimated/blob/d04720c82f5941532991b235787285d36d717247/src/reanimated2/js-reanimated/index.ts#L38-L39
+ current.setAttribute(
+ camelCaseToDashed(cleanAttribute),
+ String(cleanValue)
+ );
+ break;
+ }
+ });
+
+ const mergedProps = { ...rest, style };
+ const mergedStyle = Array.isArray(style)
+ ? Object.assign({}, ...style)
+ : style;
+ const merged = { ...mergedProps, style: mergedStyle };
+ lastMergedProps.current = merged;
+ }
+};
diff --git a/src/web/webUtils.web.ts b/src/web/webUtils.web.ts
new file mode 100644
index 000000000..4f6f26f89
--- /dev/null
+++ b/src/web/webUtils.web.ts
@@ -0,0 +1,37 @@
+'use strict';
+
+import type {
+ EventHandlers,
+ PressResponderConfig,
+ ResponderConfig,
+} from './hooks/types';
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export let usePressEvents: (
+ hostRef: any,
+ config: PressResponderConfig
+) => EventHandlers;
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export let useResponderEvents: (hostRef: any, config: ResponderConfig) => void;
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export let useMergeRefs: (
+ ...args: ReadonlyArray>
+) => void;
+
+try {
+ usePressEvents =
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ require('react-native-web/dist/modules/usePressEvents').default;
+} catch (e) {}
+
+try {
+ useResponderEvents =
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ require('react-native-web/dist/modules/useResponderEvents').default;
+} catch (e) {}
+
+try {
+ useMergeRefs =
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ require('react-native-web/dist/modules/useMergeRefs').default;
+} catch (e) {}
diff --git a/yarn.lock b/yarn.lock
index b10922132..569243a5b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1459,7 +1459,7 @@
resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310"
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
-"@babel/runtime@^7.0.0", "@babel/runtime@^7.8.4":
+"@babel/runtime@^7.0.0", "@babel/runtime@^7.18.6", "@babel/runtime@^7.8.4":
version "7.25.6"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.6.tgz#9afc3289f7184d8d7f98b099884c26317b9264d2"
integrity sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==
@@ -2541,6 +2541,11 @@
resolved "https://registry.yarnpkg.com/@react-native/normalize-colors/-/normalize-colors-0.72.0.tgz#14294b7ed3c1d92176d2a00df48456e8d7d62212"
integrity sha512-285lfdqSXaqKuBbbtP9qL2tDrfxdOFtIMvkKadtleRQkdOxx+uzGvFr82KHmc/sSiMtfXGp7JnFYWVh4sFl7Yw==
+"@react-native/normalize-colors@^0.74.1":
+ version "0.74.87"
+ resolved "https://registry.yarnpkg.com/@react-native/normalize-colors/-/normalize-colors-0.74.87.tgz#a814169d0ce4ce13ffebcda0a3a5a3f780ccd772"
+ integrity sha512-Xh7Nyk/MPefkb0Itl5Z+3oOobeG9lfLb7ZOY2DKpFnoCE1TzBmib9vMNdFaLdSxLIP+Ec6icgKtdzYg8QUPYzA==
+
"@react-native/virtualized-lists@^0.72.8":
version "0.72.8"
resolved "https://registry.yarnpkg.com/@react-native/virtualized-lists/-/virtualized-lists-0.72.8.tgz#a2c6a91ea0f1d40eb5a122fb063daedb92ed1dc3"
@@ -3241,7 +3246,7 @@ arraybuffer.prototype.slice@^1.0.3:
is-array-buffer "^3.0.4"
is-shared-array-buffer "^1.0.2"
-asap@~2.0.6:
+asap@~2.0.3, asap@~2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==
@@ -4094,6 +4099,13 @@ create-require@^1.1.0:
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
+cross-fetch@^3.1.5:
+ version "3.1.8"
+ resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82"
+ integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==
+ dependencies:
+ node-fetch "^2.6.12"
+
cross-spawn@^6.0.0:
version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
@@ -4119,6 +4131,13 @@ crypto-random-string@^2.0.0:
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==
+css-in-js-utils@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz#640ae6a33646d401fc720c54fc61c42cd76ae2bb"
+ integrity sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==
+ dependencies:
+ hyphenate-style-name "^1.0.3"
+
css-select@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6"
@@ -5184,6 +5203,11 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
+fast-loops@^1.1.3:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/fast-loops/-/fast-loops-1.1.4.tgz#61bc77d518c0af5073a638c6d9d5c7683f069ce2"
+ integrity sha512-8dbd3XWoKCTms18ize6JmQF1SFnnfj5s0B7rRry22EofgMu7B6LKHVh+XfFqFGsqnbH54xgeO83PzpKI+ODhlg==
+
fast-xml-parser@^4.0.12:
version "4.5.0"
resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz#2882b7d01a6825dfdf909638f2de0256351def37"
@@ -5205,6 +5229,24 @@ fb-watchman@^2.0.0:
dependencies:
bser "2.1.1"
+fbjs-css-vars@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz#216551136ae02fe255932c3ec8775f18e2c078b8"
+ integrity sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==
+
+fbjs@^3.0.4:
+ version "3.0.5"
+ resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-3.0.5.tgz#aa0edb7d5caa6340011790bd9249dbef8a81128d"
+ integrity sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==
+ dependencies:
+ cross-fetch "^3.1.5"
+ fbjs-css-vars "^1.0.0"
+ loose-envify "^1.0.0"
+ object-assign "^4.1.0"
+ promise "^7.1.1"
+ setimmediate "^1.0.5"
+ ua-parser-js "^1.0.35"
+
fd-slicer@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
@@ -5825,6 +5867,11 @@ husky@^8.0.1:
resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.3.tgz#4936d7212e46d1dea28fef29bb3a108872cd9184"
integrity sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==
+hyphenate-style-name@^1.0.3:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz#1797bf50369588b47b72ca6d5e65374607cf4436"
+ integrity sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==
+
iconv-lite@0.4.24, iconv-lite@^0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@@ -5940,6 +5987,14 @@ ini@~1.3.0:
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
+inline-style-prefixer@^6.0.1:
+ version "6.0.4"
+ resolved "https://registry.yarnpkg.com/inline-style-prefixer/-/inline-style-prefixer-6.0.4.tgz#4290ed453ab0e4441583284ad86e41ad88384f44"
+ integrity sha512-FwXmZC2zbeeS7NzGjJ6pAiqRhXR0ugUShSNb6GApMl6da0/XGc4MOJsoWAywia52EEWbXNSy0pzkwz/+Y+swSg==
+ dependencies:
+ css-in-js-utils "^3.1.0"
+ fast-loops "^1.1.3"
+
inquirer@8.2.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.0.tgz#f44f008dd344bbfc4b30031f45d984e034a3ac3a"
@@ -7401,6 +7456,11 @@ memoize-one@^5.0.0:
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
+memoize-one@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045"
+ integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==
+
merge-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
@@ -7872,7 +7932,7 @@ node-dir@^0.1.17:
dependencies:
minimatch "^3.0.2"
-node-fetch@^2.2.0, node-fetch@^2.6.0, node-fetch@^2.6.7:
+node-fetch@^2.2.0, node-fetch@^2.6.0, node-fetch@^2.6.12, node-fetch@^2.6.7:
version "2.7.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
@@ -7947,7 +8007,7 @@ ob1@0.76.9:
resolved "https://registry.yarnpkg.com/ob1/-/ob1-0.76.9.tgz#a493e4b83a0fb39200de639804b5d06eed5599dc"
integrity sha512-g0I/OLnSxf6OrN3QjSew3bTDJCdbZoWxnh8adh1z36alwCuGF1dgDeRA25bTYSakrG5WULSaWJPOdgnf1O/oQw==
-object-assign@^4.1.1:
+object-assign@^4.1.0, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
@@ -8473,6 +8533,11 @@ possible-typed-array-names@^1.0.0:
resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f"
integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==
+postcss-value-parser@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
+ integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
+
prelude-ls@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
@@ -8560,6 +8625,13 @@ promise.allsettled@1.0.5:
get-intrinsic "^1.1.1"
iterate-value "^1.0.2"
+promise@^7.1.1:
+ version "7.3.1"
+ resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
+ integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==
+ dependencies:
+ asap "~2.0.3"
+
promise@^8.3.0:
version "8.3.0"
resolved "https://registry.yarnpkg.com/promise/-/promise-8.3.0.tgz#8cb333d1edeb61ef23869fbb8a4ea0279ab60e0a"
@@ -8777,6 +8849,20 @@ react-native-builder-bob@^0.20.4:
optionalDependencies:
jetifier "^2.0.0"
+react-native-web@^0.19.12:
+ version "0.19.13"
+ resolved "https://registry.yarnpkg.com/react-native-web/-/react-native-web-0.19.13.tgz#2d84849bf0251ec0e3a8072fda7f9a7c29375331"
+ integrity sha512-etv3bN8rJglrRCp/uL4p7l8QvUNUC++QwDbdZ8CB7BvZiMvsxfFIRM1j04vxNldG3uo2puRd6OSWR3ibtmc29A==
+ dependencies:
+ "@babel/runtime" "^7.18.6"
+ "@react-native/normalize-colors" "^0.74.1"
+ fbjs "^3.0.4"
+ inline-style-prefixer "^6.0.1"
+ memoize-one "^6.0.0"
+ nullthrows "^1.1.1"
+ postcss-value-parser "^4.2.0"
+ styleq "^0.1.3"
+
react-native-windows@^0.72.4:
version "0.72.38"
resolved "https://registry.yarnpkg.com/react-native-windows/-/react-native-windows-0.72.38.tgz#5585d6d4e52055d2125dabba9d09c1cd1dc9e863"
@@ -9375,6 +9461,11 @@ set-function-name@^2.0.1, set-function-name@^2.0.2:
functions-have-names "^1.2.3"
has-property-descriptors "^1.0.2"
+setimmediate@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
+ integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==
+
setprototypeof@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
@@ -9786,6 +9877,11 @@ strnum@^1.0.5:
resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db"
integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==
+styleq@^0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/styleq/-/styleq-0.1.3.tgz#8efb2892debd51ce7b31dc09c227ad920decab71"
+ integrity sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA==
+
sudo-prompt@^9.0.0:
version "9.2.1"
resolved "https://registry.yarnpkg.com/sudo-prompt/-/sudo-prompt-9.2.1.tgz#77efb84309c9ca489527a4e749f287e6bdd52afd"
@@ -10094,6 +10190,11 @@ typescript@^5.1.6:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.2.tgz#d1de67b6bef77c41823f822df8f0b3bcff60a5a0"
integrity sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==
+ua-parser-js@^1.0.35:
+ version "1.0.39"
+ resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.39.tgz#bfc07f361549bf249bd8f4589a4cccec18fd2018"
+ integrity sha512-k24RCVWlEcjkdOxYmVJgeD/0a1TiSpqLg+ZalVGV9lsnr4yqu0w7tX/x2xX6G4zpkgQnRf89lxuZ1wsbjXM8lw==
+
uglify-es@^3.1.9:
version "3.3.9"
resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"