@limrun/ui 0.16.0 → 0.17.0-rc.2

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 (73) hide show
  1. package/README.md +64 -0
  2. package/dist/GLTFLoader-Cami_vza.mjs +1679 -0
  3. package/dist/GLTFLoader-DeCLxW_q.js +1 -0
  4. package/dist/components/duo-button-hinge.d.ts +16 -0
  5. package/dist/components/duo-controls.d.ts +21 -0
  6. package/dist/components/duo-flat-frame.d.ts +7 -0
  7. package/dist/components/duo-flat-geometry.d.ts +52 -0
  8. package/dist/components/duo-fold-motion.d.ts +27 -0
  9. package/dist/components/duo-fold-renderer.d.ts +19 -0
  10. package/dist/components/duo-fold-timing.d.ts +1 -0
  11. package/dist/components/duo-frame.d.ts +16 -0
  12. package/dist/components/duo-hardware.d.ts +20 -0
  13. package/dist/components/duo-loading.d.ts +8 -0
  14. package/dist/components/duo-model.d.ts +19 -0
  15. package/dist/components/duo-screen-handoff.d.ts +4 -0
  16. package/dist/components/duo-view.d.ts +9 -0
  17. package/dist/components/remote-control.d.ts +5 -1
  18. package/dist/core/duo.d.ts +63 -0
  19. package/dist/duo-fold-renderer-BXejNHcY.mjs +257 -0
  20. package/dist/duo-fold-renderer-DdltAnF7.js +53 -0
  21. package/dist/duo-frame-Cmw4p2uN.mjs +26767 -0
  22. package/dist/duo-frame-iMtrvx_a.js +4059 -0
  23. package/dist/duo-view-BAh-w2Kz.js +1 -0
  24. package/dist/duo-view-D9Ae00rj.mjs +512 -0
  25. package/dist/index.cjs +1 -1
  26. package/dist/index.css +1 -1
  27. package/dist/index.js +2 -2
  28. package/dist/lite.cjs +1 -1
  29. package/dist/lite.js +1 -1
  30. package/dist/remote-control-B2jZpf3c.js +1 -0
  31. package/dist/remote-control-DiddxS8l.mjs +2883 -0
  32. package/package.json +8 -2
  33. package/dist/demo.d.ts +0 -1
  34. package/dist/remote-control-BRgbD7yo.mjs +0 -2516
  35. package/dist/remote-control-VISrP1zI.js +0 -1
  36. package/index.html +0 -199
  37. package/src/assets/Apple_logo_white.svg +0 -4
  38. package/src/assets/android_boot.gif +0 -0
  39. package/src/assets/android_boot.webp +0 -0
  40. package/src/assets/iphone16pro_black.webp +0 -0
  41. package/src/assets/iphone16pro_black_bg.webp +0 -0
  42. package/src/assets/iphone16pro_black_landscape.webp +0 -0
  43. package/src/assets/iphone16pro_black_landscape_bg.webp +0 -0
  44. package/src/assets/pixel9_black.webp +0 -0
  45. package/src/assets/pixel9_black_landscape.webp +0 -0
  46. package/src/assets/pixel_tablet_landscape.webp +0 -0
  47. package/src/assets/pixel_tablet_portrait.webp +0 -0
  48. package/src/components/device-install/index.ts +0 -1
  49. package/src/components/inspect-overlay.css +0 -223
  50. package/src/components/inspect-overlay.tsx +0 -437
  51. package/src/components/remote-control-framed.tsx +0 -14
  52. package/src/components/remote-control.css +0 -131
  53. package/src/components/remote-control.test.ts +0 -34
  54. package/src/components/remote-control.tsx +0 -3853
  55. package/src/core/ax-fetcher.test.ts +0 -418
  56. package/src/core/ax-fetcher.ts +0 -387
  57. package/src/core/ax-tree.test.ts +0 -491
  58. package/src/core/ax-tree.ts +0 -416
  59. package/src/core/camera.test.ts +0 -155
  60. package/src/core/camera.ts +0 -133
  61. package/src/core/constants.ts +0 -344
  62. package/src/core/webrtc-messages.ts +0 -148
  63. package/src/demo.tsx +0 -269
  64. package/src/frame-assets.ts +0 -22
  65. package/src/hooks/index.ts +0 -1
  66. package/src/index.ts +0 -29
  67. package/src/lite.test.ts +0 -30
  68. package/src/lite.ts +0 -30
  69. package/src/vite-env.d.ts +0 -1
  70. package/tsconfig.json +0 -26
  71. package/tsconfig.node.json +0 -26
  72. package/vite.config.ts +0 -35
  73. package/vitest.config.ts +0 -24
