@midscene/playground-app 1.7.3 → 1.7.4

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 (50) hide show
  1. package/dist/es/PlaygroundApp.css +0 -128
  2. package/dist/es/PlaygroundApp.mjs +74 -464
  3. package/dist/es/PlaygroundThemeProvider.mjs +10 -0
  4. package/dist/es/PreviewRenderer.mjs +4 -1
  5. package/dist/es/ScrcpyPanel.mjs +97 -89
  6. package/dist/es/SessionSetupPanel.css +292 -0
  7. package/dist/es/SessionSetupPanel.mjs +60 -39
  8. package/dist/es/controller/ai-config.mjs +37 -0
  9. package/dist/es/controller/auto-create.mjs +19 -0
  10. package/dist/es/controller/selectors.mjs +66 -0
  11. package/dist/es/controller/types.mjs +0 -0
  12. package/dist/es/controller/usePlaygroundController.mjs +356 -0
  13. package/dist/es/icons/dropdown-chevron.mjs +61 -0
  14. package/dist/es/icons/midscene-logo.mjs +247 -0
  15. package/dist/es/index.mjs +4 -1
  16. package/dist/es/panels/PlaygroundConversationPanel.css +20 -0
  17. package/dist/es/panels/PlaygroundConversationPanel.mjs +134 -0
  18. package/dist/es/scrcpy-preview.mjs +30 -0
  19. package/dist/lib/PlaygroundApp.css +0 -128
  20. package/dist/lib/PlaygroundApp.js +70 -460
  21. package/dist/lib/PlaygroundThemeProvider.js +44 -0
  22. package/dist/lib/PreviewRenderer.js +4 -1
  23. package/dist/lib/ScrcpyPanel.js +96 -88
  24. package/dist/lib/SessionSetupPanel.css +292 -0
  25. package/dist/lib/SessionSetupPanel.js +70 -38
  26. package/dist/lib/controller/ai-config.js +74 -0
  27. package/dist/lib/controller/auto-create.js +59 -0
  28. package/dist/lib/controller/selectors.js +103 -0
  29. package/dist/lib/controller/types.js +18 -0
  30. package/dist/lib/controller/usePlaygroundController.js +390 -0
  31. package/dist/lib/icons/dropdown-chevron.js +95 -0
  32. package/dist/lib/icons/midscene-logo.js +281 -0
  33. package/dist/lib/index.js +14 -2
  34. package/dist/lib/panels/PlaygroundConversationPanel.css +20 -0
  35. package/dist/lib/panels/PlaygroundConversationPanel.js +168 -0
  36. package/dist/lib/scrcpy-preview.js +79 -0
  37. package/dist/types/PlaygroundPreview.d.ts +6 -0
  38. package/dist/types/PlaygroundThemeProvider.d.ts +2 -0
  39. package/dist/types/PreviewRenderer.d.ts +7 -1
  40. package/dist/types/ScrcpyPanel.d.ts +14 -1
  41. package/dist/types/SessionSetupPanel.d.ts +4 -3
  42. package/dist/types/controller/ai-config.d.ts +4 -0
  43. package/dist/types/controller/auto-create.d.ts +15 -0
  44. package/dist/types/controller/selectors.d.ts +5 -0
  45. package/dist/types/controller/types.d.ts +36 -0
  46. package/dist/types/controller/usePlaygroundController.d.ts +9 -0
  47. package/dist/types/index.d.ts +5 -0
  48. package/dist/types/panels/PlaygroundConversationPanel.d.ts +20 -0
  49. package/dist/types/scrcpy-preview.d.ts +11 -0
  50. package/package.json +4 -3
