@limrun/ui 0.17.0-rc.1 → 0.17.0-rc.3

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 (80) hide show
  1. package/README.md +18 -3
  2. package/dist/{GLTFLoader-DW6wpvyR.mjs → GLTFLoader-B4nnixLv.mjs} +1 -1
  3. package/dist/{GLTFLoader-BKnsBFsj.js → GLTFLoader-GATLvS0U.js} +1 -1
  4. package/dist/components/duo-fold-motion.d.ts +1 -1
  5. package/dist/components/duo-view.d.ts +5 -1
  6. package/dist/{duo-frame-DjfA4gG-.js → duo-frame-CfBILL32.js} +2 -2
  7. package/dist/{duo-frame-BzyYljix.mjs → duo-frame-F3hwc09B.mjs} +406 -406
  8. package/dist/duo-view-BsdKpp8d.mjs +662 -0
  9. package/dist/duo-view-Cy8kBdg9.js +1 -0
  10. package/dist/index.cjs +1 -1
  11. package/dist/index.css +1 -1
  12. package/dist/index.js +2 -2
  13. package/dist/lite.cjs +1 -1
  14. package/dist/lite.js +1 -1
  15. package/dist/{remote-control-CQjSEVDF.mjs → remote-control-BAmNHDdv.mjs} +109 -108
  16. package/dist/{remote-control-DVRXfCR2.js → remote-control-F3h8U5pg.js} +1 -1
  17. package/package.json +5 -1
  18. package/dist/demo.d.ts +0 -1
  19. package/dist/duo-view-COZg8ieG.js +0 -1
  20. package/dist/duo-view-o1yxIzxu.mjs +0 -654
  21. package/index.html +0 -199
  22. package/src/assets/Apple_logo_white.svg +0 -4
  23. package/src/assets/android_boot.gif +0 -0
  24. package/src/assets/android_boot.webp +0 -0
  25. package/src/assets/iphone16pro_black.webp +0 -0
  26. package/src/assets/iphone16pro_black_bg.webp +0 -0
  27. package/src/assets/iphone16pro_black_landscape.webp +0 -0
  28. package/src/assets/iphone16pro_black_landscape_bg.webp +0 -0
  29. package/src/assets/pixel9_black.webp +0 -0
  30. package/src/assets/pixel9_black_landscape.webp +0 -0
  31. package/src/assets/pixel_tablet_landscape.webp +0 -0
  32. package/src/assets/pixel_tablet_portrait.webp +0 -0
  33. package/src/components/device-install/index.ts +0 -1
  34. package/src/components/duo-button-hinge.test.tsx +0 -138
  35. package/src/components/duo-button-hinge.ts +0 -75
  36. package/src/components/duo-controls.tsx +0 -142
  37. package/src/components/duo-flat-frame.tsx +0 -145
  38. package/src/components/duo-flat-geometry.test.ts +0 -63
  39. package/src/components/duo-flat-geometry.ts +0 -97
  40. package/src/components/duo-fold-motion.test.tsx +0 -245
  41. package/src/components/duo-fold-motion.tsx +0 -192
  42. package/src/components/duo-fold-renderer.test.ts +0 -29
  43. package/src/components/duo-fold-renderer.ts +0 -397
  44. package/src/components/duo-fold-timing.test.ts +0 -44
  45. package/src/components/duo-fold-timing.ts +0 -9
  46. package/src/components/duo-frame.tsx +0 -579
  47. package/src/components/duo-hardware.test.ts +0 -61
  48. package/src/components/duo-hardware.ts +0 -61
  49. package/src/components/duo-model.test.ts +0 -243
  50. package/src/components/duo-model.ts +0 -607
  51. package/src/components/duo-screen-handoff.ts +0 -52
  52. package/src/components/duo-view.test.tsx +0 -104
  53. package/src/components/duo-view.tsx +0 -315
  54. package/src/components/inspect-overlay.css +0 -223
  55. package/src/components/inspect-overlay.tsx +0 -437
  56. package/src/components/remote-control-framed.tsx +0 -14
  57. package/src/components/remote-control.css +0 -441
  58. package/src/components/remote-control.test.ts +0 -34
  59. package/src/components/remote-control.tsx +0 -3989
  60. package/src/core/ax-fetcher.test.ts +0 -418
  61. package/src/core/ax-fetcher.ts +0 -387
  62. package/src/core/ax-tree.test.ts +0 -491
  63. package/src/core/ax-tree.ts +0 -416
  64. package/src/core/camera.test.ts +0 -155
  65. package/src/core/camera.ts +0 -133
  66. package/src/core/constants.ts +0 -344
  67. package/src/core/duo.test.ts +0 -142
  68. package/src/core/duo.ts +0 -156
  69. package/src/core/webrtc-messages.ts +0 -148
  70. package/src/demo.tsx +0 -283
  71. package/src/frame-assets.ts +0 -22
  72. package/src/hooks/index.ts +0 -1
  73. package/src/index.ts +0 -29
  74. package/src/lite.test.ts +0 -30
  75. package/src/lite.ts +0 -30
  76. package/src/vite-env.d.ts +0 -1
  77. package/tsconfig.json +0 -26
  78. package/tsconfig.node.json +0 -26
  79. package/vite.config.ts +0 -35
  80. 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
- }