@limrun/ui 0.17.0-rc.1 → 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 (83) hide show
  1. package/README.md +18 -3
  2. package/dist/{GLTFLoader-DW6wpvyR.mjs → GLTFLoader-Cami_vza.mjs} +1 -1
  3. package/dist/{GLTFLoader-BKnsBFsj.js → GLTFLoader-DeCLxW_q.js} +1 -1
  4. package/dist/components/duo-fold-motion.d.ts +1 -1
  5. package/dist/components/duo-loading.d.ts +8 -0
  6. package/dist/components/duo-view.d.ts +6 -1
  7. package/dist/{duo-frame-BzyYljix.mjs → duo-frame-Cmw4p2uN.mjs} +414 -414
  8. package/dist/{duo-frame-DjfA4gG-.js → duo-frame-iMtrvx_a.js} +18 -18
  9. package/dist/duo-view-BAh-w2Kz.js +1 -0
  10. package/dist/duo-view-D9Ae00rj.mjs +512 -0
  11. package/dist/index.cjs +1 -1
  12. package/dist/index.css +1 -1
  13. package/dist/index.js +2 -2
  14. package/dist/lite.cjs +1 -1
  15. package/dist/lite.js +1 -1
  16. package/dist/remote-control-B2jZpf3c.js +1 -0
  17. package/dist/remote-control-DiddxS8l.mjs +2883 -0
  18. package/package.json +5 -1
  19. package/dist/demo.d.ts +0 -1
  20. package/dist/duo-view-COZg8ieG.js +0 -1
  21. package/dist/duo-view-o1yxIzxu.mjs +0 -654
  22. package/dist/remote-control-CQjSEVDF.mjs +0 -2673
  23. package/dist/remote-control-DVRXfCR2.js +0 -1
  24. package/index.html +0 -199
  25. package/src/assets/Apple_logo_white.svg +0 -4
  26. package/src/assets/android_boot.gif +0 -0
  27. package/src/assets/android_boot.webp +0 -0
  28. package/src/assets/iphone16pro_black.webp +0 -0
  29. package/src/assets/iphone16pro_black_bg.webp +0 -0
  30. package/src/assets/iphone16pro_black_landscape.webp +0 -0
  31. package/src/assets/iphone16pro_black_landscape_bg.webp +0 -0
  32. package/src/assets/pixel9_black.webp +0 -0
  33. package/src/assets/pixel9_black_landscape.webp +0 -0
  34. package/src/assets/pixel_tablet_landscape.webp +0 -0
  35. package/src/assets/pixel_tablet_portrait.webp +0 -0
  36. package/src/components/device-install/index.ts +0 -1
  37. package/src/components/duo-button-hinge.test.tsx +0 -138
  38. package/src/components/duo-button-hinge.ts +0 -75
  39. package/src/components/duo-controls.tsx +0 -142
  40. package/src/components/duo-flat-frame.tsx +0 -145
  41. package/src/components/duo-flat-geometry.test.ts +0 -63
  42. package/src/components/duo-flat-geometry.ts +0 -97
  43. package/src/components/duo-fold-motion.test.tsx +0 -245
  44. package/src/components/duo-fold-motion.tsx +0 -192
  45. package/src/components/duo-fold-renderer.test.ts +0 -29
  46. package/src/components/duo-fold-renderer.ts +0 -397
  47. package/src/components/duo-fold-timing.test.ts +0 -44
  48. package/src/components/duo-fold-timing.ts +0 -9
  49. package/src/components/duo-frame.tsx +0 -579
  50. package/src/components/duo-hardware.test.ts +0 -61
  51. package/src/components/duo-hardware.ts +0 -61
  52. package/src/components/duo-model.test.ts +0 -243
  53. package/src/components/duo-model.ts +0 -607
  54. package/src/components/duo-screen-handoff.ts +0 -52
  55. package/src/components/duo-view.test.tsx +0 -104
  56. package/src/components/duo-view.tsx +0 -315
  57. package/src/components/inspect-overlay.css +0 -223
  58. package/src/components/inspect-overlay.tsx +0 -437
  59. package/src/components/remote-control-framed.tsx +0 -14
  60. package/src/components/remote-control.css +0 -441
  61. package/src/components/remote-control.test.ts +0 -34
  62. package/src/components/remote-control.tsx +0 -3989
  63. package/src/core/ax-fetcher.test.ts +0 -418
  64. package/src/core/ax-fetcher.ts +0 -387
  65. package/src/core/ax-tree.test.ts +0 -491
  66. package/src/core/ax-tree.ts +0 -416
  67. package/src/core/camera.test.ts +0 -155
  68. package/src/core/camera.ts +0 -133
  69. package/src/core/constants.ts +0 -344
  70. package/src/core/duo.test.ts +0 -142
  71. package/src/core/duo.ts +0 -156
  72. package/src/core/webrtc-messages.ts +0 -148
  73. package/src/demo.tsx +0 -283
  74. package/src/frame-assets.ts +0 -22
  75. package/src/hooks/index.ts +0 -1
  76. package/src/index.ts +0 -29
  77. package/src/lite.test.ts +0 -30
  78. package/src/lite.ts +0 -30
  79. package/src/vite-env.d.ts +0 -1
  80. package/tsconfig.json +0 -26
  81. package/tsconfig.node.json +0 -26
  82. package/vite.config.ts +0 -35
  83. package/vitest.config.ts +0 -24