@@ -32,6 +32,7 @@ const scrcpy_decoder_webcodecs_namespaceObject = require("@yume-chan/scrcpy-deco
32
32
  const external_antd_namespaceObject = require("antd");
33
33
  const external_react_namespaceObject = require("react");
34
34
  const external_socket_io_client_namespaceObject = require("socket.io-client");
35
+ const external_scrcpy_preview_js_namespaceObject = require("./scrcpy-preview.js");
35
36
  const external_scrcpy_stream_js_namespaceObject = require("./scrcpy-stream.js");
36
37
  function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
37
38
  try {
@@ -60,30 +61,34 @@ function _async_to_generator(fn) {
60
61
  };
61
62
  }
62
63
  const { Text } = external_antd_namespaceObject.Typography;
63
- function ScrcpyPanel({ serverUrl, reconnectInterval = 3000 }) {
64
+ function ScrcpyPanel({ connectingOverlay, onStatusChange, renderErrorOverlay, serverUrl, metadataTimeoutMs = external_scrcpy_preview_js_namespaceObject.SCRCPY_METADATA_TIMEOUT_MS, reconnectInterval = 3000 }) {
64
65
  const canvasStageRef = (0, external_react_namespaceObject.useRef)(null);
65
66
  const socketRef = (0, external_react_namespaceObject.useRef)(null);
66
67
  const decoderRef = (0, external_react_namespaceObject.useRef)(null);
68
+ const metadataTimeoutRef = (0, external_react_namespaceObject.useRef)(null);
67
69
  const reconnectTimerRef = (0, external_react_namespaceObject.useRef)(null);
68
- const connectRef = (0, external_react_namespaceObject.useRef)(null);
69
- const disconnectRef = (0, external_react_namespaceObject.useRef)(null);
70
- const manuallyDisconnectedRef = (0, external_react_namespaceObject.useRef)(false);
70
+ const ignoreDisconnectRef = (0, external_react_namespaceObject.useRef)(false);
71
71
  const [status, setStatus] = (0, external_react_namespaceObject.useState)('connecting');
72
72
  const [errorMessage, setErrorMessage] = (0, external_react_namespaceObject.useState)(null);
73
- const [screenInfo, setScreenInfo] = (0, external_react_namespaceObject.useState)(null);
73
+ const [waitingStatusMessage, setWaitingStatusMessage] = (0, external_react_namespaceObject.useState)(()=>(0, external_scrcpy_preview_js_namespaceObject.getDefaultScrcpyWaitingStatusText)());
74
74
  const [webCodecsSupported, setWebCodecsSupported] = (0, external_react_namespaceObject.useState)(true);
75
- const statusText = (0, external_react_namespaceObject.useMemo)(()=>{
76
- switch(status){
77
- case 'connected':
78
- return 'Live scrcpy preview connected';
79
- case 'error':
80
- return 'Unable to start scrcpy preview';
81
- case 'disconnected':
82
- return 'scrcpy preview disconnected, retrying…';
83
- default:
84
- return 'Connecting to scrcpy preview…';
85
- }
75
+ const [retryNonce, setRetryNonce] = (0, external_react_namespaceObject.useState)(0);
76
+ const statusText = (0, external_react_namespaceObject.useMemo)(()=>(0, external_scrcpy_preview_js_namespaceObject.getScrcpyPreviewStatusText)(status, waitingStatusMessage), [
77
+ status,
78
+ waitingStatusMessage
79
+ ]);
80
+ const showCustomErrorOverlay = ('error' === status || 'disconnected' === status) && Boolean(renderErrorOverlay);
81
+ const renderResolvedErrorOverlay = renderErrorOverlay;
82
+ const requestRetry = (0, external_react_namespaceObject.useCallback)(()=>{
83
+ setStatus('connecting');
84
+ setErrorMessage(null);
85
+ setWaitingStatusMessage((0, external_scrcpy_preview_js_namespaceObject.getDefaultScrcpyWaitingStatusText)());
86
+ setRetryNonce((current)=>current + 1);
87
+ }, []);
88
+ (0, external_react_namespaceObject.useEffect)(()=>{
89
+ null == onStatusChange || onStatusChange(status);
86
90
  }, [
91
+ onStatusChange,
87
92
  status
88
93
  ]);
89
94
  const clearCanvas = ()=>{
@@ -96,16 +101,12 @@ function ScrcpyPanel({ serverUrl, reconnectInterval = 3000 }) {
96
101
  decoderRef.current.dispose();
97
102
  decoderRef.current = null;
98
103
  };
99
- const handleConnect = (0, external_react_namespaceObject.useCallback)(()=>{
100
- var _connectRef_current;
101
- manuallyDisconnectedRef.current = false;
102
- null == (_connectRef_current = connectRef.current) || _connectRef_current.call(connectRef);
103
- }, []);
104
- const handleDisconnect = (0, external_react_namespaceObject.useCallback)(()=>{
105
- var _disconnectRef_current;
106
- manuallyDisconnectedRef.current = true;
107
- null == (_disconnectRef_current = disconnectRef.current) || _disconnectRef_current.call(disconnectRef);
108
- }, []);
104
+ const clearMetadataTimeout = ()=>{
105
+ if (metadataTimeoutRef.current) {
106
+ clearTimeout(metadataTimeoutRef.current);
107
+ metadataTimeoutRef.current = null;
108
+ }
109
+ };
109
110
  (0, external_react_namespaceObject.useEffect)(()=>{
110
111
  if (!serverUrl) {
111
112
  setStatus('error');
@@ -120,6 +121,7 @@ function ScrcpyPanel({ serverUrl, reconnectInterval = 3000 }) {
120
121
  }
121
122
  let disposed = false;
122
123
  const cleanup = ()=>{
124
+ clearMetadataTimeout();
123
125
  if (reconnectTimerRef.current) {
124
126
  clearTimeout(reconnectTimerRef.current);
125
127
  reconnectTimerRef.current = null;
@@ -132,7 +134,7 @@ function ScrcpyPanel({ serverUrl, reconnectInterval = 3000 }) {
132
134
  clearCanvas();
133
135
  };
134
136
  const scheduleReconnect = ()=>{
135
- if (disposed || reconnectTimerRef.current || manuallyDisconnectedRef.current) return;
137
+ if (disposed || reconnectTimerRef.current) return;
136
138
  reconnectTimerRef.current = setTimeout(()=>{
137
139
  reconnectTimerRef.current = null;
138
140
  cleanup();
@@ -152,20 +154,14 @@ function ScrcpyPanel({ serverUrl, reconnectInterval = 3000 }) {
152
154
  codec: codecId,
153
155
  renderer
154
156
  });
155
- decoder.sizeChanged(({ width, height })=>{
156
- setScreenInfo({
157
- width,
158
- height
159
- });
160
- });
161
157
  return decoder;
162
158
  })();
163
159
  const connect = ()=>{
164
160
  if (disposed) return;
165
- manuallyDisconnectedRef.current = false;
161
+ ignoreDisconnectRef.current = false;
166
162
  setStatus('connecting');
167
163
  setErrorMessage(null);
168
- setScreenInfo(null);
164
+ setWaitingStatusMessage((0, external_scrcpy_preview_js_namespaceObject.getDefaultScrcpyWaitingStatusText)());
169
165
  const socket = (0, external_socket_io_client_namespaceObject.io)(serverUrl, {
170
166
  withCredentials: true,
171
167
  reconnection: false,
@@ -174,113 +170,100 @@ function ScrcpyPanel({ serverUrl, reconnectInterval = 3000 }) {
174
170
  socketRef.current = socket;
175
171
  const videoStream = (0, external_scrcpy_stream_js_namespaceObject.createScrcpyVideoStream)(socket);
176
172
  socket.on('connect', ()=>{
173
+ setStatus('waiting-for-stream');
174
+ setWaitingStatusMessage((0, external_scrcpy_preview_js_namespaceObject.getDefaultScrcpyWaitingStatusText)());
175
+ clearMetadataTimeout();
176
+ metadataTimeoutRef.current = setTimeout(()=>{
177
+ if (disposed) return;
178
+ ignoreDisconnectRef.current = true;
179
+ setStatus('error');
180
+ setErrorMessage((0, external_scrcpy_preview_js_namespaceObject.getScrcpyMetadataTimeoutMessage)(metadataTimeoutMs));
181
+ setWaitingStatusMessage((0, external_scrcpy_preview_js_namespaceObject.getDefaultScrcpyWaitingStatusText)());
182
+ socket.disconnect();
183
+ socketRef.current = null;
184
+ scheduleReconnect();
185
+ }, metadataTimeoutMs);
177
186
  socket.emit('connect-device', {
178
187
  maxSize: 1024
179
188
  });
180
189
  });
190
+ socket.on('preview-status', (event)=>{
191
+ if (disposed || !(0, external_scrcpy_preview_js_namespaceObject.isScrcpyPreviewStatusEvent)(event)) return;
192
+ setStatus('waiting-for-stream');
193
+ setWaitingStatusMessage(event.message);
194
+ });
181
195
  socket.on('video-metadata', (metadata)=>_async_to_generator(function*() {
182
196
  try {
197
+ clearMetadataTimeout();
183
198
  disposeDecoder();
199
+ setWaitingStatusMessage((0, external_scrcpy_preview_js_namespaceObject.getScrcpyDecoderStatusText)());
184
200
  const codecId = metadata.codec ? metadata.codec : scrcpy_namespaceObject.ScrcpyVideoCodecId.H264;
185
201
  const decoder = yield createDecoder(codecId);
186
202
  decoderRef.current = decoder;
187
- if (metadata.width && metadata.height) setScreenInfo({
188
- width: metadata.width,
189
- height: metadata.height
190
- });
191
203
  videoStream.pipeTo(decoder.writable).catch((error)=>{
192
204
  if (disposed) return;
193
205
  setStatus('error');
194
206
  setErrorMessage(error.message);
195
207
  scheduleReconnect();
196
208
  });
209
+ setWaitingStatusMessage((0, external_scrcpy_preview_js_namespaceObject.getDefaultScrcpyWaitingStatusText)());
197
210
  setStatus('connected');
198
211
  } catch (error) {
199
212
  if (disposed) return;
200
213
  setStatus('error');
201
214
  setErrorMessage(error instanceof Error ? error.message : 'Failed to start decoder.');
215
+ setWaitingStatusMessage((0, external_scrcpy_preview_js_namespaceObject.getDefaultScrcpyWaitingStatusText)());
202
216
  scheduleReconnect();
203
217
  }
204
218
  })());
205
219
  socket.on('disconnect', ()=>{
220
+ clearMetadataTimeout();
206
221
  if (disposed) return;
222
+ if (ignoreDisconnectRef.current) {
223
+ ignoreDisconnectRef.current = false;
224
+ return;
225
+ }
207
226
  setStatus('disconnected');
227
+ setErrorMessage(null);
228
+ setWaitingStatusMessage((0, external_scrcpy_preview_js_namespaceObject.getDefaultScrcpyWaitingStatusText)());
208
229
  scheduleReconnect();
209
230
  });
210
231
  socket.on('connect_error', (error)=>{
232
+ clearMetadataTimeout();
211
233
  if (disposed) return;
212
234
  setStatus('error');
213
235
  setErrorMessage(error.message);
236
+ setWaitingStatusMessage((0, external_scrcpy_preview_js_namespaceObject.getDefaultScrcpyWaitingStatusText)());
214
237
  scheduleReconnect();
215
238
  });
216
239
  socket.on('error', (error)=>{
240
+ clearMetadataTimeout();
217
241
  if (disposed) return;
218
242
  setStatus('error');
219
243
  setErrorMessage(error.message);
244
+ setWaitingStatusMessage((0, external_scrcpy_preview_js_namespaceObject.getDefaultScrcpyWaitingStatusText)());
220
245
  scheduleReconnect();
221
246
  });
222
247
  };
223
- const disconnect = ()=>{
224
- cleanup();
225
- setStatus('disconnected');
226
- setErrorMessage(null);
227
- setScreenInfo(null);
228
- };
229
- connectRef.current = connect;
230
- disconnectRef.current = disconnect;
231
248
  connect();
232
249
  return ()=>{
233
250
  disposed = true;
234
- connectRef.current = null;
235
- disconnectRef.current = null;
236
251
  cleanup();
237
252
  };
238
253
  }, [
254
+ metadataTimeoutMs,
239
255
  reconnectInterval,
256
+ retryNonce,
240
257
  serverUrl
241
258
  ]);
242
- return /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsxs)(external_antd_namespaceObject.Card, {
243
- size: "small",
244
- title: "Live scrcpy preview",
259
+ return /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsxs)("div", {
245
260
  style: {
246
261
  height: '100%',
247
262
  display: 'flex',
248
263
  flexDirection: 'column'
249
264
  },
250
- styles: {
251
- body: {
252
- flex: 1,
253
- display: 'flex',
254
- flexDirection: 'column',
255
- minHeight: 0
256
- }
257
- },
258
- extra: /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsxs)(external_antd_namespaceObject.Space, {
259
- size: "small",
260
- children: [
261
- screenInfo ? /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsxs)(Text, {
262
- type: "secondary",
263
- children: [
264
- screenInfo.width,
265
- " \xd7 ",
266
- screenInfo.height
267
- ]
268
- }) : null,
269
- 'connected' === status ? /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)(external_antd_namespaceObject.Button, {
270
- size: "small",
271
- onClick: handleDisconnect,
272
- children: "Disconnect"
273
- }) : /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)(external_antd_namespaceObject.Button, {
274
- size: "small",
275
- type: "primary",
276
- loading: 'connecting' === status,
277
- onClick: handleConnect,
278
- children: "Connect"
279
- })
280
- ]
281
- }),
282
265
  children: [
283
- errorMessage ? /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)(external_antd_namespaceObject.Alert, {
266
+ errorMessage && !showCustomErrorOverlay ? /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)(external_antd_namespaceObject.Alert, {
284
267
  type: "warning",
285
268
  showIcon: true,
286
269
  style: {
@@ -312,7 +295,26 @@ function ScrcpyPanel({ serverUrl, reconnectInterval = 3000 }) {
312
295
  justifyContent: 'center'
313
296
  }
314
297
  }),
315
- 'connected' !== status && /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsxs)("div", {
298
+ 'connected' !== status ? showCustomErrorOverlay && renderResolvedErrorOverlay ? /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)("div", {
299
+ style: {
300
+ position: 'absolute',
301
+ inset: 0,
302
+ zIndex: 1
303
+ },
304
+ children: renderResolvedErrorOverlay({
305
+ errorMessage,
306
+ retry: requestRetry,
307
+ status,
308
+ statusText
309
+ })
310
+ }) : 'error' !== status && 'disconnected' !== status && connectingOverlay ? /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)("div", {
311
+ style: {
312
+ position: 'absolute',
313
+ inset: 0,
314
+ zIndex: 1
315
+ },
316
+ children: connectingOverlay
317
+ }) : /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsxs)("div", {
316
318
  style: {
317
319
  position: 'absolute',
318
320
  inset: 0,
@@ -328,7 +330,7 @@ function ScrcpyPanel({ serverUrl, reconnectInterval = 3000 }) {
328
330
  zIndex: 1
329
331
  },
330
332
  children: [
331
- /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)(external_antd_namespaceObject.Spin, {
333
+ 'error' === status ? null : /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)(external_antd_namespaceObject.Spin, {
332
334
  spinning: true
333
335
  }),
334
336
  /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)(Text, {
@@ -337,6 +339,12 @@ function ScrcpyPanel({ serverUrl, reconnectInterval = 3000 }) {
337
339
  },
338
340
  children: statusText
339
341
  }),
342
+ 'error' === status ? /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)(Text, {
343
+ style: {
344
+ color: '#d1d5db'
345
+ },
346
+ children: "Scrcpy preview will retry automatically."
347
+ }) : null,
340
348
  !webCodecsSupported && /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)(Text, {
341
349
  style: {
342
350
  color: '#d1d5db'
@@ -344,7 +352,7 @@ function ScrcpyPanel({ serverUrl, reconnectInterval = 3000 }) {
344
352
  children: "Please use a modern Chromium browser to view the stream."
345
353
  })
346
354
  ]
347
- })
355
+ }) : null
348
356
  ]
349
357
  })
350
358
  ]
@@ -0,0 +1,292 @@
1
+ .session-setup-panel {
2
+ background: #fff;
3
+ flex-direction: column;
4
+ flex: 1;
5
+ justify-content: flex-start;
6
+ align-items: center;
7
+ padding: 0 56px;
8
+ display: flex;
9
+ }
10
+
11
+ .session-setup-card {
12
+ flex-direction: column;
13
+ align-items: center;
14
+ width: 100%;
15
+ max-width: 288px;
16
+ margin-top: 96px;
17
+ display: flex;
18
+ }
19
+
20
+ .session-setup-logo {
21
+ object-fit: contain;
22
+ flex-shrink: 0;
23
+ width: 51px;
24
+ height: 48px;
25
+ }
26
+
27
+ .session-setup-title {
28
+ color: #000;
29
+ text-align: center;
30
+ white-space: pre-line;
31
+ width: 240px;
32
+ margin: 16px 0 0;
33
+ font-family: Roboto, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, sans-serif;
34
+ font-size: 18px;
35
+ font-weight: 600;
36
+ line-height: 22px;
37
+ }
38
+
39
+ .session-setup-description {
40
+ color: rgba(0, 0, 0, .7);
41
+ text-align: center;
42
+ width: 276px;
43
+ margin: 14.7px 0 0;
44
+ font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, sans-serif;
45
+ font-size: 12px;
46
+ font-weight: 400;
47
+ line-height: 20px;
48
+ }
49
+
50
+ .session-setup-alert.ant-alert {
51
+ width: 100%;
52
+ margin-top: 16px;
53
+ }
54
+
55
+ .session-setup-form {
56
+ width: 100%;
57
+ margin-top: 24px;
58
+ }
59
+
60
+ .session-setup-form .ant-form-item {
61
+ margin-bottom: 16px;
62
+ }
63
+
64
+ .session-setup-form .ant-form-item-label {
65
+ padding-bottom: 4px;
66
+ }
67
+
68
+ .session-setup-form .ant-form-item-label > label {
69
+ color: rgba(0, 0, 0, .5);
70
+ height: 15px;
71
+ font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, sans-serif;
72
+ font-size: 12px;
73
+ font-weight: 500;
74
+ line-height: 14.5px;
75
+ }
76
+
77
+ .session-setup-form .ant-form-item-label > label.ant-form-item-required:before {
78
+ color: rgba(0, 0, 0, .5);
79
+ }
80
+
81
+ .session-setup-form .ant-form-item-label > label.ant-form-item-required:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))):before {
82
+ margin-right: 2px;
83
+ }
84
+
85
+ .session-setup-form .ant-form-item-label > label.ant-form-item-required:not(:-moz-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))):before {
86
+ margin-right: 2px;
87
+ }
88
+
89
+ .session-setup-form .ant-form-item-label > label.ant-form-item-required:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))):before {
90
+ margin-right: 2px;
91
+ }
92
+
93
+ .session-setup-form .ant-form-item-label > label.ant-form-item-required:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)):before {
94
+ margin-left: 2px;
95
+ }
96
+
97
+ .session-setup-form .ant-form-item-label > label.ant-form-item-required:-moz-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)):before {
98
+ margin-left: 2px;
99
+ }
100
+
101
+ .session-setup-form .ant-form-item-label > label.ant-form-item-required:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)):before {
102
+ margin-left: 2px;
103
+ }
104
+
105
+ .session-setup-form .ant-select-single .ant-select-selector {
106
+ height: 36px;
107
+ box-shadow: none;
108
+ background: #f2f4f7;
109
+ border: none;
110
+ border-radius: 8px;
111
+ align-items: center;
112
+ padding: 0 12px;
113
+ display: flex;
114
+ }
115
+
116
+ .session-setup-form .ant-select-single .ant-select-selector .ant-select-selection-item, .session-setup-form .ant-select-single .ant-select-selector .ant-select-selection-placeholder {
117
+ color: #000;
118
+ font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, sans-serif;
119
+ font-size: 14px;
120
+ font-weight: 400;
121
+ line-height: 36px;
122
+ }
123
+
124
+ .session-setup-form .ant-select-single .ant-select-selector .ant-select-selection-placeholder {
125
+ color: rgba(0, 0, 0, .4);
126
+ }
127
+
128
+ .session-setup-form .ant-select-arrow:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
129
+ right: 12px;
130
+ }
131
+
132
+ .session-setup-form .ant-select-arrow:not(:-moz-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
133
+ right: 12px;
134
+ }
135
+
136
+ .session-setup-form .ant-select-arrow:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
137
+ right: 12px;
138
+ }
139
+
140
+ .session-setup-form .ant-select-arrow:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
141
+ left: 12px;
142
+ }
143
+
144
+ .session-setup-form .ant-select-arrow:-moz-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
145
+ left: 12px;
146
+ }
147
+
148
+ .session-setup-form .ant-select-arrow:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
149
+ left: 12px;
150
+ }
151
+
152
+ .session-setup-form .ant-input, .session-setup-form .ant-input-number {
153
+ height: 36px;
154
+ box-shadow: none;
155
+ color: #000;
156
+ background: #f2f4f7;
157
+ border: none;
158
+ border-radius: 8px;
159
+ font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, sans-serif;
160
+ font-size: 14px;
161
+ }
162
+
163
+ .session-setup-form .ant-input-number-input {
164
+ height: 36px;
165
+ }
166
+
167
+ .session-setup-select-icon {
168
+ object-fit: contain;
169
+ pointer-events: none;
170
+ width: 16px;
171
+ height: 16px;
172
+ }
173
+
174
+ .session-setup-submit {
175
+ color: #f2f4f7;
176
+ cursor: pointer;
177
+ background: #1979ff;
178
+ border: 0;
179
+ border-radius: 8px;
180
+ width: 100%;
181
+ height: 32px;
182
+ margin-top: 7px;
183
+ font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, sans-serif;
184
+ font-size: 14px;
185
+ font-weight: 500;
186
+ line-height: 22px;
187
+ transition: opacity .2s;
188
+ }
189
+
190
+ .session-setup-submit:hover:not(:disabled) {
191
+ opacity: .9;
192
+ }
193
+
194
+ .session-setup-submit:disabled {
195
+ cursor: not-allowed;
196
+ opacity: .6;
197
+ }
198
+
199
+ .platform-selector-group {
200
+ grid-template-columns: repeat(2, minmax(0, 1fr));
201
+ gap: 12px;
202
+ width: 100%;
203
+ display: grid;
204
+ }
205
+
206
+ .platform-selector-group .ant-radio-button-wrapper {
207
+ white-space: normal;
208
+ background: #fff;
209
+ border-radius: 14px;
210
+ justify-content: flex-start;
211
+ align-items: flex-start;
212
+ height: auto;
213
+ min-height: 92px;
214
+ padding: 14px 16px;
215
+ transition: border-color .2s, box-shadow .2s, transform .2s;
216
+ display: flex;
217
+ }
218
+
219
+ .platform-selector-group .ant-radio-button-wrapper:not(:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
220
+ border-left-width: 1px;
221
+ }
222
+
223
+ .platform-selector-group .ant-radio-button-wrapper:not(:-moz-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
224
+ border-left-width: 1px;
225
+ }
226
+
227
+ .platform-selector-group .ant-radio-button-wrapper:not(:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi))) {
228
+ border-left-width: 1px;
229
+ }
230
+
231
+ .platform-selector-group .ant-radio-button-wrapper:-webkit-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
232
+ border-right-width: 1px;
233
+ }
234
+
235
+ .platform-selector-group .ant-radio-button-wrapper:-moz-any(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
236
+ border-right-width: 1px;
237
+ }
238
+
239
+ .platform-selector-group .ant-radio-button-wrapper:is(:lang(ae), :lang(ar), :lang(arc), :lang(bcc), :lang(bqi), :lang(ckb), :lang(dv), :lang(fa), :lang(glk), :lang(he), :lang(ku), :lang(mzn), :lang(nqo), :lang(pnb), :lang(ps), :lang(sd), :lang(ug), :lang(ur), :lang(yi)) {
240
+ border-right-width: 1px;
241
+ }
242
+
243
+ .platform-selector-group .ant-radio-button-wrapper:before {
244
+ display: none;
245
+ }
246
+
247
+ .platform-selector-group .ant-radio-button-wrapper:hover {
248
+ border-color: #1677ff;
249
+ transform: translateY(-1px);
250
+ }
251
+
252
+ .platform-selector-group .ant-radio-button-wrapper-checked {
253
+ border-color: #1677ff;
254
+ box-shadow: 0 10px 24px rgba(22, 119, 255, .12);
255
+ }
256
+
257
+ .platform-selector-card .platform-selector-title {
258
+ color: rgba(0, 0, 0, .88);
259
+ font-size: 15px;
260
+ font-weight: 600;
261
+ line-height: 1.4;
262
+ }
263
+
264
+ .platform-selector-card .platform-selector-description {
265
+ color: rgba(0, 0, 0, .5);
266
+ margin-top: 6px;
267
+ font-size: 12px;
268
+ line-height: 1.5;
269
+ }
270
+
271
+ .session-select-option {
272
+ flex-direction: column;
273
+ gap: 2px;
274
+ line-height: 1.4;
275
+ display: flex;
276
+ }
277
+
278
+ .session-select-option-label {
279
+ color: rgba(0, 0, 0, .88);
280
+ }
281
+
282
+ .session-select-option-description {
283
+ color: rgba(0, 0, 0, .45);
284
+ font-size: 12px;
285
+ }
286
+
287
+ @media (max-width: 640px) {
288
+ .platform-selector-group {
289
+ grid-template-columns: 1fr;
290
+ }
291
+ }
292
+