@ray-js/ipc-player-integration 0.0.1-beta-60 → 0.0.1-beta-62
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.
- package/lib/ctx/ctx.composition.js +2 -2
- package/lib/ports.output.js +1 -1
- package/lib/ui/ui.d.ts +1 -0
- package/lib/ui/ui.js +81 -23
- package/lib/utils/device/index.js +2 -28
- package/lib/widgets/fullScreen/fullTravelRouteControl.js +5 -11
- package/lib/widgets/ptz/ptzControl.js +6 -6
- package/lib/widgets/recordVideo/recordVideo.js +2 -2
- package/lib/widgets/screenshot/screenshot.js +2 -2
- package/lib/widgets/videoBitKBP/videoBitKBP.js +2 -2
- package/package.json +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
|
|
2
2
|
import _objectWithoutProperties from "@babel/runtime/helpers/esm/objectWithoutProperties";
|
|
3
3
|
const _excluded = ["title", "duration"];
|
|
4
|
-
import
|
|
4
|
+
import { getCameraConfigInfo } from '@ray-js/ray-ipc-utils';
|
|
5
5
|
import { IntercomMode, MuteMode, ClarityType } from '@ray-js/ray-ipc-utils/lib/interface';
|
|
6
6
|
import { createUseCtx } from './ctx';
|
|
7
7
|
import { FullSmallIntercom, VerticalSmallIntercom, BatteryFull, Battery, Screenshot, TempHumidity, RecordVideo, FullScreen, VideoBitKBP, Muted, Resolution, Ptz } from '../widgets';
|
|
@@ -72,7 +72,7 @@ const getMemoryState = devId => {
|
|
|
72
72
|
isIntercomSupported: false
|
|
73
73
|
};
|
|
74
74
|
return new Promise(resolve => {
|
|
75
|
-
|
|
75
|
+
getCameraConfigInfo(devId).then(res => {
|
|
76
76
|
if (res.code === -1) {
|
|
77
77
|
return resolve(defaultValue);
|
|
78
78
|
}
|
package/lib/ports.output.js
CHANGED
package/lib/ui/ui.d.ts
CHANGED
package/lib/ui/ui.js
CHANGED
|
@@ -3,7 +3,7 @@ import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
|
|
|
3
3
|
import "core-js/modules/esnext.iterator.constructor.js";
|
|
4
4
|
import "core-js/modules/esnext.iterator.map.js";
|
|
5
5
|
import React, { useContext, useState, useRef, useMemo, useEffect, useImperativeHandle } from 'react';
|
|
6
|
-
import { View, CoverView, getSystemInfoSync, usePageEvent } from '@ray-js/ray';
|
|
6
|
+
import { View, CoverView, getSystemInfoSync, usePageEvent, setNavigationBarBack, setPageOrientation } from '@ray-js/ray';
|
|
7
7
|
import clsx from 'clsx';
|
|
8
8
|
import IPCPlayer from '@ray-js/ray-ipc-player';
|
|
9
9
|
import { PlayState, PlayerStreamStatus } from '../interface';
|
|
@@ -51,6 +51,7 @@ export const IPCPlayerIntegration = /*#__PURE__*/React.memo(props => {
|
|
|
51
51
|
brandColor = '#FF592A',
|
|
52
52
|
verticalMic = true,
|
|
53
53
|
eventRef: eventRefProp,
|
|
54
|
+
landscapeMode = 'standard',
|
|
54
55
|
onPlayerTap,
|
|
55
56
|
extend = {}
|
|
56
57
|
} = props;
|
|
@@ -73,7 +74,6 @@ export const IPCPlayerIntegration = /*#__PURE__*/React.memo(props => {
|
|
|
73
74
|
这里用一个统一的状态标志期望控件是否展示
|
|
74
75
|
*/
|
|
75
76
|
const [componentHideState, setComponentHideState] = useState(false);
|
|
76
|
-
const [showRightContent, setShowRightContent] = useState(true);
|
|
77
77
|
prevTriggerEvent.current = componentHideState ? hideAllComponent : showAllComponent;
|
|
78
78
|
const eventRef = useRef(instance.event);
|
|
79
79
|
if (eventRef.current !== instance.event) {
|
|
@@ -81,15 +81,16 @@ export const IPCPlayerIntegration = /*#__PURE__*/React.memo(props => {
|
|
|
81
81
|
}
|
|
82
82
|
useImperativeHandle(eventRefProp, () => eventRef.current, [eventRef.current]);
|
|
83
83
|
const timer = useRef();
|
|
84
|
+
const [scaleMultiple, setScaleMultiple] = useState(1);
|
|
85
|
+
const [currentZoomLevel, setCurrentZoomLevel] = useState(1);
|
|
84
86
|
useEffect(() => {
|
|
85
87
|
setBrandColor(brandColor);
|
|
86
88
|
setVerticalMic(verticalMic);
|
|
87
89
|
}, []);
|
|
88
|
-
const refreshBottomLeft =
|
|
90
|
+
const refreshBottomLeft = () => {
|
|
89
91
|
event.current.emit(startTimeToHideAllComponent);
|
|
90
92
|
event.current.emit(showAllComponent);
|
|
91
93
|
event.current.emit(playerTap);
|
|
92
|
-
setShowRightContent(value);
|
|
93
94
|
};
|
|
94
95
|
|
|
95
96
|
/**
|
|
@@ -106,11 +107,21 @@ export const IPCPlayerIntegration = /*#__PURE__*/React.memo(props => {
|
|
|
106
107
|
deviceType
|
|
107
108
|
} = systemInfo.current;
|
|
108
109
|
// 针对pad 暂不支持横屏,且在ios pad模式下 会触发onResize事件, 待解决
|
|
110
|
+
|
|
109
111
|
if (deviceType === 'pad') {
|
|
110
112
|
setScreenType('vertical');
|
|
111
113
|
} else {
|
|
112
114
|
setScreenType(type === 'landscape' ? 'full' : 'vertical');
|
|
113
115
|
}
|
|
116
|
+
|
|
117
|
+
// 若为全屏模式并且要求按宽填充,即横屏时充满,主动设置模式为-1即可
|
|
118
|
+
if (type === 'landscape' && landscapeMode === 'fill') {
|
|
119
|
+
console.log('横屏时充满');
|
|
120
|
+
setScaleMultiple(-1);
|
|
121
|
+
} else {
|
|
122
|
+
// 将屏幕播放比例设为1
|
|
123
|
+
setScaleMultiple(1);
|
|
124
|
+
}
|
|
114
125
|
} catch (err) {
|
|
115
126
|
console.log(err, 'err');
|
|
116
127
|
}
|
|
@@ -141,6 +152,43 @@ export const IPCPlayerIntegration = /*#__PURE__*/React.memo(props => {
|
|
|
141
152
|
mute: instance.mute
|
|
142
153
|
});
|
|
143
154
|
|
|
155
|
+
// 监听物理返回按键
|
|
156
|
+
const hackHandle = () => {
|
|
157
|
+
setPageOrientation({
|
|
158
|
+
pageOrientation: 'portrait',
|
|
159
|
+
success: () => {
|
|
160
|
+
console.log('set success');
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
setScreenType('vertical');
|
|
164
|
+
};
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
// 针对安卓使用系统按键导航
|
|
167
|
+
if (screenType === 'full') {
|
|
168
|
+
const {
|
|
169
|
+
platform
|
|
170
|
+
} = getSystemInfoSync();
|
|
171
|
+
if (platform !== 'ios') {
|
|
172
|
+
setNavigationBarBack({
|
|
173
|
+
type: 'custom'
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
setNavigationBarBack({
|
|
178
|
+
type: 'system'
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}, [screenType]);
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
ty.onNavigationBarBack(hackHandle);
|
|
184
|
+
return () => {
|
|
185
|
+
ty.offNavigationBarBack(hackHandle);
|
|
186
|
+
setNavigationBarBack({
|
|
187
|
+
type: 'system'
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
}, []);
|
|
191
|
+
|
|
144
192
|
/**
|
|
145
193
|
* 渲染随机定位播放器内容
|
|
146
194
|
*/
|
|
@@ -206,7 +254,25 @@ export const IPCPlayerIntegration = /*#__PURE__*/React.memo(props => {
|
|
|
206
254
|
/**
|
|
207
255
|
* 视频流加载状态封装
|
|
208
256
|
*/
|
|
209
|
-
|
|
257
|
+
const onChangeStreamStatus = useMemoizedFn(code => {
|
|
258
|
+
var _props$onPlayStatus;
|
|
259
|
+
const playStateMap = {
|
|
260
|
+
[PlayerStreamStatus.PreviewSuccess]: PlayState.PLAYING,
|
|
261
|
+
// 播放中
|
|
262
|
+
[PlayerStreamStatus.PAUSE]: PlayState.PAUSE // 暂停
|
|
263
|
+
};
|
|
264
|
+
// 合并为三种状态 连接中 暂停 播放中
|
|
265
|
+
const playState = playStateMap[code] || PlayState.CONNECTING;
|
|
266
|
+
if (playState !== PlayState.PLAYING) {
|
|
267
|
+
setScaleMultiple(currentZoomLevel);
|
|
268
|
+
}
|
|
269
|
+
setPlayState(playState);
|
|
270
|
+
(_props$onPlayStatus = props.onPlayStatus) === null || _props$onPlayStatus === void 0 || _props$onPlayStatus.call(props, {
|
|
271
|
+
playState,
|
|
272
|
+
playCode: code
|
|
273
|
+
});
|
|
274
|
+
instance.changeStreamStatus(code);
|
|
275
|
+
});
|
|
210
276
|
const disablePlayerTap = useRef(false);
|
|
211
277
|
const handDisablePlayerTap = value => {
|
|
212
278
|
disablePlayerTap.current = !!value;
|
|
@@ -277,29 +343,21 @@ export const IPCPlayerIntegration = /*#__PURE__*/React.memo(props => {
|
|
|
277
343
|
devId: devId,
|
|
278
344
|
onlineStatus: deviceOnline,
|
|
279
345
|
ipcPlayerContext: instance.IPCPlayerInstance,
|
|
280
|
-
onChangeStreamStatus:
|
|
281
|
-
|
|
282
|
-
const playStateMap = {
|
|
283
|
-
[PlayerStreamStatus.PreviewSuccess]: PlayState.PLAYING,
|
|
284
|
-
// 播放中
|
|
285
|
-
[PlayerStreamStatus.PAUSE]: PlayState.PAUSE // 暂停
|
|
286
|
-
};
|
|
287
|
-
// 合并为三种状态 连接中 暂停 播放中
|
|
288
|
-
const playState = playStateMap[code] || PlayState.CONNECTING;
|
|
289
|
-
setPlayState(playState);
|
|
290
|
-
(_props$onPlayStatus = props.onPlayStatus) === null || _props$onPlayStatus === void 0 || _props$onPlayStatus.call(props, {
|
|
291
|
-
playState,
|
|
292
|
-
playCode: code
|
|
293
|
-
});
|
|
294
|
-
instance.changeStreamStatus(code);
|
|
295
|
-
}
|
|
296
|
-
// onCtx={getIpcPlayer}
|
|
346
|
+
onChangeStreamStatus: onChangeStreamStatus
|
|
347
|
+
// onCtx={getIpcPlayerCtx}
|
|
297
348
|
// onPlayerTap={handlePlayerClick}
|
|
298
349
|
,
|
|
299
350
|
onCameraNotifyWeakNetwork: data => {
|
|
300
351
|
console.log(`onCameraNotifyWeakNetwork: ${JSON.stringify(data)}`);
|
|
301
352
|
},
|
|
302
|
-
clarity: decodeClarityDic[resolution]
|
|
353
|
+
clarity: decodeClarityDic[resolution],
|
|
354
|
+
onZoomChange: data => {
|
|
355
|
+
const {
|
|
356
|
+
zoomLevel
|
|
357
|
+
} = data === null || data === void 0 ? void 0 : data.detail;
|
|
358
|
+
setCurrentZoomLevel(zoomLevel);
|
|
359
|
+
},
|
|
360
|
+
scaleMultiple: scaleMultiple
|
|
303
361
|
// 安卓横屏问题
|
|
304
362
|
// ptzControllable={screenType === 'vertical'}
|
|
305
363
|
,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "core-js/modules/esnext.iterator.constructor.js";
|
|
2
2
|
import "core-js/modules/esnext.iterator.find.js";
|
|
3
3
|
import "core-js/modules/esnext.iterator.for-each.js";
|
|
4
|
-
import
|
|
4
|
+
import { getDevInfo } from '@ray-js/ray-ipc-utils';
|
|
5
5
|
export function getDpValue(options) {
|
|
6
6
|
return new Promise((resolve, reject) => {
|
|
7
7
|
ty.device.getDeviceInfo({
|
|
@@ -90,32 +90,6 @@ export const showMathPowValue = (value, scale) => {
|
|
|
90
90
|
}
|
|
91
91
|
return v;
|
|
92
92
|
};
|
|
93
|
-
export const getDevInfo = deviceId => {
|
|
94
|
-
return new Promise(resolve => {
|
|
95
|
-
try {
|
|
96
|
-
ty.device.getDeviceInfo({
|
|
97
|
-
deviceId,
|
|
98
|
-
success: res => {
|
|
99
|
-
resolve({
|
|
100
|
-
code: 0,
|
|
101
|
-
data: res
|
|
102
|
-
});
|
|
103
|
-
},
|
|
104
|
-
fail: err => {
|
|
105
|
-
resolve({
|
|
106
|
-
code: -1,
|
|
107
|
-
msg: err
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
} catch (err) {
|
|
112
|
-
resolve({
|
|
113
|
-
code: -1,
|
|
114
|
-
msg: String(err)
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
};
|
|
119
93
|
|
|
120
94
|
/**
|
|
121
95
|
* 根据DpCode获取枚举型DP的range是否包含对应项
|
|
@@ -123,7 +97,7 @@ export const getDevInfo = deviceId => {
|
|
|
123
97
|
*/
|
|
124
98
|
|
|
125
99
|
export const getEnumRangeIsValid = async (devId, dpCode, rangValue) => {
|
|
126
|
-
const infoData = await
|
|
100
|
+
const infoData = await getDevInfo(devId);
|
|
127
101
|
if (infoData.code === 0) {
|
|
128
102
|
var _targetSchema$propert;
|
|
129
103
|
const {
|
|
@@ -29,13 +29,11 @@ export function FullTravelRouteControl(props) {
|
|
|
29
29
|
brandColor: props.brandColor
|
|
30
30
|
});
|
|
31
31
|
const timerRef = useRef();
|
|
32
|
-
const [touching, setTouching] = useState(false);
|
|
33
32
|
const [state, _, sendDp] = useDpState({
|
|
34
33
|
devId: props.devId,
|
|
35
34
|
dpCodes: [DIRECTION_CONTROL_DP_CODE],
|
|
36
35
|
listenDpChange: true
|
|
37
36
|
});
|
|
38
|
-
console.log('touching', touching);
|
|
39
37
|
const onPtzControlShow = useMemoizedFn(() => {
|
|
40
38
|
setIsPtzActive(true);
|
|
41
39
|
});
|
|
@@ -75,28 +73,24 @@ export function FullTravelRouteControl(props) {
|
|
|
75
73
|
return /*#__PURE__*/React.createElement(View, {
|
|
76
74
|
className: clsx('ipc-player-plugin-full-screen-voice', {
|
|
77
75
|
'ipc-player-plugin-full-screen-voice-hide': shouldHide || isPtzActive
|
|
78
|
-
})
|
|
79
|
-
style: {
|
|
80
|
-
width: touching ? '100vw' : 'auto',
|
|
81
|
-
height: touching ? '100vh' : 'auto',
|
|
82
|
-
backgroundColor: 'transparent'
|
|
83
|
-
}
|
|
76
|
+
})
|
|
84
77
|
}, /*#__PURE__*/React.createElement(RectDirectionControl, {
|
|
85
78
|
className: "ipc-plugin-full-travel-route-control",
|
|
86
79
|
value: 0,
|
|
87
80
|
onTouchStart: value => {
|
|
88
|
-
|
|
81
|
+
pauseTimeToHideAllComponentFn();
|
|
89
82
|
sendDpValue(String(value));
|
|
83
|
+
ty.nativeDisabled(true);
|
|
90
84
|
},
|
|
91
85
|
onTouchMove: value => {
|
|
92
|
-
setTouching(true);
|
|
93
86
|
pauseTimeToHideAllComponentFn();
|
|
94
87
|
sendDpValue(String(value));
|
|
88
|
+
ty.nativeDisabled(true);
|
|
95
89
|
},
|
|
96
90
|
onTouchEnd: () => {
|
|
97
91
|
event.emit(startTimeToHideAllComponent);
|
|
98
|
-
setTouching(false);
|
|
99
92
|
sendDpValue('-1');
|
|
93
|
+
ty.nativeDisabled(false);
|
|
100
94
|
},
|
|
101
95
|
onMoveNonIntersection: () => {
|
|
102
96
|
sendDpValue('-1');
|
|
@@ -5,7 +5,7 @@ import clsx from 'clsx';
|
|
|
5
5
|
import _find from 'lodash/find';
|
|
6
6
|
import _get from 'lodash/get';
|
|
7
7
|
import IpcPtzZoom from '@ray-js/ipc-ptz-zoom';
|
|
8
|
-
import
|
|
8
|
+
import { getDpIdByCode, publishDps } from '@ray-js/ray-ipc-utils';
|
|
9
9
|
import { useMemoizedFn } from '../../hooks';
|
|
10
10
|
import { useComponentHideState } from '../../ui/hooks';
|
|
11
11
|
import { UIEventContext } from '../../ui/context';
|
|
@@ -106,17 +106,17 @@ export const PtzControl = props => {
|
|
|
106
106
|
const {
|
|
107
107
|
type
|
|
108
108
|
} = data;
|
|
109
|
-
const dpData = await
|
|
109
|
+
const dpData = await getDpIdByCode(devId, 'ptz_control');
|
|
110
110
|
if (dpData.code === 0) {
|
|
111
111
|
const ptzControlId = dpData.data;
|
|
112
112
|
const sndDpValue = _get(_find(ptzData.current, {
|
|
113
113
|
type
|
|
114
114
|
}), 'dpValue', null);
|
|
115
|
-
|
|
115
|
+
publishDps(devId, {
|
|
116
116
|
[ptzControlId]: sndDpValue
|
|
117
117
|
});
|
|
118
118
|
ptzTimeId.current = setInterval(() => {
|
|
119
|
-
|
|
119
|
+
publishDps(devId, {
|
|
120
120
|
[ptzControlId]: sndDpValue
|
|
121
121
|
});
|
|
122
122
|
}, 1000);
|
|
@@ -133,10 +133,10 @@ export const PtzControl = props => {
|
|
|
133
133
|
}
|
|
134
134
|
},
|
|
135
135
|
onTouchPtzEnd: async () => {
|
|
136
|
-
const ptzStopData = await
|
|
136
|
+
const ptzStopData = await getDpIdByCode(devId, 'ptz_stop');
|
|
137
137
|
if (ptzStopData.code === 0) {
|
|
138
138
|
const ptzStopId = ptzStopData.data;
|
|
139
|
-
|
|
139
|
+
publishDps(devId, {
|
|
140
140
|
[ptzStopId]: true
|
|
141
141
|
});
|
|
142
142
|
}
|
|
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef, useContext } from 'react';
|
|
|
2
2
|
import { View, Text, Image } from '@ray-js/ray';
|
|
3
3
|
import { useSetState, useUpdateEffect } from 'ahooks';
|
|
4
4
|
import clsx from 'clsx';
|
|
5
|
-
import
|
|
5
|
+
import { goToIpcPageNativeRoute } from '@ray-js/ray-ipc-utils';
|
|
6
6
|
import Strings from '../../i18n';
|
|
7
7
|
import { PlayState } from '../../interface';
|
|
8
8
|
import { UIEventContext } from '../../ui/context';
|
|
@@ -167,7 +167,7 @@ export function RecordVideo(props) {
|
|
|
167
167
|
});
|
|
168
168
|
deleteContent('absolute', RECORD_VIDEO_SUCCESS_TOAST_ID);
|
|
169
169
|
clearInterval(timer.current);
|
|
170
|
-
|
|
170
|
+
goToIpcPageNativeRoute('ipc_album_panel', devId);
|
|
171
171
|
};
|
|
172
172
|
|
|
173
173
|
/** 添加录制成功弹窗 */
|
|
@@ -2,7 +2,7 @@ import React, { useContext, useRef } from 'react';
|
|
|
2
2
|
import { View, Image, Text, showModal, openAppSystemSettingPage } from '@ray-js/ray';
|
|
3
3
|
import { useSetState, useUpdateEffect, useMemoizedFn } from 'ahooks';
|
|
4
4
|
import clsx from 'clsx';
|
|
5
|
-
import
|
|
5
|
+
import { goToIpcPageNativeRoute } from '@ray-js/ray-ipc-utils';
|
|
6
6
|
import Strings from '../../i18n';
|
|
7
7
|
import { useStore } from '../../ctx/store';
|
|
8
8
|
import { UIEventContext } from '../../ui/context';
|
|
@@ -61,7 +61,7 @@ export function Screenshot(props) {
|
|
|
61
61
|
});
|
|
62
62
|
deleteContent('absolute', 'plugin-screenshot-toast');
|
|
63
63
|
clearInterval(timer.current);
|
|
64
|
-
|
|
64
|
+
goToIpcPageNativeRoute('ipc_album_panel', devId);
|
|
65
65
|
};
|
|
66
66
|
|
|
67
67
|
/** 添加截屏成功弹窗 */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useEffect, useState } from 'react';
|
|
2
2
|
import { View } from '@ray-js/ray';
|
|
3
3
|
import clsx from 'clsx';
|
|
4
|
-
import
|
|
4
|
+
import { getVideoBitrateKbps } from '@ray-js/ray-ipc-utils';
|
|
5
5
|
import { PlayState } from '../../interface';
|
|
6
6
|
import './videoBitKBP.less';
|
|
7
7
|
import { useStore } from '../../ctx/store';
|
|
@@ -30,7 +30,7 @@ export const VideoBitKBP = props => {
|
|
|
30
30
|
};
|
|
31
31
|
}, [store.playState]);
|
|
32
32
|
const init = async () => {
|
|
33
|
-
const res = await
|
|
33
|
+
const res = await getVideoBitrateKbps(devId);
|
|
34
34
|
if (res.code !== -1) {
|
|
35
35
|
setBitKBP(`${res.data.kbps}KB/S`);
|
|
36
36
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ray-js/ipc-player-integration",
|
|
3
|
-
"version": "0.0.1-beta-
|
|
3
|
+
"version": "0.0.1-beta-62",
|
|
4
4
|
"description": "IPC 播放器功能集成",
|
|
5
5
|
"main": "lib/index",
|
|
6
6
|
"files": [
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"@ray-js/ipc-ptz-zoom": "0.0.2-beta-7",
|
|
40
40
|
"@ray-js/panel-sdk": "^1.13.1",
|
|
41
41
|
"@ray-js/ray-ipc-player": "2.0.20-beta-13",
|
|
42
|
-
"@ray-js/ray-ipc-utils": "1.1.0-beta-
|
|
42
|
+
"@ray-js/ray-ipc-utils": "1.1.0-beta-17",
|
|
43
43
|
"@ray-js/svg": "0.2.0",
|
|
44
44
|
"clsx": "^1.2.1",
|
|
45
45
|
"jotai": "^2.10.2"
|