@limrun/ui 0.9.0-rc.5 → 0.9.0-rc.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/components/device-install/device-install-dialog.d.ts +5 -0
- package/dist/components/device-install/index.d.ts +2 -0
- package/dist/components/inspect-overlay.d.ts +1 -0
- package/dist/components/remote-control.d.ts +13 -2
- package/dist/core/ax-tree.d.ts +2 -0
- package/dist/core/device-install/apple/client.d.ts +17 -0
- package/dist/core/device-install/apple/crypto.d.ts +20 -0
- package/dist/core/device-install/apple/gsa-srp.d.ts +26 -0
- package/dist/core/device-install/apple/index.d.ts +5 -0
- package/dist/core/device-install/apple/provisioning.d.ts +161 -0
- package/dist/core/device-install/apple/relay.d.ts +29 -0
- package/dist/core/device-install/index.d.ts +4 -0
- package/dist/core/device-install/operations/index.d.ts +6 -0
- package/dist/core/device-install/operations/limbuild-client.d.ts +28 -0
- package/dist/core/device-install/operations/operations.d.ts +32 -0
- package/dist/core/device-install/operations/relay-client.d.ts +25 -0
- package/dist/core/device-install/operations/relay-protocol.d.ts +27 -0
- package/dist/core/device-install/operations/usbmux.d.ts +32 -0
- package/dist/core/device-install/operations/webusb.d.ts +21 -0
- package/dist/core/device-install/storage/browser-storage.d.ts +44 -0
- package/dist/core/device-install/storage/index.d.ts +1 -0
- package/dist/core/device-install/types.d.ts +48 -0
- package/dist/device-install/index.cjs +1 -0
- package/dist/device-install/index.d.ts +3 -0
- package/dist/device-install/index.js +78 -0
- package/dist/device-install/react.cjs +1 -0
- package/dist/device-install/react.d.ts +1 -0
- package/dist/device-install/react.js +4 -0
- package/dist/device-install-dialog-86RDdoK9.js +2 -0
- package/dist/device-install-dialog-CnyDWf0q.mjs +462 -0
- package/dist/device-install-dialog.css +1 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/use-device-install.d.ts +73 -0
- package/dist/index.cjs +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +737 -703
- package/dist/use-device-install-CbGVvwPp.js +31 -0
- package/dist/use-device-install-j1Gekpl4.mjs +13623 -0
- package/package.json +15 -2
- package/src/components/device-install/device-install-dialog.css +325 -0
- package/src/components/device-install/device-install-dialog.tsx +513 -0
- package/src/components/device-install/index.ts +2 -0
- package/src/components/inspect-overlay.css +6 -0
- package/src/components/inspect-overlay.tsx +46 -15
- package/src/components/remote-control.tsx +16 -2
- package/src/core/ax-tree.test.ts +124 -0
- package/src/core/ax-tree.ts +107 -0
- package/src/core/device-install/apple/client.ts +152 -0
- package/src/core/device-install/apple/crypto.ts +202 -0
- package/src/core/device-install/apple/gsa-srp.ts +127 -0
- package/src/core/device-install/apple/index.ts +5 -0
- package/src/core/device-install/apple/provisioning.ts +298 -0
- package/src/core/device-install/apple/relay.ts +221 -0
- package/src/core/device-install/index.ts +4 -0
- package/src/core/device-install/operations/index.ts +6 -0
- package/src/core/device-install/operations/limbuild-client.ts +104 -0
- package/src/core/device-install/operations/operations.ts +217 -0
- package/src/core/device-install/operations/relay-client.ts +255 -0
- package/src/core/device-install/operations/relay-protocol.ts +71 -0
- package/src/core/device-install/operations/usbmux.ts +270 -0
- package/src/core/device-install/operations/webusb-dom.d.ts +54 -0
- package/src/core/device-install/operations/webusb.ts +105 -0
- package/src/core/device-install/storage/browser-storage.ts +263 -0
- package/src/core/device-install/storage/index.ts +1 -0
- package/src/core/device-install/types.ts +65 -0
- package/src/device-install/index.ts +3 -0
- package/src/device-install/react.ts +1 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/use-device-install.ts +1210 -0
- package/src/index.ts +4 -0
- package/vite.config.ts +6 -2
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
import { useEffect, useId, useState, type ChangeEvent, type ReactNode } from 'react';
|
|
2
|
+
import { clsx } from 'clsx';
|
|
3
|
+
import { useDeviceInstall, type UseDeviceInstallOptions } from '../../hooks/use-device-install';
|
|
4
|
+
import type { DeviceInstallStep, DeviceInstallStepStatus } from '../../core/device-install';
|
|
5
|
+
import './device-install-dialog.css';
|
|
6
|
+
|
|
7
|
+
export type DeviceInstallDialogProps = UseDeviceInstallOptions & {
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const steps: Array<{ id: DeviceInstallStep; title: string; description: string }> = [
|
|
12
|
+
{
|
|
13
|
+
id: 'signing',
|
|
14
|
+
title: 'Prepare signing',
|
|
15
|
+
description: 'Choose Apple ID login or upload certificates, then confirm the target developer device.',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'connect',
|
|
19
|
+
title: 'Connect and pair',
|
|
20
|
+
description: 'Connect the iPhone with WebUSB, then pair this browser so installs can use the device.',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: 'build',
|
|
24
|
+
title: 'Check and build',
|
|
25
|
+
description: 'Verify the device and provisioning profile are ready, then start the signed build.',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'install',
|
|
29
|
+
title: 'Start installation',
|
|
30
|
+
description: 'Relay the last successful device build to the paired iPhone.',
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
type SigningSection = 'apple-id' | 'upload';
|
|
35
|
+
|
|
36
|
+
export function DeviceInstallDialog({
|
|
37
|
+
disabled,
|
|
38
|
+
...hookOptions
|
|
39
|
+
}: DeviceInstallDialogProps) {
|
|
40
|
+
const [open, setOpen] = useState(false);
|
|
41
|
+
const [openStep, setOpenStep] = useState<DeviceInstallStep>('signing');
|
|
42
|
+
const [signingSection, setSigningSection] = useState<SigningSection>();
|
|
43
|
+
const [appleAccountName, setAppleAccountName] = useState('');
|
|
44
|
+
const [applePassword, setApplePassword] = useState('');
|
|
45
|
+
const [appleTwoFactorCode, setAppleTwoFactorCode] = useState('');
|
|
46
|
+
const dialogTitleId = useId();
|
|
47
|
+
const deviceInstall = useDeviceInstall(hookOptions);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
setOpenStep(deviceInstall.currentStep);
|
|
51
|
+
}, [deviceInstall.currentStep]);
|
|
52
|
+
|
|
53
|
+
const updateSigningFiles = (field: 'certificateFile' | 'provisioningProfileFile', event: ChangeEvent<HTMLInputElement>) => {
|
|
54
|
+
deviceInstall.setSigningFiles({
|
|
55
|
+
[field]: event.currentTarget.files?.[0],
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div className="lr-device-install">
|
|
61
|
+
<button
|
|
62
|
+
type="button"
|
|
63
|
+
className="lr-device-install__trigger"
|
|
64
|
+
disabled={disabled || !hookOptions.apiUrl}
|
|
65
|
+
onClick={() => setOpen(true)}
|
|
66
|
+
>
|
|
67
|
+
Install to iPhone
|
|
68
|
+
</button>
|
|
69
|
+
|
|
70
|
+
{open && (
|
|
71
|
+
<div className="lr-device-install__backdrop" role="presentation">
|
|
72
|
+
<section
|
|
73
|
+
aria-labelledby={dialogTitleId}
|
|
74
|
+
aria-modal="true"
|
|
75
|
+
className="lr-device-install__dialog"
|
|
76
|
+
role="dialog"
|
|
77
|
+
>
|
|
78
|
+
<header className="lr-device-install__header">
|
|
79
|
+
<div>
|
|
80
|
+
<h2 id={dialogTitleId}>Install to a real iPhone</h2>
|
|
81
|
+
<p>Prepare signing, connect and pair the device, build, then install from this browser.</p>
|
|
82
|
+
</div>
|
|
83
|
+
<button type="button" className="lr-device-install__icon-button" onClick={() => setOpen(false)}>
|
|
84
|
+
Close
|
|
85
|
+
</button>
|
|
86
|
+
</header>
|
|
87
|
+
|
|
88
|
+
{deviceInstall.error && <div className="lr-device-install__error">{deviceInstall.error}</div>}
|
|
89
|
+
|
|
90
|
+
<div className="lr-device-install__steps">
|
|
91
|
+
{steps.map((step, index) => (
|
|
92
|
+
<StepCard
|
|
93
|
+
key={step.id}
|
|
94
|
+
index={index + 1}
|
|
95
|
+
step={step}
|
|
96
|
+
active={deviceInstall.currentStep === step.id}
|
|
97
|
+
open={openStep === step.id}
|
|
98
|
+
status={deviceInstall.stepStatuses[step.id]}
|
|
99
|
+
onToggle={() => setOpenStep(step.id)}
|
|
100
|
+
>
|
|
101
|
+
{step.id === 'signing' && (
|
|
102
|
+
<div className="lr-device-install__step-body">
|
|
103
|
+
<div className="lr-device-install__choice-grid">
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
className={clsx(
|
|
107
|
+
'lr-device-install__choice',
|
|
108
|
+
signingSection === 'apple-id' && 'lr-device-install__choice--active',
|
|
109
|
+
)}
|
|
110
|
+
onClick={() => setSigningSection('apple-id')}
|
|
111
|
+
>
|
|
112
|
+
<strong>Apple ID login</strong>
|
|
113
|
+
<span>Sign in, choose team, bundle ID, devices, then generate signing assets.</span>
|
|
114
|
+
</button>
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
className={clsx(
|
|
118
|
+
'lr-device-install__choice',
|
|
119
|
+
signingSection === 'upload' && 'lr-device-install__choice--active',
|
|
120
|
+
)}
|
|
121
|
+
onClick={() => setSigningSection('upload')}
|
|
122
|
+
>
|
|
123
|
+
<strong>Upload certificates</strong>
|
|
124
|
+
<span>Use an existing .p12 certificate and provisioning profile.</span>
|
|
125
|
+
</button>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{signingSection === 'apple-id' && (
|
|
129
|
+
<div className="lr-device-install__section-panel">
|
|
130
|
+
<div className="lr-device-install__grid">
|
|
131
|
+
<label className="lr-device-install__field">
|
|
132
|
+
<span>Apple ID</span>
|
|
133
|
+
<input
|
|
134
|
+
type="email"
|
|
135
|
+
autoComplete="username"
|
|
136
|
+
placeholder="name@example.com"
|
|
137
|
+
value={appleAccountName}
|
|
138
|
+
onChange={(event) => setAppleAccountName(event.currentTarget.value)}
|
|
139
|
+
/>
|
|
140
|
+
</label>
|
|
141
|
+
<label className="lr-device-install__field">
|
|
142
|
+
<span>Apple ID password</span>
|
|
143
|
+
<input
|
|
144
|
+
type="password"
|
|
145
|
+
autoComplete="current-password"
|
|
146
|
+
placeholder="Password stays in this browser"
|
|
147
|
+
value={applePassword}
|
|
148
|
+
onChange={(event) => setApplePassword(event.currentTarget.value)}
|
|
149
|
+
/>
|
|
150
|
+
</label>
|
|
151
|
+
{!deviceInstall.hasReusableAppleCertificate && (
|
|
152
|
+
<label className="lr-device-install__field">
|
|
153
|
+
<span>Generated .p12 password</span>
|
|
154
|
+
<input
|
|
155
|
+
type="password"
|
|
156
|
+
placeholder="Used when exporting Apple certificate"
|
|
157
|
+
onChange={(event) =>
|
|
158
|
+
deviceInstall.setSigningFiles({ certificatePassword: event.currentTarget.value })
|
|
159
|
+
}
|
|
160
|
+
/>
|
|
161
|
+
</label>
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
<div className="lr-device-install__actions">
|
|
165
|
+
<button
|
|
166
|
+
type="button"
|
|
167
|
+
className="lr-device-install__secondary"
|
|
168
|
+
disabled={
|
|
169
|
+
disabled ||
|
|
170
|
+
!hookOptions.apiUrl ||
|
|
171
|
+
!appleAccountName ||
|
|
172
|
+
!applePassword ||
|
|
173
|
+
deviceInstall.busyAction === 'signing'
|
|
174
|
+
}
|
|
175
|
+
onClick={() =>
|
|
176
|
+
void deviceInstall.startAppleIDLogin({
|
|
177
|
+
accountName: appleAccountName,
|
|
178
|
+
password: applePassword,
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
>
|
|
182
|
+
{deviceInstall.appleSigningStatus === 'authenticating'
|
|
183
|
+
? 'Signing in...'
|
|
184
|
+
: 'Sign in with Apple ID'}
|
|
185
|
+
</button>
|
|
186
|
+
<span className="lr-device-install__hint">
|
|
187
|
+
Apple password is used only by browser-side SRP. Status: {deviceInstall.appleSigningStatus}
|
|
188
|
+
</span>
|
|
189
|
+
</div>
|
|
190
|
+
{deviceInstall.appleSigningStatus === 'two-factor-required' && (
|
|
191
|
+
<div className="lr-device-install__grid">
|
|
192
|
+
<label className="lr-device-install__field">
|
|
193
|
+
<span>Two-factor code</span>
|
|
194
|
+
<input
|
|
195
|
+
type="text"
|
|
196
|
+
inputMode="numeric"
|
|
197
|
+
autoComplete="one-time-code"
|
|
198
|
+
value={appleTwoFactorCode}
|
|
199
|
+
onChange={(event) => setAppleTwoFactorCode(event.currentTarget.value)}
|
|
200
|
+
/>
|
|
201
|
+
</label>
|
|
202
|
+
<button
|
|
203
|
+
type="button"
|
|
204
|
+
className="lr-device-install__secondary"
|
|
205
|
+
disabled={!appleTwoFactorCode || deviceInstall.busyAction === 'signing'}
|
|
206
|
+
onClick={() => void deviceInstall.submitAppleTwoFactorCode(appleTwoFactorCode)}
|
|
207
|
+
>
|
|
208
|
+
Submit Apple ID code
|
|
209
|
+
</button>
|
|
210
|
+
</div>
|
|
211
|
+
)}
|
|
212
|
+
{deviceInstall.appleTeams.length > 0 && (
|
|
213
|
+
<label className="lr-device-install__field">
|
|
214
|
+
<span>Apple Developer team</span>
|
|
215
|
+
<select
|
|
216
|
+
value={deviceInstall.selectedAppleTeamID ?? ''}
|
|
217
|
+
onChange={(event) =>
|
|
218
|
+
deviceInstall.setSelectedAppleTeamID(event.currentTarget.value || undefined)
|
|
219
|
+
}
|
|
220
|
+
>
|
|
221
|
+
{deviceInstall.appleTeams.map((team, index) => {
|
|
222
|
+
const teamID =
|
|
223
|
+
team.teamId ??
|
|
224
|
+
(team.providerId === undefined ? undefined : String(team.providerId)) ??
|
|
225
|
+
team.publicProviderId ??
|
|
226
|
+
'';
|
|
227
|
+
return (
|
|
228
|
+
<option key={`${teamID}-${index}`} value={teamID}>
|
|
229
|
+
{team.name ?? 'Apple Developer Team'} {teamID ? `(${teamID})` : ''}
|
|
230
|
+
</option>
|
|
231
|
+
);
|
|
232
|
+
})}
|
|
233
|
+
</select>
|
|
234
|
+
</label>
|
|
235
|
+
)}
|
|
236
|
+
{deviceInstall.appleDevices.length > 0 && (
|
|
237
|
+
<label className="lr-device-install__field">
|
|
238
|
+
<span>Apple Developer devices</span>
|
|
239
|
+
<select
|
|
240
|
+
multiple
|
|
241
|
+
value={deviceInstall.selectedAppleDeviceIDs}
|
|
242
|
+
onChange={(event) =>
|
|
243
|
+
deviceInstall.setSelectedAppleDeviceIDs(
|
|
244
|
+
Array.from(event.currentTarget.selectedOptions).map((option) => option.value),
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
>
|
|
248
|
+
{deviceInstall.appleDevices.map((appleDevice) => (
|
|
249
|
+
<option
|
|
250
|
+
key={appleDevice.deviceId ?? appleDevice.deviceNumber}
|
|
251
|
+
value={appleDevice.deviceId ?? ''}
|
|
252
|
+
>
|
|
253
|
+
{appleDevice.name ?? appleDevice.model ?? 'Apple device'} {appleDevice.deviceNumber ?? ''}
|
|
254
|
+
</option>
|
|
255
|
+
))}
|
|
256
|
+
</select>
|
|
257
|
+
</label>
|
|
258
|
+
)}
|
|
259
|
+
{deviceInstall.applePortalSummary && (
|
|
260
|
+
<p className="lr-device-install__hint">
|
|
261
|
+
Found {deviceInstall.applePortalSummary.certificateCount} certificates and{' '}
|
|
262
|
+
{deviceInstall.applePortalSummary.profileCount} provisioning profiles.
|
|
263
|
+
</p>
|
|
264
|
+
)}
|
|
265
|
+
{deviceInstall.hasReusableAppleCertificate && (
|
|
266
|
+
<p className="lr-device-install__hint">
|
|
267
|
+
Reusing the certificate and private key stored in this browser.
|
|
268
|
+
</p>
|
|
269
|
+
)}
|
|
270
|
+
<button
|
|
271
|
+
type="button"
|
|
272
|
+
className="lr-device-install__primary"
|
|
273
|
+
disabled={disabled || !deviceInstall.canPrepareAppleSigningAssets}
|
|
274
|
+
onClick={() => void deviceInstall.prepareAppleSigningAssets()}
|
|
275
|
+
>
|
|
276
|
+
{deviceInstall.appleSigningStatus === 'preparing-assets'
|
|
277
|
+
? 'Preparing signing assets...'
|
|
278
|
+
: 'Generate certificate and profile'}
|
|
279
|
+
</button>
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
282
|
+
|
|
283
|
+
{signingSection === 'upload' && (
|
|
284
|
+
<div className="lr-device-install__section-panel">
|
|
285
|
+
<div className="lr-device-install__grid">
|
|
286
|
+
<label className="lr-device-install__field">
|
|
287
|
+
<span>Certificate (.p12)</span>
|
|
288
|
+
<input
|
|
289
|
+
type="file"
|
|
290
|
+
accept=".p12,application/x-pkcs12"
|
|
291
|
+
onChange={(event) => updateSigningFiles('certificateFile', event)}
|
|
292
|
+
/>
|
|
293
|
+
</label>
|
|
294
|
+
<label className="lr-device-install__field">
|
|
295
|
+
<span>Provisioning profile</span>
|
|
296
|
+
<input
|
|
297
|
+
type="file"
|
|
298
|
+
accept=".mobileprovision"
|
|
299
|
+
onChange={(event) => updateSigningFiles('provisioningProfileFile', event)}
|
|
300
|
+
/>
|
|
301
|
+
</label>
|
|
302
|
+
<label className="lr-device-install__field">
|
|
303
|
+
<span>Uploaded .p12 password</span>
|
|
304
|
+
<input
|
|
305
|
+
type="password"
|
|
306
|
+
placeholder="Export password"
|
|
307
|
+
onChange={(event) =>
|
|
308
|
+
deviceInstall.setSigningFiles({ certificatePassword: event.currentTarget.value })
|
|
309
|
+
}
|
|
310
|
+
/>
|
|
311
|
+
</label>
|
|
312
|
+
</div>
|
|
313
|
+
<p className="lr-device-install__hint">
|
|
314
|
+
The provisioning profile will be checked against the connected iPhone before the build starts.
|
|
315
|
+
</p>
|
|
316
|
+
</div>
|
|
317
|
+
)}
|
|
318
|
+
|
|
319
|
+
{deviceInstall.hasSigningAssets && (
|
|
320
|
+
<p>Signing assets are stored in this browser for the selected bundle and device.</p>
|
|
321
|
+
)}
|
|
322
|
+
</div>
|
|
323
|
+
)}
|
|
324
|
+
|
|
325
|
+
{step.id === 'connect' && (
|
|
326
|
+
<div className="lr-device-install__step-body">
|
|
327
|
+
<p>
|
|
328
|
+
WebUSB works in Chromium browsers on secure origins. Connect the iPhone over USB, approve the
|
|
329
|
+
browser permission prompt, then pair this browser.
|
|
330
|
+
</p>
|
|
331
|
+
<div className="lr-device-install__actions">
|
|
332
|
+
<button
|
|
333
|
+
type="button"
|
|
334
|
+
className="lr-device-install__primary"
|
|
335
|
+
disabled={disabled || !deviceInstall.canRequestUSBAccess}
|
|
336
|
+
onClick={() => void deviceInstall.requestUSBAccess()}
|
|
337
|
+
>
|
|
338
|
+
{deviceInstall.busyAction === 'usb' ? 'Selecting iPhone...' : 'Allow USB access'}
|
|
339
|
+
</button>
|
|
340
|
+
<button
|
|
341
|
+
type="button"
|
|
342
|
+
className="lr-device-install__secondary"
|
|
343
|
+
disabled={disabled || !deviceInstall.canPairBrowser}
|
|
344
|
+
onClick={() => void deviceInstall.pairBrowser()}
|
|
345
|
+
>
|
|
346
|
+
{deviceInstall.busyAction === 'pair'
|
|
347
|
+
? 'Pairing...'
|
|
348
|
+
: deviceInstall.pairConfirmationRequired
|
|
349
|
+
? 'Confirm pair record'
|
|
350
|
+
: 'Pair browser'}
|
|
351
|
+
</button>
|
|
352
|
+
</div>
|
|
353
|
+
{deviceInstall.device && (
|
|
354
|
+
<div className="lr-device-install__device">
|
|
355
|
+
{`${deviceInstall.device.productName ?? 'iPhone'} ${
|
|
356
|
+
deviceInstall.device.serialNumber ?? ''
|
|
357
|
+
}`.trim()}
|
|
358
|
+
</div>
|
|
359
|
+
)}
|
|
360
|
+
{deviceInstall.pairConfirmationRequired && (
|
|
361
|
+
<p>
|
|
362
|
+
Unlock the iPhone and tap <strong>Trust</strong> in the system dialog, then confirm the pair
|
|
363
|
+
record.
|
|
364
|
+
</p>
|
|
365
|
+
)}
|
|
366
|
+
<p>
|
|
367
|
+
{deviceInstall.hasPairRecord
|
|
368
|
+
? 'Pair record is stored locally. Continue to the build check.'
|
|
369
|
+
: 'Pair this browser once before building and installing.'}
|
|
370
|
+
</p>
|
|
371
|
+
</div>
|
|
372
|
+
)}
|
|
373
|
+
|
|
374
|
+
{step.id === 'build' && (
|
|
375
|
+
<div className="lr-device-install__step-body">
|
|
376
|
+
<div className="lr-device-install__checklist">
|
|
377
|
+
<StatusLine label="Signing assets" ready={deviceInstall.hasSigningInputs} pendingText="Ready to verify" />
|
|
378
|
+
<StatusLine label="USB device" ready={!!deviceInstall.device} />
|
|
379
|
+
<StatusLine label="Pair record" ready={deviceInstall.hasPairRecord} />
|
|
380
|
+
<StatusLine
|
|
381
|
+
label="Profile includes connected device"
|
|
382
|
+
ready={deviceInstall.connectedDeviceInProfile}
|
|
383
|
+
pendingText="Checked when the build starts"
|
|
384
|
+
/>
|
|
385
|
+
</div>
|
|
386
|
+
{deviceInstall.device &&
|
|
387
|
+
deviceInstall.appleTeams.length > 0 &&
|
|
388
|
+
!deviceInstall.connectedAppleDeviceRegistered && (
|
|
389
|
+
<button
|
|
390
|
+
type="button"
|
|
391
|
+
className="lr-device-install__secondary"
|
|
392
|
+
disabled={disabled || !!deviceInstall.busyAction}
|
|
393
|
+
onClick={() => void deviceInstall.registerConnectedAppleDevice()}
|
|
394
|
+
>
|
|
395
|
+
Register connected iPhone
|
|
396
|
+
</button>
|
|
397
|
+
)}
|
|
398
|
+
<button
|
|
399
|
+
type="button"
|
|
400
|
+
className="lr-device-install__primary"
|
|
401
|
+
disabled={disabled || !deviceInstall.canBuild}
|
|
402
|
+
onClick={() => void deviceInstall.startDeviceBuild()}
|
|
403
|
+
>
|
|
404
|
+
{deviceInstall.busyAction === 'build' ? 'Starting build...' : 'Start device build'}
|
|
405
|
+
</button>
|
|
406
|
+
<details
|
|
407
|
+
className="lr-device-install__build-logs"
|
|
408
|
+
open={deviceInstall.buildLogPanelOpen}
|
|
409
|
+
onToggle={(event) => deviceInstall.setBuildLogPanelOpen(event.currentTarget.open)}
|
|
410
|
+
>
|
|
411
|
+
<summary>Build logs ({deviceInstall.buildStatus})</summary>
|
|
412
|
+
<pre>
|
|
413
|
+
{deviceInstall.buildLogs.length > 0
|
|
414
|
+
? deviceInstall.buildLogs
|
|
415
|
+
.filter((line) => line.type !== 'meta')
|
|
416
|
+
.map((line) => line.data)
|
|
417
|
+
.join('\n')
|
|
418
|
+
: 'Build logs will appear here while the device build is running.'}
|
|
419
|
+
</pre>
|
|
420
|
+
</details>
|
|
421
|
+
</div>
|
|
422
|
+
)}
|
|
423
|
+
|
|
424
|
+
{step.id === 'install' && (
|
|
425
|
+
<div className="lr-device-install__step-body">
|
|
426
|
+
<button
|
|
427
|
+
type="button"
|
|
428
|
+
className="lr-device-install__primary"
|
|
429
|
+
disabled={disabled || !deviceInstall.canInstall}
|
|
430
|
+
onClick={() => void deviceInstall.startInstallation()}
|
|
431
|
+
>
|
|
432
|
+
{deviceInstall.busyAction === 'install' ? 'Installing...' : 'Install last build'}
|
|
433
|
+
</button>
|
|
434
|
+
<button type="button" className="lr-device-install__secondary" onClick={deviceInstall.stopRelay}>
|
|
435
|
+
Stop relay
|
|
436
|
+
</button>
|
|
437
|
+
</div>
|
|
438
|
+
)}
|
|
439
|
+
</StepCard>
|
|
440
|
+
))}
|
|
441
|
+
</div>
|
|
442
|
+
|
|
443
|
+
<footer className="lr-device-install__logs">
|
|
444
|
+
<h3>Progress</h3>
|
|
445
|
+
<ol>
|
|
446
|
+
{deviceInstall.logs.map((entry, index) => (
|
|
447
|
+
<li key={`${index}-${entry.slice(0, 24)}`}>{entry}</li>
|
|
448
|
+
))}
|
|
449
|
+
</ol>
|
|
450
|
+
</footer>
|
|
451
|
+
</section>
|
|
452
|
+
</div>
|
|
453
|
+
)}
|
|
454
|
+
</div>
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function StepCard({
|
|
459
|
+
index,
|
|
460
|
+
step,
|
|
461
|
+
active,
|
|
462
|
+
open,
|
|
463
|
+
status,
|
|
464
|
+
onToggle,
|
|
465
|
+
children,
|
|
466
|
+
}: {
|
|
467
|
+
index: number;
|
|
468
|
+
step: { id: DeviceInstallStep; title: string; description: string };
|
|
469
|
+
active: boolean;
|
|
470
|
+
open: boolean;
|
|
471
|
+
status: DeviceInstallStepStatus;
|
|
472
|
+
onToggle: () => void;
|
|
473
|
+
children: ReactNode;
|
|
474
|
+
}) {
|
|
475
|
+
return (
|
|
476
|
+
<article className={clsx('lr-device-install__step', active && 'lr-device-install__step--active')}>
|
|
477
|
+
<button
|
|
478
|
+
type="button"
|
|
479
|
+
className="lr-device-install__step-header"
|
|
480
|
+
aria-expanded={open}
|
|
481
|
+
onClick={onToggle}
|
|
482
|
+
>
|
|
483
|
+
<div className="lr-device-install__step-number">{index}</div>
|
|
484
|
+
<div>
|
|
485
|
+
<h3>{step.title}</h3>
|
|
486
|
+
<p>{step.description}</p>
|
|
487
|
+
</div>
|
|
488
|
+
<span className={clsx('lr-device-install__status', `lr-device-install__status--${status}`)}>
|
|
489
|
+
{status === 'complete' ? '✓ Completed' : status}
|
|
490
|
+
</span>
|
|
491
|
+
</button>
|
|
492
|
+
{open && children}
|
|
493
|
+
</article>
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function StatusLine({
|
|
498
|
+
label,
|
|
499
|
+
ready,
|
|
500
|
+
pendingText = 'Not ready',
|
|
501
|
+
}: {
|
|
502
|
+
label: string;
|
|
503
|
+
ready?: boolean;
|
|
504
|
+
pendingText?: string;
|
|
505
|
+
}) {
|
|
506
|
+
const text = ready === undefined ? pendingText : ready ? 'Ready' : 'Needs attention';
|
|
507
|
+
return (
|
|
508
|
+
<div className="lr-device-install__check-row">
|
|
509
|
+
<span>{label}</span>
|
|
510
|
+
<strong>{text}</strong>
|
|
511
|
+
</div>
|
|
512
|
+
);
|
|
513
|
+
}
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
/* When click-to-select is enabled, the container also captures clicks
|
|
10
10
|
that fall outside any box so we can clear selection. */
|
|
11
11
|
pointer-events: auto;
|
|
12
|
+
/* Match Chrome DevTools' crosshair cursor while inspect mode is active.
|
|
13
|
+
The merged `.rc-inspect-overlay-select .rc-inspect-box` rule below
|
|
14
|
+
restates the cursor on each inner box, since `.rc-inspect-box` sets
|
|
15
|
+
its own `cursor: pointer`. */
|
|
16
|
+
cursor: crosshair;
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
.rc-inspect-box {
|
|
@@ -32,6 +37,7 @@
|
|
|
32
37
|
|
|
33
38
|
.rc-inspect-overlay-select .rc-inspect-box {
|
|
34
39
|
pointer-events: auto;
|
|
40
|
+
cursor: crosshair;
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
.rc-inspect-box-disabled {
|
|
@@ -7,8 +7,9 @@ import {
|
|
|
7
7
|
AxElement,
|
|
8
8
|
AxPlatform,
|
|
9
9
|
AxSnapshot,
|
|
10
|
+
axCliTapCommand,
|
|
10
11
|
axElementRoleLabel,
|
|
11
|
-
|
|
12
|
+
axElementSelectorObject,
|
|
12
13
|
axElementSummary,
|
|
13
14
|
axElementsEqual,
|
|
14
15
|
clampAxFrameForScreen,
|
|
@@ -37,6 +38,10 @@ export interface InspectOverlayProps {
|
|
|
37
38
|
highlightedId: string | null;
|
|
38
39
|
selectedId: string | null;
|
|
39
40
|
mode: InspectMode;
|
|
41
|
+
// Optional instance id used to render the "Copy command" CLI snippet
|
|
42
|
+
// (e.g. `lim ios tap-element --ax-label 'Sign in' --id <instanceId>`)
|
|
43
|
+
// in the info card. When omitted the Copy command button is hidden.
|
|
44
|
+
instanceId?: string;
|
|
40
45
|
// Current pointer position in viewport coordinates (clientX/Y). Drives the
|
|
41
46
|
// cursor-anchored preview card while hovering. null when the pointer is
|
|
42
47
|
// outside the device.
|
|
@@ -117,6 +122,10 @@ const InspectBox = memo(
|
|
|
117
122
|
if (!selectable) return;
|
|
118
123
|
e.preventDefault();
|
|
119
124
|
e.stopPropagation();
|
|
125
|
+
// Chrome DevTools parity: clicking the already-selected element
|
|
126
|
+
// is a no-op. Re-firing the selection change callback would
|
|
127
|
+
// needlessly reset the card anchor and consumer-side state.
|
|
128
|
+
if (selected) return;
|
|
120
129
|
onClick(element, { x: e.clientX, y: e.clientY });
|
|
121
130
|
}}
|
|
122
131
|
style={{
|
|
@@ -151,6 +160,9 @@ interface InfoCardProps {
|
|
|
151
160
|
anchor: { x: number; y: number };
|
|
152
161
|
cursorAnchored: boolean;
|
|
153
162
|
showActions: boolean;
|
|
163
|
+
// Used to render the "Copy command" CLI snippet. When absent the button
|
|
164
|
+
// is hidden.
|
|
165
|
+
instanceId?: string;
|
|
154
166
|
// Receives the element AND the viewport-space coordinate to tap at
|
|
155
167
|
// (the frozen click position). Tapping at the click point — rather than
|
|
156
168
|
// the element's frame center — preserves the user's aim when the
|
|
@@ -180,6 +192,7 @@ const InfoCard = memo(function InfoCard({
|
|
|
180
192
|
anchor,
|
|
181
193
|
cursorAnchored,
|
|
182
194
|
showActions,
|
|
195
|
+
instanceId,
|
|
183
196
|
onTap,
|
|
184
197
|
}: InfoCardProps) {
|
|
185
198
|
const [copied, setCopied] = useState<string | null>(null);
|
|
@@ -235,11 +248,26 @@ const InfoCard = memo(function InfoCard({
|
|
|
235
248
|
return { left: `${left}px`, top: `${top}px`, transform };
|
|
236
249
|
}, [anchor.x, anchor.y]);
|
|
237
250
|
|
|
238
|
-
|
|
251
|
+
// "Copy selector" returns the raw selector hash as JSON (the same shape
|
|
252
|
+
// accepted by the CLI / instance API). Every available selector field is
|
|
253
|
+
// included so the consumer can disambiguate (e.g. `AXLabel + type`).
|
|
254
|
+
const selectorObject = useMemo(() => axElementSelectorObject(element, platform), [element, platform]);
|
|
255
|
+
const selectorJson = useMemo(
|
|
256
|
+
() => (selectorObject ? JSON.stringify(selectorObject, null, 2) : null),
|
|
257
|
+
[selectorObject],
|
|
258
|
+
);
|
|
259
|
+
// "Copy command" emits a selector-based `lim … tap-element …` invocation
|
|
260
|
+
// that targets this exact element on this exact instance, robust to
|
|
261
|
+
// layout changes that would invalidate a coordinate-based tap.
|
|
262
|
+
const cliCommand = useMemo(
|
|
263
|
+
() => (instanceId ? axCliTapCommand(element, platform, instanceId) : null),
|
|
264
|
+
[element, platform, instanceId],
|
|
265
|
+
);
|
|
266
|
+
|
|
239
267
|
const primaryIdField = platform === 'ios' ? element.selectors.AXUniqueId : element.selectors.resourceId;
|
|
240
268
|
const primaryIdLabel = platform === 'ios' ? 'AXUniqueId' : 'resourceId';
|
|
241
269
|
|
|
242
|
-
const handleCopy = useCallback(async (text: string |
|
|
270
|
+
const handleCopy = useCallback(async (text: string | null, key: string) => {
|
|
243
271
|
if (!text) return;
|
|
244
272
|
const ok = await copyToClipboard(text);
|
|
245
273
|
if (ok) setCopied(key);
|
|
@@ -308,21 +336,22 @@ const InfoCard = memo(function InfoCard({
|
|
|
308
336
|
<button
|
|
309
337
|
type="button"
|
|
310
338
|
className={clsx('rc-inspect-card-btn', copied === 'selector' && 'rc-inspect-card-btn-copied')}
|
|
311
|
-
disabled={!
|
|
312
|
-
title={
|
|
313
|
-
onClick={() => handleCopy(
|
|
339
|
+
disabled={!selectorJson}
|
|
340
|
+
title={selectorJson ?? 'No usable selector for this element'}
|
|
341
|
+
onClick={() => handleCopy(selectorJson, 'selector')}
|
|
314
342
|
>
|
|
315
343
|
{copied === 'selector' ? 'Copied!' : 'Copy selector'}
|
|
316
344
|
</button>
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
345
|
+
{cliCommand && (
|
|
346
|
+
<button
|
|
347
|
+
type="button"
|
|
348
|
+
className={clsx('rc-inspect-card-btn', copied === 'command' && 'rc-inspect-card-btn-copied')}
|
|
349
|
+
title={cliCommand}
|
|
350
|
+
onClick={() => handleCopy(cliCommand, 'command')}
|
|
351
|
+
>
|
|
352
|
+
{copied === 'command' ? 'Copied!' : 'Copy command'}
|
|
353
|
+
</button>
|
|
354
|
+
)}
|
|
326
355
|
</div>
|
|
327
356
|
)}
|
|
328
357
|
</div>,
|
|
@@ -340,6 +369,7 @@ export const InspectOverlay = memo(function InspectOverlay({
|
|
|
340
369
|
highlightedId,
|
|
341
370
|
selectedId,
|
|
342
371
|
mode,
|
|
372
|
+
instanceId,
|
|
343
373
|
cursorPosition,
|
|
344
374
|
frozenCursorPosition,
|
|
345
375
|
onSelectChange,
|
|
@@ -429,6 +459,7 @@ export const InspectOverlay = memo(function InspectOverlay({
|
|
|
429
459
|
anchor={anchor}
|
|
430
460
|
cursorAnchored={cursorAnchored}
|
|
431
461
|
showActions={showActions}
|
|
462
|
+
instanceId={instanceId}
|
|
432
463
|
onTap={onTapElement}
|
|
433
464
|
/>
|
|
434
465
|
)}
|