@@ -1,104 +0,0 @@
1
- import React, { act, useEffect } from 'react';
2
- import { createRoot } from 'react-dom/client';
3
- import { afterEach, expect, it, vi } from 'vitest';
4
- import DuoView from './duo-view';
5
- import type { DuoFrameProps } from './duo-frame';
6
-
7
- const renderer = vi.hoisted(() => ({ loaded: vi.fn(), released: vi.fn() }));
8
- vi.mock('./duo-frame', () => {
9
- renderer.loaded();
10
- return {
11
- default: function Frame() {
12
- useEffect(() => () => renderer.released(), []);
13
- return <canvas />;
14
- },
15
- };
16
- });
17
- afterEach(() => {
18
- vi.restoreAllMocks();
19
- vi.unstubAllGlobals();
20
- });
21
-
22
- it('starts without a renderer, loads it on request and releases it when returning to native video', async () => {
23
- vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
24
- vi.stubGlobal(
25
- 'ResizeObserver',
26
- class {
27
- observe() {}
28
- disconnect() {}
29
- },
30
- );
31
- vi.spyOn(HTMLMediaElement.prototype, 'play').mockResolvedValue(undefined);
32
- vi.spyOn(HTMLMediaElement.prototype, 'pause').mockImplementation(() => undefined);
33
- const outer = document.createElement('video');
34
- const inner = document.createElement('video');
35
- outer.srcObject = {} as MediaStream;
36
- inner.srcObject = {} as MediaStream;
37
- const props: DuoFrameProps = {
38
- outer,
39
- inner,
40
- state: {
41
- angleDegrees: 0,
42
- minAngleDegrees: 0,
43
- maxAngleDegrees: 180,
44
- orientation: 'portrait',
45
- displays: [
46
- { id: 'outer', screenId: 1, trackId: 'outer', width: 1398, height: 2034, scale: 3, orientation: 1 },
47
- { id: 'inner', screenId: 2, trackId: 'inner', width: 2034, height: 2912, scale: 3, orientation: 3 },
48
- ],
49
- },
50
- setAngle: vi.fn().mockResolvedValue(undefined),
51
- setOrientation: vi.fn().mockResolvedValue(undefined),
52
- button: vi.fn().mockResolvedValue(undefined),
53
- touch: vi.fn(),
54
- onKeyDown: vi.fn(),
55
- onKeyUp: vi.fn(),
56
- };
57
- const host = document.createElement('div');
58
- const root = createRoot(host);
59
- const clickMode = async (label: string) => {
60
- await act(async () => {
61
- [...host.querySelectorAll('.rc-duo-mode button')]
62
- .find((b) => b.textContent === label)!
63
- .dispatchEvent(new MouseEvent('click', { bubbles: true }));
64
- });
65
- };
66
- try {
67
- await act(async () => root.render(<DuoView {...props} />));
68
- expect(renderer.loaded).not.toHaveBeenCalled();
69
- expect(host.querySelector('canvas')).toBeNull();
70
- expect(host.querySelector('.rc-duo-flat-frame')).not.toBeNull();
71
- expect(host.querySelectorAll('.rc-duo-physical')).toHaveLength(3);
72
- expect(host.querySelectorAll('.rc-duo-hardware')).toHaveLength(3);
73
- expect(host.querySelector('[aria-label="Rotate device"]')).not.toBeNull();
74
- expect(host.querySelector('input[type=range]')).not.toBeNull();
75
- expect(host.querySelector('video')!.srcObject).toBe(outer.srcObject);
76
- const press = async (selector: string, type: string) => {
77
- await act(async () => {
78
- host.querySelector(selector)!.dispatchEvent(new KeyboardEvent(type, { key: 'Enter', bubbles: true }));
79
- });
80
- };
81
- await press('.rc-duo-physical[data-duo-button=side]', 'keydown');
82
- await press('.rc-duo-physical[data-duo-button=side]', 'keyup');
83
- await press('.rc-duo-hardware[data-duo-button=volumeUp]', 'keydown');
84
- await press('.rc-duo-hardware[data-duo-button=volumeUp]', 'keyup');
85
- expect(props.button).toHaveBeenNthCalledWith(1, 'side', true);
86
- expect(props.button).toHaveBeenNthCalledWith(2, 'side', false);
87
- expect(props.button).toHaveBeenNthCalledWith(3, 'volumeUp', true);
88
- expect(props.button).toHaveBeenNthCalledWith(4, 'volumeUp', false);
89
- await press('.rc-duo-hardware[data-duo-button=volumeDown]', 'keydown');
90
- await clickMode('3D');
91
- expect(props.button).toHaveBeenLastCalledWith('volumeDown', false);
92
- expect(renderer.loaded).toHaveBeenCalledOnce();
93
- expect(host.querySelector('canvas')).not.toBeNull();
94
- await clickMode('2D');
95
- expect(renderer.released).toHaveBeenCalledOnce();
96
- expect(host.querySelector('canvas')).toBeNull();
97
- expect(host.querySelector('video')!.srcObject).toBe(outer.srcObject);
98
- expect(props.setAngle).not.toHaveBeenCalled();
99
- await act(async () => root.render(<DuoView {...props} state={{ ...props.state, angleDegrees: 180 }} />));
100
- expect(host.querySelector('video')!.srcObject).toBe(inner.srcObject);
101
- } finally {
102
- await act(async () => root.unmount());
103
- }
104
- });
@@ -1,315 +0,0 @@
1
- import React, { lazy, Suspense, useEffect, useRef, useState } from 'react';
2
- import { DUO_BUTTONS, DuoButtonSender, flatDisplayTouch, type DuoButton } from '../core/duo';
3
- import type { DuoFrameProps } from './duo-frame';
4
- import { DuoControls, DuoHardwareIcon, useDuoHinge } from './duo-controls';
5
- import { DuoFlatFrame } from './duo-flat-frame';
6
- import { flatFrameGeometry, flatHoverEdge } from './duo-flat-geometry';
7
- import { DuoFoldMotion, useDuoFoldMotion } from './duo-fold-motion';
8
-
9
- const DuoFrame = lazy(() => import('./duo-frame'));
10
-
11
- /** Native video is the default; only an explicit 3D selection loads the renderer and body asset. */
12
- export default function DuoView(props: DuoFrameProps) {
13
- const [mode, setMode] = useState<'2d' | '3d'>('2d');
14
- return (
15
- <div className="rc-duo-view">
16
- {mode === '3d' ?
17
- <Suspense fallback={<DuoFlat {...props} />}>
18
- <DuoFrame {...props} />
19
- </Suspense>
20
- : <DuoFlat {...props} />}
21
- <div className="rc-duo-mode" role="group" aria-label="Device rendering">
22
- <button type="button" aria-pressed={mode === '2d'} onClick={() => setMode('2d')}>
23
- 2D
24
- </button>
25
- <button type="button" aria-pressed={mode === '3d'} onClick={() => setMode('3d')}>
26
- 3D
27
- </button>
28
- </div>
29
- </div>
30
- );
31
- }
32
-
33
- function DuoFlat(props: DuoFrameProps) {
34
- const stage = useRef<HTMLDivElement>(null);
35
- const video = useRef<HTMLVideoElement>(null);
36
- const latest = useRef(props);
37
- latest.current = props;
38
- const [size, setSize] = useState({ width: 0, height: 0 });
39
- const [error, setError] = useState<string>();
40
- const [edge, setEdge] = useState<ReturnType<typeof flatHoverEdge>>();
41
- const [hovered, setHovered] = useState<DuoButton>();
42
- const [pressed, setPressed] = useState<DuoButton>();
43
- const held = useRef<{ pointer: number; screenId: number; x: number; y: number } | undefined>(undefined);
44
- const hardware = useRef<DuoButtonSender | null>(null);
45
- const display = props.state.displays.find(
46
- (d) => d.id === (props.state.angleDegrees < 90 ? 'outer' : 'inner'),
47
- );
48
- const source = display?.id === 'inner' ? props.inner : props.outer;
49
- const turns =
50
- display?.orientation === 2 ? 2
51
- : display?.orientation === 3 ? 1
52
- : display?.orientation === 4 ? 3
53
- : 0;
54
- const inner = display?.id === 'inner';
55
- const frame = flatFrameGeometry(inner, turns, size.width, size.height);
56
- const { bodyWidth, bodyHeight, screenWidth, screenHeight, scale } = frame;
57
- const width = screenWidth * scale;
58
- const height = screenHeight * scale;
59
-
60
- useEffect(() => {
61
- const observer = new ResizeObserver(([entry]) => {
62
- if (entry) setSize({ width: entry.contentRect.width, height: entry.contentRect.height });
63
- });
64
- if (stage.current) observer.observe(stage.current);
65
- return () => observer.disconnect();
66
- }, []);
67
-
68
- useEffect(() => {
69
- const target = video.current;
70
- if (!target || !source) return;
71
- const sync = () => {
72
- if (target.srcObject !== source.srcObject) target.srcObject = source.srcObject;
73
- void target.play().catch(() => undefined);
74
- props.outer?.pause();
75
- props.inner?.pause();
76
- };
77
- sync();
78
- source.addEventListener('loadedmetadata', sync);
79
- return () => {
80
- source.removeEventListener('loadedmetadata', sync);
81
- target.pause();
82
- target.srcObject = null;
83
- for (const original of [props.outer, props.inner]) void original?.play().catch(() => undefined);
84
- };
85
- }, [source, props.outer, props.inner]);
86
-
87
- const {
88
- motion,
89
- finish: finishMotion,
90
- prepare,
91
- } = useDuoFoldMotion(video, inner, frame, props.state.orientation);
92
- const { angle, changeAngle, interacting } = useDuoHinge(
93
- props.state.angleDegrees,
94
- async (value) => {
95
- prepare(value >= 90);
96
- try {
97
- await props.setAngle(value);
98
- } catch (reason) {
99
- finishMotion();
100
- throw reason;
101
- }
102
- },
103
- setError,
104
- );
105
-
106
- const releaseTouch = () => {
107
- if (!held.current) return;
108
- const { screenId, x, y } = held.current;
109
- latest.current.touch(1, screenId, x, y);
110
- held.current = undefined;
111
- };
112
- useEffect(() => {
113
- hardware.current = new DuoButtonSender(
114
- (button, down) => latest.current.button(button, down),
115
- (e) => setError(String(e)),
116
- );
117
- const release = () => {
118
- releaseTouch();
119
- hardware.current?.release();
120
- setPressed(undefined);
121
- };
122
- window.addEventListener('blur', release);
123
- return () => {
124
- release();
125
- window.removeEventListener('blur', release);
126
- };
127
- }, []);
128
- useEffect(() => {
129
- releaseTouch();
130
- }, [display?.screenId, turns]);
131
-
132
- const releaseButton = () => {
133
- hardware.current?.release();
134
- setPressed(undefined);
135
- };
136
- const buttonEvents = (button: DuoButton): React.ButtonHTMLAttributes<HTMLButtonElement> => ({
137
- onPointerDown: (event) => {
138
- if (motion || event.button !== 0) return;
139
- event.currentTarget.setPointerCapture(event.pointerId);
140
- hardware.current?.press(button);
141
- setPressed(button);
142
- },
143
- onPointerUp: releaseButton,
144
- onPointerCancel: releaseButton,
145
- onLostPointerCapture: releaseButton,
146
- onBlur: releaseButton,
147
- onPointerEnter: () => setHovered(button),
148
- onPointerLeave: () => setHovered(undefined),
149
- onFocus: () => setEdge(frame.guides.find((g) => g.button === button)?.edge),
150
- onKeyDown: (event) => {
151
- if (motion) return;
152
- if (event.key === 'Enter' || event.key === ' ') {
153
- event.preventDefault();
154
- if (!event.repeat) {
155
- hardware.current?.press(button);
156
- setPressed(button);
157
- }
158
- }
159
- },
160
- onKeyUp: (event) => {
161
- if (event.key === 'Enter' || event.key === ' ') {
162
- event.preventDefault();
163
- releaseButton();
164
- }
165
- },
166
- });
167
- const point = (event: React.PointerEvent<HTMLDivElement>) => {
168
- const rect = event.currentTarget.getBoundingClientRect();
169
- return flatDisplayTouch(
170
- (event.clientX - rect.left) / rect.width,
171
- (event.clientY - rect.top) / rect.height,
172
- turns,
173
- );
174
- };
175
- return (
176
- <div className="rc-duo">
177
- <div
178
- ref={stage}
179
- className="rc-duo-flat-stage"
180
- data-folding={!!motion}
181
- aria-busy={!!motion}
182
- onPointerMove={(event) => {
183
- const rect = event.currentTarget.getBoundingClientRect();
184
- setEdge(flatHoverEdge(event.clientX - rect.left, event.clientY - rect.top, frame.rect));
185
- }}
186
- onPointerLeave={() => {
187
- setEdge(undefined);
188
- setHovered(undefined);
189
- }}
190
- >
191
- <div
192
- className="rc-duo-flat-device"
193
- data-display={inner ? 'inner' : 'outer'}
194
- style={{
195
- width: bodyWidth * scale,
196
- height: bodyHeight * scale,
197
- transform: 'translate(-50%, -50%) rotate(' + frame.turns * 90 + 'deg)',
198
- }}
199
- >
200
- <DuoFlatFrame inner={inner} width={bodyWidth} height={bodyHeight} />
201
- <div
202
- className="rc-duo-flat-screen"
203
- role="application"
204
- aria-label="iPhone Duo 2D simulator"
205
- tabIndex={0}
206
- style={{
207
- width,
208
- height,
209
- left: ((bodyWidth - screenWidth) / 2) * scale,
210
- top: ((bodyHeight - screenHeight) / 2) * scale,
211
- borderRadius: inner ? 5.3 * scale : [0.8, 6.6, 6.6, 0.8].map((r) => r * scale + 'px').join(' '),
212
- }}
213
- onKeyDown={motion ? undefined : props.onKeyDown}
214
- onKeyUp={props.onKeyUp}
215
- onPointerDown={(event) => {
216
- if (motion || held.current || event.button !== 0 || !display) return;
217
- event.currentTarget.focus({ preventScroll: true });
218
- event.currentTarget.setPointerCapture(event.pointerId);
219
- const p = point(event);
220
- held.current = { pointer: event.pointerId, screenId: display.screenId, ...p };
221
- props.touch(0, display.screenId, p.x, p.y);
222
- }}
223
- onPointerMove={(event) => {
224
- if (held.current?.pointer !== event.pointerId) return;
225
- const p = point(event);
226
- Object.assign(held.current, p);
227
- props.touch(2, held.current.screenId, p.x, p.y);
228
- }}
229
- onPointerUp={(event) => {
230
- if (held.current?.pointer === event.pointerId) releaseTouch();
231
- }}
232
- onPointerCancel={releaseTouch}
233
- onLostPointerCapture={releaseTouch}
234
- >
235
- <video
236
- ref={video}
237
- autoPlay
238
- muted
239
- playsInline
240
- style={{
241
- width: inner ? height : width,
242
- height: inner ? width : height,
243
- transform: 'translate(-50%, -50%) rotate(' + (inner ? 90 : 0) + 'deg)',
244
- }}
245
- />
246
- </div>
247
- {frame.buttons.map((b) => (
248
- <button
249
- key={b.button}
250
- type="button"
251
- className="rc-duo-physical"
252
- data-duo-button={b.button}
253
- data-edge={b.edge}
254
- data-highlighted={hovered === b.button}
255
- data-pressed={pressed === b.button}
256
- aria-label={DUO_BUTTONS[b.button] + ' physical button'}
257
- title={DUO_BUTTONS[b.button]}
258
- style={{
259
- left: b.x * scale,
260
- top: b.y * scale,
261
- width: b.edge === 'top' ? Math.max(28, b.width * scale) : 28,
262
- height: b.edge === 'top' ? 28 : Math.max(28, b.height * scale),
263
- }}
264
- {...buttonEvents(b.button)}
265
- >
266
- <span style={{ width: Math.max(2, b.width * scale), height: Math.max(2, b.height * scale) }} />
267
- </button>
268
- ))}
269
- </div>
270
- {frame.guides.map((g) => (
271
- <button
272
- key={g.button}
273
- type="button"
274
- className="rc-duo-hardware"
275
- data-duo-button={g.button}
276
- data-visible={edge === g.edge}
277
- data-pressed={pressed === g.button}
278
- aria-label={DUO_BUTTONS[g.button]}
279
- title={DUO_BUTTONS[g.button]}
280
- style={
281
- {
282
- left: g.targetX,
283
- top: g.targetY,
284
- '--duo-hardware-icon-size': `${frame.iconSize}px`,
285
- '--duo-hardware-icon-x': `${g.x - g.targetX}px`,
286
- '--duo-hardware-icon-y': `${g.y - g.targetY}px`,
287
- } as React.CSSProperties
288
- }
289
- {...buttonEvents(g.button)}
290
- >
291
- <DuoHardwareIcon button={g.button} />
292
- </button>
293
- ))}
294
- {motion && <DuoFoldMotion motion={motion} onFinish={finishMotion} />}
295
- </div>
296
- <DuoControls
297
- angle={angle}
298
- changeAngle={changeAngle}
299
- interacting={interacting}
300
- rotate={() => {
301
- void props
302
- .setOrientation(props.state.orientation === 'portrait' ? 'landscape-left' : 'portrait')
303
- .catch((e) => setError(String(e)));
304
- }}
305
- >
306
- <span className="rc-duo-view-label">Front view</span>
307
- </DuoControls>
308
- {error && (
309
- <div role="alert" className="rc-duo-error">
310
- {error}
311
- </div>
312
- )}
313
- </div>
314
- );
315
- }
@@ -1,223 +0,0 @@
1
- .rc-inspect-overlay {
2
- position: absolute;
3
- z-index: 25;
4
- pointer-events: none;
5
- overflow: hidden;
6
- }
7
-
8
- .rc-inspect-overlay-select {
9
- /* When click-to-select is enabled, the container also captures clicks
10
- that fall outside any box so we can clear selection. */
11
- pointer-events: auto;
12
- }
13
-
14
- .rc-inspect-box {
15
- position: absolute;
16
- box-sizing: border-box;
17
- border: 1px solid rgba(64, 169, 255, 0.55);
18
- background: rgba(64, 169, 255, 0.06);
19
- border-radius: 2px;
20
- cursor: pointer;
21
- pointer-events: none;
22
- transition:
23
- border-color 80ms ease,
24
- border-width 80ms ease,
25
- background-color 80ms ease;
26
- padding: 0;
27
- margin: 0;
28
- font: inherit;
29
- color: inherit;
30
- outline: none;
31
- }
32
-
33
- .rc-inspect-overlay-select .rc-inspect-box {
34
- pointer-events: auto;
35
- }
36
-
37
- .rc-inspect-box-disabled {
38
- opacity: 0.55;
39
- }
40
-
41
- .rc-inspect-box[data-highlighted='true'] {
42
- border-color: rgba(255, 197, 28, 0.95);
43
- border-width: 1.5px;
44
- background: rgba(255, 197, 28, 0.16);
45
- z-index: 1;
46
- }
47
-
48
- .rc-inspect-box[data-selected='true'] {
49
- border-color: rgba(0, 122, 255, 1);
50
- border-width: 2px;
51
- background: rgba(0, 122, 255, 0.22);
52
- z-index: 2;
53
- }
54
-
55
- .rc-inspect-card {
56
- /* Card is rendered via React portal to document.body so it can escape
57
- any ancestor overflow:hidden (e.g. customer-provided .device-wrapper
58
- in our demo). Coordinates passed in via inline style are
59
- viewport-absolute. */
60
- position: fixed;
61
- z-index: 2147483600;
62
- pointer-events: auto;
63
- background: rgba(22, 24, 30, 0.97);
64
- color: #f5f5f7;
65
- border: 1px solid rgba(255, 255, 255, 0.1);
66
- border-radius: 10px;
67
- padding: 9px 11px 11px;
68
- min-width: 200px;
69
- max-width: 260px;
70
- /* Cap card height. Anything longer scrolls inside the card instead of
71
- pushing the viewport. */
72
- max-height: 220px;
73
- overflow-y: auto;
74
- box-shadow:
75
- 0 12px 36px rgba(0, 0, 0, 0.4),
76
- 0 1px 0 rgba(255, 255, 255, 0.04) inset;
77
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
78
- font-size: 12px;
79
- line-height: 1.4;
80
- backdrop-filter: blur(8px);
81
- -webkit-backdrop-filter: blur(8px);
82
- scrollbar-width: thin;
83
- }
84
-
85
- .rc-inspect-card-header {
86
- display: flex;
87
- align-items: center;
88
- gap: 5px;
89
- flex-wrap: wrap;
90
- margin-bottom: 4px;
91
- }
92
-
93
- .rc-inspect-card-role {
94
- font-size: 10px;
95
- font-weight: 600;
96
- text-transform: uppercase;
97
- letter-spacing: 0.06em;
98
- color: rgba(245, 245, 247, 0.55);
99
- display: inline-block;
100
- white-space: nowrap;
101
- }
102
-
103
- .rc-inspect-card-tag {
104
- font-size: 9px;
105
- font-weight: 600;
106
- text-transform: uppercase;
107
- letter-spacing: 0.05em;
108
- padding: 1px 5px;
109
- border-radius: 4px;
110
- background: rgba(255, 255, 255, 0.08);
111
- color: rgba(245, 245, 247, 0.7);
112
- border: 1px solid rgba(255, 255, 255, 0.06);
113
- }
114
-
115
- .rc-inspect-card-tag-blue {
116
- background: rgba(10, 132, 255, 0.18);
117
- border-color: rgba(10, 132, 255, 0.45);
118
- color: rgb(140, 195, 255);
119
- }
120
-
121
- .rc-inspect-card-title {
122
- font-size: 12.5px;
123
- font-weight: 600;
124
- color: #fff;
125
- margin: 0 0 7px;
126
- word-break: break-word;
127
- /* Clamp to 2 lines so a paragraph-long AXLabel doesn't take over. The
128
- full text is available on hover via the title attribute and via the
129
- value/id rows below. */
130
- display: -webkit-box;
131
- -webkit-line-clamp: 2;
132
- line-clamp: 2;
133
- -webkit-box-orient: vertical;
134
- overflow: hidden;
135
- }
136
-
137
- .rc-inspect-card-row {
138
- display: flex;
139
- gap: 6px;
140
- align-items: baseline;
141
- font-size: 10.5px;
142
- color: rgba(245, 245, 247, 0.72);
143
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
144
- monospace;
145
- margin: 1px 0;
146
- min-width: 0;
147
- }
148
-
149
- .rc-inspect-card-row-label {
150
- color: rgba(245, 245, 247, 0.42);
151
- font-weight: 500;
152
- flex-shrink: 0;
153
- }
154
-
155
- .rc-inspect-card-row-value {
156
- flex: 1 1 auto;
157
- min-width: 0;
158
- word-break: break-word;
159
- /* Clamp long values (selectors, IDs) to two lines, full value visible
160
- via the row's title attr or via "Copy" action. */
161
- display: -webkit-box;
162
- -webkit-line-clamp: 2;
163
- line-clamp: 2;
164
- -webkit-box-orient: vertical;
165
- overflow: hidden;
166
- }
167
-
168
- .rc-inspect-card-actions {
169
- display: flex;
170
- flex-wrap: wrap;
171
- gap: 6px;
172
- margin-top: 10px;
173
- padding-top: 9px;
174
- border-top: 1px solid rgba(255, 255, 255, 0.08);
175
- }
176
-
177
- .rc-inspect-card-btn {
178
- appearance: none;
179
- border: 1px solid rgba(255, 255, 255, 0.12);
180
- background: rgba(255, 255, 255, 0.06);
181
- color: #f5f5f7;
182
- font: inherit;
183
- font-size: 11px;
184
- font-weight: 500;
185
- padding: 4px 9px;
186
- border-radius: 6px;
187
- cursor: pointer;
188
- transition:
189
- background-color 100ms ease,
190
- border-color 100ms ease,
191
- color 100ms ease;
192
- }
193
-
194
- .rc-inspect-card-btn:hover:not(:disabled) {
195
- background: rgba(255, 255, 255, 0.12);
196
- border-color: rgba(255, 255, 255, 0.2);
197
- }
198
-
199
- .rc-inspect-card-btn:active:not(:disabled) {
200
- background: rgba(255, 255, 255, 0.16);
201
- }
202
-
203
- .rc-inspect-card-btn:disabled {
204
- opacity: 0.4;
205
- cursor: not-allowed;
206
- }
207
-
208
- .rc-inspect-card-btn-primary {
209
- background: rgba(0, 122, 255, 0.85);
210
- border-color: rgba(0, 122, 255, 0.9);
211
- color: #fff;
212
- }
213
-
214
- .rc-inspect-card-btn-primary:hover:not(:disabled) {
215
- background: rgba(10, 132, 255, 1);
216
- border-color: rgba(10, 132, 255, 1);
217
- }
218
-
219
- .rc-inspect-card-btn-copied {
220
- background: rgba(48, 209, 88, 0.22);
221
- border-color: rgba(48, 209, 88, 0.55);
222
- color: rgb(159, 240, 178);
223
- }