package/src/demo.tsx DELETED
@@ -1,269 +0,0 @@
1
- import { useState, useRef } from 'react';
2
- import { createRoot } from 'react-dom/client';
3
- import { RemoteControl } from './components/remote-control-framed';
4
- import type { RemoteControlHandle } from './components/remote-control';
5
- import { AxSnapshot } from './core/ax-tree';
6
-
7
- type InspectChoice = 'off' | 'hover-only' | 'select';
8
-
9
- // Pre-fill from query string so the developer testing the demo can deeplink
10
- // `?url=...&token=...&inspect=select&autoconnect=1`. Nothing is persisted.
11
- const initialParams = new URLSearchParams(window.location.search);
12
- const initialUrl = initialParams.get('url') || 'ws://localhost:8833/signaling';
13
- const initialToken = initialParams.get('token') || 'token';
14
- const initialPlatformParam = initialParams.get('platform');
15
- const initialPlatform: 'ios' | 'android' = initialPlatformParam === 'android' ? 'android' : 'ios';
16
- const initialInspect = (initialParams.get('inspect') as InspectChoice | null) ?? 'off';
17
- const initialAutoconnect =
18
- initialParams.get('autoconnect') === '1' && initialParams.has('url') && initialParams.has('token');
19
-
20
- function Demo() {
21
- const [url, setUrl] = useState(initialUrl);
22
- const [token, setToken] = useState(initialToken);
23
- const [platform, setPlatform] = useState<'ios' | 'android'>(initialPlatform);
24
- const [isConnected, setIsConnected] = useState(initialAutoconnect);
25
- const [key, setKey] = useState(0);
26
- const [showDebugInfo, setShowDebugInfo] = useState(false);
27
- const [inspectChoice, setInspectChoice] = useState<InspectChoice>(initialInspect);
28
- const [latestSnapshot, setLatestSnapshot] = useState<AxSnapshot | null>(null);
29
- const [latestSelection, setLatestSelection] = useState<string | null>(null);
30
-
31
- const remoteControlRef = useRef<RemoteControlHandle>(null);
32
-
33
- const inspectModeProp: boolean | 'hover-only' | undefined =
34
- inspectChoice === 'off' ? undefined
35
- : inspectChoice === 'hover-only' ? 'hover-only'
36
- : true;
37
-
38
- const handleConnect = () => {
39
- if (url) {
40
- setIsConnected(true);
41
- // Force remount by changing key
42
- setKey((prev) => prev + 1);
43
- }
44
- };
45
-
46
- const handleDisconnect = () => {
47
- setIsConnected(false);
48
- setKey((prev) => prev + 1);
49
- };
50
-
51
- const handleScreenshot = async () => {
52
- if (remoteControlRef.current) {
53
- try {
54
- const screenshot = await remoteControlRef.current.screenshot();
55
- // Open screenshot in new window
56
- const win = window.open();
57
- if (win) {
58
- win.document.write(`<img src="${screenshot.dataUri}" style="max-width: 100%;" />`);
59
- }
60
- } catch (error) {
61
- console.error('Screenshot failed:', error);
62
- alert('Screenshot failed: ' + (error as Error).message);
63
- }
64
- }
65
- };
66
-
67
- return (
68
- <>
69
- <div className="header">
70
- <h1>📱 RemoteControl Component Demo</h1>
71
- <p>Test the iOS device frame and remote control features</p>
72
- </div>
73
-
74
- <div className="demo-container">
75
- <div className="info-box">
76
- <h4>ℹ️ How to Use:</h4>
77
- <p>
78
- Enter your WebSocket URL and authentication token below, select iOS or Android platform, then
79
- click Connect to see the remote control in action. The iOS platform will display a realistic
80
- iPhone frame around the stream.
81
- </p>
82
- <p style={{ marginTop: '10px', fontWeight: 600 }}>
83
- ✨ iOS Feature: Touches can start from the bottom bezel area (below the screen, near the home
84
- indicator) to enable authentic iOS swipe-up gestures for going home or switching apps!
85
- </p>
86
- </div>
87
-
88
- <div className="controls">
89
- <div className="control-group">
90
- <label htmlFor="url">WebSocket URL</label>
91
- <input
92
- id="url"
93
- type="text"
94
- value={url}
95
- onChange={(e) => setUrl(e.target.value)}
96
- placeholder="wss://your-instance.limrun.com/control"
97
- disabled={isConnected}
98
- />
99
- </div>
100
-
101
- <div className="control-group">
102
- <label htmlFor="token">Authentication Token</label>
103
- <input
104
- id="token"
105
- type="password"
106
- value={token}
107
- onChange={(e) => setToken(e.target.value)}
108
- placeholder="Enter your token"
109
- disabled={isConnected}
110
- />
111
- </div>
112
-
113
- <div className="control-group">
114
- <label htmlFor="platform">Platform</label>
115
- <select
116
- id="platform"
117
- value={platform}
118
- onChange={(e) => setPlatform(e.target.value as 'ios' | 'android')}
119
- disabled={isConnected}
120
- >
121
- <option value="ios">iOS (with frame)</option>
122
- <option value="android">Android (no frame)</option>
123
- </select>
124
- </div>
125
-
126
- <div className="control-group">
127
- <label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
128
- <input
129
- type="checkbox"
130
- checked={showDebugInfo}
131
- onChange={(e) => setShowDebugInfo(e.target.checked)}
132
- style={{ cursor: 'pointer' }}
133
- />
134
- Show iOS Extended Touch Area Info
135
- </label>
136
- </div>
137
-
138
- <div className="control-group">
139
- <label htmlFor="inspect-mode">Inspect Mode</label>
140
- <select
141
- id="inspect-mode"
142
- value={inspectChoice}
143
- onChange={(e) => setInspectChoice(e.target.value as InspectChoice)}
144
- >
145
- <option value="off">Off</option>
146
- <option value="hover-only">Hover-only (input still works)</option>
147
- <option value="select">Select (click to pin, ESC to clear)</option>
148
- </select>
149
- </div>
150
-
151
- <div className="button-group">
152
- {!isConnected ?
153
- <button className="primary" onClick={handleConnect} disabled={!url}>
154
- Connect
155
- </button>
156
- : <>
157
- <button className="secondary" onClick={handleDisconnect}>
158
- Disconnect
159
- </button>
160
- <button className="primary" onClick={handleScreenshot}>
161
- Take Screenshot
162
- </button>
163
- </>
164
- }
165
- </div>
166
- </div>
167
-
168
- {showDebugInfo && platform === 'ios' && (
169
- <div
170
- style={{
171
- background: '#e0f2fe',
172
- border: '2px solid #0284c7',
173
- borderRadius: '8px',
174
- padding: '15px',
175
- marginBottom: '20px',
176
- }}
177
- >
178
- <h4 style={{ color: '#0c4a6e', marginBottom: '8px', fontSize: '0.95rem' }}>
179
- 🔧 iOS Extended Touch Area
180
- </h4>
181
- <p style={{ color: '#075985', fontSize: '0.9rem', lineHeight: '1.5', margin: 0 }}>
182
- The iOS frame includes a <strong>60-pixel extended touch area</strong> below the visible screen.
183
- This area (where the home indicator is located) can receive touch events and sends coordinates
184
- beyond the screen bounds (y &gt; screenHeight), allowing iOS to properly detect gestures that
185
- start from outside the screen - just like on a real iPhone. Try starting a swipe gesture from
186
- the home indicator area!
187
- </p>
188
- </div>
189
- )}
190
-
191
- {isConnected ?
192
- <>
193
- <div className="device-preview">
194
- <div className="preview-item">
195
- <h3>{platform === 'ios' ? '📱 iOS with Frame' : '🤖 Android (No Frame)'}</h3>
196
- <div className="device-wrapper">
197
- <RemoteControl
198
- key={key}
199
- ref={remoteControlRef}
200
- url={url}
201
- token={token}
202
- inspectMode={inspectModeProp}
203
- onAxSnapshotChange={setLatestSnapshot}
204
- onInspectSelectionChange={(sel) =>
205
- setLatestSelection(
206
- sel ?
207
- `${sel.element.role || sel.element.type} · ${sel.element.label || '(no label)'}`
208
- : null,
209
- )
210
- }
211
- />
212
- </div>
213
- </div>
214
- </div>
215
-
216
- {inspectChoice !== 'off' && (
217
- <div
218
- style={{
219
- marginTop: '20px',
220
- background: '#0f172a',
221
- color: '#e2e8f0',
222
- border: '1px solid #1e293b',
223
- borderRadius: '8px',
224
- padding: '12px 14px',
225
- fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
226
- fontSize: '12px',
227
- }}
228
- >
229
- <div
230
- style={{
231
- fontWeight: 600,
232
- marginBottom: '6px',
233
- display: 'flex',
234
- gap: '12px',
235
- flexWrap: 'wrap',
236
- }}
237
- >
238
- <span>Inspect debug (demo-only)</span>
239
- <span style={{ color: '#94a3b8', fontWeight: 400 }}>
240
- elements: {latestSnapshot?.elements.length ?? 0} · screen:{' '}
241
- {latestSnapshot ? `${latestSnapshot.screen.width}×${latestSnapshot.screen.height}` : '—'}{' '}
242
- · platform: {latestSnapshot?.platform ?? '—'} · selection: {latestSelection ?? '(none)'}
243
- </span>
244
- </div>
245
- </div>
246
- )}
247
- </>
248
- : <div
249
- style={{
250
- textAlign: 'center',
251
- padding: '60px 20px',
252
- color: '#9ca3af',
253
- fontSize: '1.1rem',
254
- }}
255
- >
256
- Enter your connection details above and click Connect to start
257
- </div>
258
- }
259
- </div>
260
- </>
261
- );
262
- }
263
-
264
- // Mount the demo app
265
- const container = document.getElementById('root');
266
- if (container) {
267
- const root = createRoot(container);
268
- root.render(<Demo />);
269
- }
@@ -1,22 +0,0 @@
1
- // Limrun's bundled device frames and boot logos. Imported only by the default
2
- // entry so `@limrun/ui/lite` ships without ~600 KB of inlined artwork.
3
- import type { RemoteControlAssets } from './components/remote-control';
4
- import iphoneFrame from './assets/iphone16pro_black_bg.webp';
5
- import iphoneFrameLandscape from './assets/iphone16pro_black_landscape_bg.webp';
6
- import pixelFrame from './assets/pixel9_black.webp';
7
- import pixelFrameLandscape from './assets/pixel9_black_landscape.webp';
8
- import pixelTabletFrame from './assets/pixel_tablet_portrait.webp';
9
- import pixelTabletFrameLandscape from './assets/pixel_tablet_landscape.webp';
10
- import appleLogo from './assets/Apple_logo_white.svg';
11
- import androidBootLogo from './assets/android_boot.webp';
12
-
13
- export const defaultRemoteControlAssets: RemoteControlAssets = {
14
- ios: { frame: iphoneFrame, frameLandscape: iphoneFrameLandscape, loadingLogo: appleLogo },
15
- android: {
16
- frame: pixelFrame,
17
- frameLandscape: pixelFrameLandscape,
18
- tabletFrame: pixelTabletFrame,
19
- tabletFrameLandscape: pixelTabletFrameLandscape,
20
- loadingLogo: androidBootLogo,
21
- },
22
- };
@@ -1 +0,0 @@
1
- export {};
package/src/index.ts DELETED
@@ -1,29 +0,0 @@
1
- export { RemoteControl } from './components/remote-control-framed';
2
- export { defaultRemoteControlAssets } from './frame-assets';
3
- export type {
4
- RemoteControlProps,
5
- RemoteControlHandle,
6
- RemoteControlAssets,
7
- RemoteControlPlatformAssets,
8
- RemoteControlConnectionState,
9
- MicrophoneState,
10
- CameraStreamStats,
11
- } from './components/remote-control';
12
- export type { CameraCaptureInfo, CameraFacingMode, CameraRequest, CameraResult } from './core/camera';
13
-
14
- // Accessibility / inspect-mode types and helpers. Exported so customers can
15
- // build their own side panels, search UIs, or agent-driven inspectors on top
16
- // of the snapshots delivered via `onAxSnapshotChange`.
17
- export type { AxSnapshot, AxElement, AxRect, AxSelectors, AxPlatform } from './core/ax-tree';
18
- export type { AndroidElementTreeOptions, AxStatus } from './core/ax-fetcher';
19
- export {
20
- axElementAtPoint,
21
- axElementSelectorExpression,
22
- axElementSummary,
23
- axElementsEqual,
24
- axSnapshotsEqual,
25
- clampAxFrameForScreen,
26
- normalizeAndroidTree,
27
- normalizeIosTree,
28
- AX_UNAVAILABLE_ERROR,
29
- } from './core/ax-tree';
package/src/lite.test.ts DELETED
@@ -1,30 +0,0 @@
1
- import { describe, expect, test } from 'vitest';
2
- import { existsSync, readFileSync, readdirSync } from 'node:fs';
3
- import { join } from 'node:path';
4
-
5
- // Guards the reason the lite entry exists: an embed that imports it must not
6
- // pull the bundled device artwork. Runs against dist, so build first.
7
- const dist = join(__dirname, '..', 'dist');
8
- const describeDist = existsSync(join(dist, 'lite.js')) ? describe : describe.skip;
9
-
10
- describeDist('@limrun/ui/lite bundle', () => {
11
- const chunksOf = (entry: string): string[] => {
12
- const src = readFileSync(join(dist, entry), 'utf8');
13
- const chunks = [...src.matchAll(/from "\.\/([^"]+)"/g)].map((m) => m[1]);
14
- return [src, ...chunks.map((c) => readFileSync(join(dist, c), 'utf8'))];
15
- };
16
-
17
- test('lite entry and its chunks carry no inlined images', () => {
18
- for (const code of chunksOf('lite.js')) {
19
- expect(code).not.toMatch(/data:image\//);
20
- }
21
- });
22
-
23
- test('default entry still bundles the device frames (positive control)', () => {
24
- expect(chunksOf('index.js').some((code) => /data:image\/webp/.test(code))).toBe(true);
25
- });
26
-
27
- test('one stylesheet keeps its historical name', () => {
28
- expect(readdirSync(dist).filter((f) => f.endsWith('.css'))).toEqual(['index.css']);
29
- });
30
- });
package/src/lite.ts DELETED
@@ -1,30 +0,0 @@
1
- // Frameless entry: identical API, but no bundled device artwork. Pass `assets`
2
- // to add frames or boot logos.
3
- export { RemoteControl } from './components/remote-control';
4
- export type {
5
- RemoteControlProps,
6
- RemoteControlHandle,
7
- RemoteControlAssets,
8
- RemoteControlPlatformAssets,
9
- RemoteControlConnectionState,
10
- MicrophoneState,
11
- CameraStreamStats,
12
- } from './components/remote-control';
13
- export type { CameraCaptureInfo, CameraFacingMode, CameraRequest, CameraResult } from './core/camera';
14
-
15
- // Accessibility / inspect-mode types and helpers. Exported so customers can
16
- // build their own side panels, search UIs, or agent-driven inspectors on top
17
- // of the snapshots delivered via `onAxSnapshotChange`.
18
- export type { AxSnapshot, AxElement, AxRect, AxSelectors, AxPlatform } from './core/ax-tree';
19
- export type { AndroidElementTreeOptions, AxStatus } from './core/ax-fetcher';
20
- export {
21
- axElementAtPoint,
22
- axElementSelectorExpression,
23
- axElementSummary,
24
- axElementsEqual,
25
- axSnapshotsEqual,
26
- clampAxFrameForScreen,
27
- normalizeAndroidTree,
28
- normalizeIosTree,
29
- AX_UNAVAILABLE_ERROR,
30
- } from './core/ax-tree';
package/src/vite-env.d.ts DELETED
@@ -1 +0,0 @@
1
- /// <reference types="vite/client" />
package/tsconfig.json DELETED
@@ -1,26 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2020",
4
- "useDefineForClassFields": true,
5
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
- "module": "ESNext",
7
- "skipLibCheck": true,
8
- "moduleResolution": "bundler",
9
- "allowImportingTsExtensions": false,
10
- "resolveJsonModule": true,
11
- "isolatedModules": true,
12
- "noEmit": false,
13
- "emitDeclarationOnly": true,
14
- "declaration": true,
15
- "declarationDir": "dist",
16
- "jsx": "react-jsx",
17
- "strict": true,
18
- "noUnusedLocals": true,
19
- "noUnusedParameters": true,
20
- "noFallthroughCasesInSwitch": true,
21
- "outDir": "dist"
22
- },
23
- "include": ["src"],
24
- "exclude": ["**/*.test.ts", "**/*.test.tsx", "node_modules", "dist"],
25
- "references": [{ "path": "./tsconfig.node.json" }]
26
- }
@@ -1,26 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "composite": true,
4
- "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
5
- "target": "ES2022",
6
- "lib": ["ES2023"],
7
- "module": "ESNext",
8
- "skipLibCheck": true,
9
-
10
- /* Bundler mode */
11
- "moduleResolution": "bundler",
12
- "allowImportingTsExtensions": true,
13
- "isolatedModules": true,
14
- "moduleDetection": "force",
15
- "noEmit": false,
16
- "emitDeclarationOnly": true,
17
-
18
- /* Linting */
19
- "strict": true,
20
- "noUnusedLocals": true,
21
- "noUnusedParameters": true,
22
- "noFallthroughCasesInSwitch": true,
23
- "noUncheckedSideEffectImports": true
24
- },
25
- "include": ["vite.config.ts"]
26
- }
package/vite.config.ts DELETED
@@ -1,35 +0,0 @@
1
- import { defineConfig } from 'vite';
2
- import react from '@vitejs/plugin-react-swc';
3
- import { resolve } from 'path';
4
- import dts from 'vite-plugin-dts';
5
- import { libInjectCss } from 'vite-plugin-lib-inject-css';
6
-
7
- // https://vite.dev/config/
8
- export default defineConfig({
9
- plugins: [react(), libInjectCss(), dts({ include: ['src'] })],
10
- build: {
11
- lib: {
12
- entry: {
13
- index: resolve(__dirname, 'src/index.ts'),
14
- lite: resolve(__dirname, 'src/lite.ts'),
15
- },
16
- name: 'LimrunUI',
17
- formats: ['es', 'cjs'],
18
- fileName: (format, entryName) => `${entryName}.${format === 'es' ? 'js' : 'cjs'}`,
19
- },
20
- rollupOptions: {
21
- external: ['react', 'react-dom', 'react/jsx-runtime'],
22
- output: {
23
- // Both entries share one stylesheet; keep its historical name so
24
- // consumers that reference dist/index.css directly keep working.
25
- assetFileNames: (info) =>
26
- info.names?.some((n) => n.endsWith('.css')) ? 'index.css' : '[name][extname]',
27
- globals: {
28
- react: 'React',
29
- 'react-dom': 'ReactDOM',
30
- 'react/jsx-runtime': 'jsxRuntime',
31
- },
32
- },
33
- },
34
- },
35
- });
package/vitest.config.ts DELETED
@@ -1,24 +0,0 @@
1
- import { defineConfig } from 'vitest/config';
2
-
3
- // Vitest config for @limrun/ui unit tests.
4
- //
5
- // We use jsdom as the default environment because the runtime code uses
6
- // browser globals (window.setTimeout, window.requestAnimationFrame, etc.).
7
- // Pure modules can opt back into the node env per-file via:
8
- //
9
- // // @vitest-environment node
10
- //
11
- // at the top of the test file.
12
- //
13
- // The actual <RemoteControl> component is intentionally NOT under unit
14
- // test here — its WebRTC plumbing is integration-tested via the demo +
15
- // staging instance. These tests cover the smaller, pure-logic pieces:
16
- // `core/ax-tree.ts`, `core/ax-fetcher.ts` and the URL helper exported from
17
- // `components/remote-control.tsx`.
18
- export default defineConfig({
19
- test: {
20
- environment: 'jsdom',
21
- include: ['src/**/*.test.ts', 'src/**/*.test.tsx'],
22
- globals: false,
23
- },
24
- });