sliccy 1.3.0 → 1.4.1
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/dist/cli/electron-controller.d.ts +6 -0
- package/dist/cli/electron-controller.js +229 -7
- package/dist/cli/electron-runtime.d.ts +26 -0
- package/dist/cli/electron-runtime.js +225 -1
- package/dist/cli/index.js +64 -11
- package/dist/cli/runtime-flags.d.ts +2 -0
- package/dist/cli/runtime-flags.js +1 -0
- package/dist/ui/electron-overlay-entry.js +91 -312
- package/package.json +1 -1
|
@@ -21,6 +21,7 @@ export declare class ElectronOverlayInjector {
|
|
|
21
21
|
private readonly cdpPort;
|
|
22
22
|
private readonly bootstrapScript;
|
|
23
23
|
private readonly connections;
|
|
24
|
+
private readonly cspBypassedTargets;
|
|
24
25
|
private syncTimer;
|
|
25
26
|
private syncing;
|
|
26
27
|
private constructor();
|
|
@@ -33,6 +34,11 @@ export declare class ElectronOverlayInjector {
|
|
|
33
34
|
start(): Promise<void>;
|
|
34
35
|
stop(): void;
|
|
35
36
|
private syncTargets;
|
|
37
|
+
/**
|
|
38
|
+
* Check if the overlay iframe loaded successfully by evaluating a probe script.
|
|
39
|
+
* Returns true if the iframe element exists and has started loading content.
|
|
40
|
+
*/
|
|
41
|
+
private probeOverlayIframeLoaded;
|
|
36
42
|
private connectToTarget;
|
|
37
43
|
}
|
|
38
44
|
export {};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { execFile as nodeExecFile, spawn } from 'child_process';
|
|
2
2
|
import { existsSync } from 'fs';
|
|
3
3
|
import { readFile } from 'fs/promises';
|
|
4
|
+
import * as http from 'http';
|
|
5
|
+
import * as https from 'https';
|
|
4
6
|
import { promisify } from 'util';
|
|
5
7
|
import { WebSocket } from 'ws';
|
|
6
|
-
import { buildElectronAppLaunchSpec, buildElectronOverlayAppUrl, buildElectronOverlayBootstrapScript, buildElectronOverlayEntryUrl, getElectronOverlayEntryDistPath, getElectronServeOrigin,
|
|
8
|
+
import { buildElectronAppLaunchSpec, buildElectronOverlayAppUrl, buildElectronOverlayBootstrapScript, buildElectronOverlayEntryUrl, getElectronOverlayEntryDistPath, getElectronServeOrigin, selectBestOverlayTargets, } from './electron-runtime.js';
|
|
7
9
|
const execFile = promisify(nodeExecFile);
|
|
8
10
|
const ELECTRON_OVERLAY_SYNC_INTERVAL_MS = 1500;
|
|
9
11
|
function commandLineExecutableMatchesPattern(commandLine, pattern) {
|
|
@@ -183,6 +185,7 @@ export class ElectronOverlayInjector {
|
|
|
183
185
|
cdpPort;
|
|
184
186
|
bootstrapScript;
|
|
185
187
|
connections = new Map();
|
|
188
|
+
cspBypassedTargets = new Set();
|
|
186
189
|
syncTimer = null;
|
|
187
190
|
syncing = false;
|
|
188
191
|
constructor(cdpPort, bootstrapScript) {
|
|
@@ -228,7 +231,14 @@ export class ElectronOverlayInjector {
|
|
|
228
231
|
throw new Error(`CDP target listing failed with ${response.status} ${response.statusText}`);
|
|
229
232
|
}
|
|
230
233
|
const targets = (await response.json());
|
|
231
|
-
const
|
|
234
|
+
const pageCount = targets.filter(t => t.type === 'page').length;
|
|
235
|
+
const injectableTargets = selectBestOverlayTargets(targets);
|
|
236
|
+
if (injectableTargets.length < pageCount) {
|
|
237
|
+
console.log(`[electron-float] Selected ${injectableTargets.length}/${pageCount} page targets for overlay injection`);
|
|
238
|
+
for (const t of injectableTargets) {
|
|
239
|
+
console.log(`[electron-float] → ${t.title || '(untitled)'} @ ${t.url.substring(0, 80)}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
232
242
|
const liveConnectionIds = new Set(injectableTargets.map((target) => target.webSocketDebuggerUrl));
|
|
233
243
|
for (const [targetId, connection] of this.connections.entries()) {
|
|
234
244
|
if (liveConnectionIds.has(targetId))
|
|
@@ -256,20 +266,232 @@ export class ElectronOverlayInjector {
|
|
|
256
266
|
this.syncing = false;
|
|
257
267
|
}
|
|
258
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* Check if the overlay iframe loaded successfully by evaluating a probe script.
|
|
271
|
+
* Returns true if the iframe element exists and has started loading content.
|
|
272
|
+
*/
|
|
273
|
+
async probeOverlayIframeLoaded(ws, send) {
|
|
274
|
+
return new Promise((resolve) => {
|
|
275
|
+
const probeId = send('Runtime.evaluate', {
|
|
276
|
+
expression: `(function() {
|
|
277
|
+
var host = document.getElementById('slicc-electron-overlay-root');
|
|
278
|
+
if (!host || !host.shadowRoot) return 'no-host';
|
|
279
|
+
var sidebar = host.shadowRoot.querySelector('slicc-electron-sidebar');
|
|
280
|
+
if (!sidebar || !sidebar.shadowRoot) return 'no-sidebar';
|
|
281
|
+
var iframe = sidebar.shadowRoot.querySelector('iframe');
|
|
282
|
+
if (!iframe) return 'no-iframe';
|
|
283
|
+
if (!iframe.src) return 'no-src';
|
|
284
|
+
return 'ok';
|
|
285
|
+
})()`,
|
|
286
|
+
awaitPromise: false,
|
|
287
|
+
returnByValue: true,
|
|
288
|
+
});
|
|
289
|
+
const timeout = setTimeout(() => {
|
|
290
|
+
cleanup();
|
|
291
|
+
resolve(false);
|
|
292
|
+
}, 3000);
|
|
293
|
+
const onMessage = (data) => {
|
|
294
|
+
try {
|
|
295
|
+
const msg = JSON.parse(data.toString());
|
|
296
|
+
if (msg.id === probeId) {
|
|
297
|
+
cleanup();
|
|
298
|
+
const value = msg.result?.result?.value;
|
|
299
|
+
resolve(value === 'ok');
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch { /* ignore */ }
|
|
303
|
+
};
|
|
304
|
+
const cleanup = () => {
|
|
305
|
+
clearTimeout(timeout);
|
|
306
|
+
ws.off('message', onMessage);
|
|
307
|
+
};
|
|
308
|
+
ws.on('message', onMessage);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
259
311
|
connectToTarget(target) {
|
|
260
312
|
const targetId = target.webSocketDebuggerUrl;
|
|
261
313
|
const ws = new WebSocket(targetId);
|
|
262
314
|
this.connections.set(targetId, ws);
|
|
263
315
|
let messageId = 1;
|
|
264
316
|
const send = (method, params) => {
|
|
265
|
-
|
|
317
|
+
const id = messageId++;
|
|
318
|
+
ws.send(JSON.stringify({ id, method, params }));
|
|
319
|
+
return id;
|
|
266
320
|
};
|
|
321
|
+
const cspBypassedTargets = this.cspBypassedTargets;
|
|
322
|
+
const bootstrapScript = this.bootstrapScript;
|
|
323
|
+
let pendingReload = false;
|
|
324
|
+
let pendingCspEscalation = false;
|
|
325
|
+
let fetchProxyActive = false;
|
|
267
326
|
ws.on('open', () => {
|
|
268
|
-
|
|
327
|
+
const isWebContent = target.url.startsWith('https://');
|
|
328
|
+
const alreadyBypassed = cspBypassedTargets.has(target.url);
|
|
329
|
+
console.log(`[electron-float] Connected to target, web=${isWebContent}, bypassed=${alreadyBypassed}, url=${target.url}`);
|
|
269
330
|
send('Runtime.enable');
|
|
270
|
-
send('Page.
|
|
271
|
-
|
|
272
|
-
|
|
331
|
+
send('Page.enable');
|
|
332
|
+
// Set CSP bypass — affects future resource loads on the current page
|
|
333
|
+
send('Page.setBypassCSP', { enabled: true });
|
|
334
|
+
if (alreadyBypassed) {
|
|
335
|
+
// Already reloaded with CSP bypass previously — just inject
|
|
336
|
+
console.log(`[electron-float] Injecting overlay (CSP already bypassed)...`);
|
|
337
|
+
send('Runtime.evaluate', { expression: bootstrapScript, awaitPromise: false });
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
// First connection to this target URL: inject overlay immediately, then
|
|
341
|
+
// check if the iframe loaded. If CSP blocked it, fall back to reload+proxy.
|
|
342
|
+
console.log(`[electron-float] Injecting overlay (first attempt)...`);
|
|
343
|
+
send('Runtime.evaluate', { expression: bootstrapScript, awaitPromise: false });
|
|
344
|
+
if (!isWebContent) {
|
|
345
|
+
// Local content (file://, app protocol) — CSP is not an issue
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
// After a short delay, probe whether the overlay iframe loaded.
|
|
349
|
+
// If CSP blocked it, reload the page so Page.setBypassCSP takes effect.
|
|
350
|
+
// If that still doesn't work, escalate to the Fetch proxy.
|
|
351
|
+
setTimeout(async () => {
|
|
352
|
+
if (ws.readyState !== WebSocket.OPEN)
|
|
353
|
+
return;
|
|
354
|
+
const loaded = await this.probeOverlayIframeLoaded(ws, send);
|
|
355
|
+
if (loaded) {
|
|
356
|
+
console.log(`[electron-float] Overlay iframe loaded successfully — no CSP reload needed`);
|
|
357
|
+
cspBypassedTargets.add(target.url);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
// Phase 2: Page.setBypassCSP was already set — a simple reload should
|
|
361
|
+
// make the browser ignore CSP headers on the fresh navigation.
|
|
362
|
+
console.log(`[electron-float] Overlay iframe blocked by CSP, reloading with bypass: ${target.url}`);
|
|
363
|
+
cspBypassedTargets.add(target.url);
|
|
364
|
+
pendingReload = true;
|
|
365
|
+
pendingCspEscalation = true;
|
|
366
|
+
send('Page.reload', { ignoreCache: true });
|
|
367
|
+
}, 1500);
|
|
368
|
+
});
|
|
369
|
+
// Handle CDP events: lifecycle events and Fetch interception
|
|
370
|
+
ws.on('message', (data) => {
|
|
371
|
+
try {
|
|
372
|
+
const msg = JSON.parse(data.toString());
|
|
373
|
+
// Inject overlay after page load completes (after CSP-bypass reload)
|
|
374
|
+
if (msg.method === 'Page.loadEventFired' && pendingReload) {
|
|
375
|
+
pendingReload = false;
|
|
376
|
+
console.log(`[electron-float] Page loaded after CSP reload, injecting overlay...`);
|
|
377
|
+
if (ws.readyState !== WebSocket.OPEN)
|
|
378
|
+
return;
|
|
379
|
+
send('Runtime.evaluate', { expression: bootstrapScript, awaitPromise: false });
|
|
380
|
+
// If this was a simple reload (no proxy), check if the iframe loads now.
|
|
381
|
+
// If it still doesn't, escalate to the Fetch proxy as a last resort.
|
|
382
|
+
if (pendingCspEscalation) {
|
|
383
|
+
pendingCspEscalation = false;
|
|
384
|
+
setTimeout(async () => {
|
|
385
|
+
if (ws.readyState !== WebSocket.OPEN)
|
|
386
|
+
return;
|
|
387
|
+
const loaded = await this.probeOverlayIframeLoaded(ws, send);
|
|
388
|
+
if (loaded) {
|
|
389
|
+
console.log(`[electron-float] Overlay iframe loaded after CSP reload — no proxy needed`);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
console.log(`[electron-float] CSP reload insufficient, escalating to Fetch proxy: ${target.url}`);
|
|
393
|
+
fetchProxyActive = true;
|
|
394
|
+
const urlOrigin = new URL(target.url).origin;
|
|
395
|
+
send('Fetch.enable', {
|
|
396
|
+
patterns: [{ urlPattern: `${urlOrigin}/*`, requestStage: 'Request' }],
|
|
397
|
+
});
|
|
398
|
+
pendingReload = true;
|
|
399
|
+
send('Page.reload', { ignoreCache: true });
|
|
400
|
+
}, 1500);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (msg.method === 'Fetch.requestPaused' && fetchProxyActive) {
|
|
404
|
+
const requestId = msg.params?.requestId;
|
|
405
|
+
if (!requestId) {
|
|
406
|
+
console.warn('[electron-float] Fetch.requestPaused without requestId, skipping');
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const url = msg.params?.request?.url || '';
|
|
410
|
+
const method = msg.params?.request?.method || 'GET';
|
|
411
|
+
const requestHeaders = msg.params?.request?.headers || {};
|
|
412
|
+
const postData = msg.params?.request?.postData;
|
|
413
|
+
// Only proxy HTML document requests (Accept header contains text/html)
|
|
414
|
+
const acceptHeader = requestHeaders['Accept'] || requestHeaders['accept'] || '';
|
|
415
|
+
if (!acceptHeader.includes('text/html')) {
|
|
416
|
+
send('Fetch.continueRequest', { requestId });
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
console.log(`[electron-float] Proxying request to strip CSP: ${url.substring(0, 60)}`);
|
|
420
|
+
// Make the request ourselves using Node.js http/https
|
|
421
|
+
const parsedUrl = new URL(url);
|
|
422
|
+
const transport = parsedUrl.protocol === 'https:' ? https : http;
|
|
423
|
+
const options = {
|
|
424
|
+
hostname: parsedUrl.hostname,
|
|
425
|
+
port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
|
|
426
|
+
path: parsedUrl.pathname + parsedUrl.search,
|
|
427
|
+
method: method,
|
|
428
|
+
headers: requestHeaders,
|
|
429
|
+
};
|
|
430
|
+
const proxyReq = transport.request(options, (proxyRes) => {
|
|
431
|
+
const bodyChunks = [];
|
|
432
|
+
proxyRes.on('data', (chunk) => bodyChunks.push(chunk));
|
|
433
|
+
proxyRes.on('end', () => {
|
|
434
|
+
if (ws.readyState !== WebSocket.OPEN)
|
|
435
|
+
return;
|
|
436
|
+
const fullBody = Buffer.concat(bodyChunks);
|
|
437
|
+
// Build response headers, stripping CSP and hop-by-hop headers
|
|
438
|
+
// that are invalid in Fetch.fulfillRequest responses
|
|
439
|
+
const HOP_BY_HOP = new Set([
|
|
440
|
+
'content-security-policy',
|
|
441
|
+
'content-security-policy-report-only',
|
|
442
|
+
'transfer-encoding',
|
|
443
|
+
'connection',
|
|
444
|
+
'keep-alive',
|
|
445
|
+
]);
|
|
446
|
+
const responseHeaders = [];
|
|
447
|
+
let strippedCSP = false;
|
|
448
|
+
for (const [name, value] of Object.entries(proxyRes.headers)) {
|
|
449
|
+
const lower = name.toLowerCase();
|
|
450
|
+
if (lower.includes('content-security-policy')) {
|
|
451
|
+
strippedCSP = true;
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (HOP_BY_HOP.has(lower))
|
|
455
|
+
continue;
|
|
456
|
+
// Update content-length to match actual body size
|
|
457
|
+
if (lower === 'content-length') {
|
|
458
|
+
responseHeaders.push({ name, value: String(fullBody.length) });
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
if (Array.isArray(value)) {
|
|
462
|
+
value.forEach(v => responseHeaders.push({ name, value: v }));
|
|
463
|
+
}
|
|
464
|
+
else if (value) {
|
|
465
|
+
responseHeaders.push({ name, value });
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (strippedCSP) {
|
|
469
|
+
console.log(`[electron-float] Stripped CSP from: ${url.substring(0, 60)}`);
|
|
470
|
+
}
|
|
471
|
+
send('Fetch.fulfillRequest', {
|
|
472
|
+
requestId,
|
|
473
|
+
responseCode: proxyRes.statusCode || 200,
|
|
474
|
+
responseHeaders,
|
|
475
|
+
body: fullBody.toString('base64'),
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
proxyReq.on('error', (err) => {
|
|
480
|
+
console.error(`[electron-float] Proxy request failed for ${url.substring(0, 60)}:`, err.message);
|
|
481
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
482
|
+
send('Fetch.failRequest', { requestId, errorReason: 'Failed' });
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
// Forward request body if present (for POST/PUT requests)
|
|
486
|
+
if (postData) {
|
|
487
|
+
proxyReq.write(postData);
|
|
488
|
+
}
|
|
489
|
+
proxyReq.end();
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
// Ignore parse errors for non-JSON messages
|
|
494
|
+
}
|
|
273
495
|
});
|
|
274
496
|
ws.on('close', () => {
|
|
275
497
|
if (this.connections.get(targetId) === ws) {
|
|
@@ -17,6 +17,7 @@ export interface ElectronAppLaunchSpec {
|
|
|
17
17
|
}
|
|
18
18
|
export interface ElectronInspectableTarget {
|
|
19
19
|
type: string;
|
|
20
|
+
title?: string;
|
|
20
21
|
url: string;
|
|
21
22
|
webSocketDebuggerUrl?: string;
|
|
22
23
|
}
|
|
@@ -26,6 +27,25 @@ export declare const DEFAULT_ELECTRON_CDP_PORT = 9223;
|
|
|
26
27
|
export declare const DEFAULT_ELECTRON_TARGET_URL = "about:blank";
|
|
27
28
|
export declare const DEFAULT_ELECTRON_OVERLAY_TAB = "chat";
|
|
28
29
|
export declare const ELECTRON_OVERLAY_APP_PATH = "/electron";
|
|
30
|
+
export declare const PORT_HASH_RANGE = 40;
|
|
31
|
+
/**
|
|
32
|
+
* Simple string hash function that returns a value between 0 and max-1.
|
|
33
|
+
* Exported for testing.
|
|
34
|
+
*/
|
|
35
|
+
export declare function hashString(str: string, max: number): number;
|
|
36
|
+
/**
|
|
37
|
+
* Get a port for an Electron app based on its path.
|
|
38
|
+
* Uses hash-based offset from base port, with fallback to next available port
|
|
39
|
+
* starting from the preferred port (to stay in the app's "slot" range).
|
|
40
|
+
*/
|
|
41
|
+
export declare function getElectronAppPort(appPath: string, basePort: number): Promise<number>;
|
|
42
|
+
/**
|
|
43
|
+
* Get both CDP and serve ports for an Electron app.
|
|
44
|
+
*/
|
|
45
|
+
export declare function getElectronAppPorts(appPath: string): Promise<{
|
|
46
|
+
cdpPort: number;
|
|
47
|
+
servePort: number;
|
|
48
|
+
}>;
|
|
29
49
|
export declare function getElectronAppDisplayName(appPath: string): string;
|
|
30
50
|
export declare function resolveElectronAppExecutablePath(appPath: string, platform?: NodeJS.Platform): string;
|
|
31
51
|
export declare function buildElectronAppProcessMatchPatterns(appPath: string, platform?: NodeJS.Platform): string[];
|
|
@@ -56,3 +76,9 @@ export declare function buildElectronOverlayBootstrapScript(options: {
|
|
|
56
76
|
activeTab?: string;
|
|
57
77
|
}): string;
|
|
58
78
|
export declare function shouldInjectElectronOverlayTarget(target: ElectronInspectableTarget): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* From a list of injectable targets, select the best target per origin.
|
|
81
|
+
* Multi-window apps (like Teams) expose several page targets for the same origin;
|
|
82
|
+
* we only want to inject the overlay into the primary content window.
|
|
83
|
+
*/
|
|
84
|
+
export declare function selectBestOverlayTargets(targets: ElectronInspectableTarget[]): ElectronInspectableTarget[];
|
|
@@ -1,10 +1,97 @@
|
|
|
1
1
|
import { basename, join, resolve } from 'path';
|
|
2
|
+
import { accessSync, constants, readdirSync, statSync } from 'fs';
|
|
2
3
|
export const DEFAULT_ELECTRON_SERVE_PORT = 5710;
|
|
3
4
|
export const DEFAULT_ELECTRON_SERVE_HOST = 'localhost';
|
|
4
5
|
export const DEFAULT_ELECTRON_CDP_PORT = 9223;
|
|
5
6
|
export const DEFAULT_ELECTRON_TARGET_URL = 'about:blank';
|
|
6
7
|
export const DEFAULT_ELECTRON_OVERLAY_TAB = 'chat';
|
|
7
8
|
export const ELECTRON_OVERLAY_APP_PATH = '/electron';
|
|
9
|
+
// Port allocation constants
|
|
10
|
+
export const PORT_HASH_RANGE = 40;
|
|
11
|
+
/**
|
|
12
|
+
* Simple string hash function that returns a value between 0 and max-1.
|
|
13
|
+
* Exported for testing.
|
|
14
|
+
*/
|
|
15
|
+
export function hashString(str, max) {
|
|
16
|
+
let hash = 0;
|
|
17
|
+
for (let i = 0; i < str.length; i++) {
|
|
18
|
+
const char = str.charCodeAt(i);
|
|
19
|
+
hash = ((hash << 5) - hash) + char;
|
|
20
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
21
|
+
}
|
|
22
|
+
return Math.abs(hash) % max;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Try to listen on a specific port and host, returning the assigned port.
|
|
26
|
+
*/
|
|
27
|
+
async function tryListenOnPort(port, host) {
|
|
28
|
+
const { createServer } = await import('net');
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const server = createServer();
|
|
31
|
+
server.on('error', reject);
|
|
32
|
+
server.listen(port, host, () => {
|
|
33
|
+
const addr = server.address();
|
|
34
|
+
const assignedPort = addr && typeof addr === 'object' ? addr.port : port;
|
|
35
|
+
server.close(() => resolve(assignedPort));
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Check if a port is available on both IPv4 (127.0.0.1) and IPv6 (::1).
|
|
41
|
+
* On macOS, `localhost` resolves to `::1`, so we need to check both.
|
|
42
|
+
*/
|
|
43
|
+
async function isPortAvailable(port) {
|
|
44
|
+
try {
|
|
45
|
+
await tryListenOnPort(port, '127.0.0.1');
|
|
46
|
+
try {
|
|
47
|
+
await tryListenOnPort(port, '::1');
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
// ::1 may not be available on some systems — only fail on EADDRINUSE
|
|
51
|
+
if (err.code === 'EADDRINUSE') {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Find an available port starting from the given port.
|
|
63
|
+
*/
|
|
64
|
+
async function findAvailablePort(startPort, maxAttempts = 100) {
|
|
65
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
66
|
+
const port = startPort + i;
|
|
67
|
+
if (await isPortAvailable(port)) {
|
|
68
|
+
return port;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
throw new Error(`Could not find available port starting from ${startPort}`);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Get a port for an Electron app based on its path.
|
|
75
|
+
* Uses hash-based offset from base port, with fallback to next available port
|
|
76
|
+
* starting from the preferred port (to stay in the app's "slot" range).
|
|
77
|
+
*/
|
|
78
|
+
export async function getElectronAppPort(appPath, basePort) {
|
|
79
|
+
const offset = hashString(appPath, PORT_HASH_RANGE);
|
|
80
|
+
const preferredPort = basePort + offset;
|
|
81
|
+
if (await isPortAvailable(preferredPort)) {
|
|
82
|
+
return preferredPort;
|
|
83
|
+
}
|
|
84
|
+
// Fallback: find next available port starting from preferred (stay in slot range)
|
|
85
|
+
return findAvailablePort(preferredPort + 1);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get both CDP and serve ports for an Electron app.
|
|
89
|
+
*/
|
|
90
|
+
export async function getElectronAppPorts(appPath) {
|
|
91
|
+
const cdpPort = await getElectronAppPort(appPath, DEFAULT_ELECTRON_CDP_PORT);
|
|
92
|
+
const servePort = await getElectronAppPort(appPath, DEFAULT_ELECTRON_SERVE_PORT);
|
|
93
|
+
return { cdpPort, servePort };
|
|
94
|
+
}
|
|
8
95
|
export function getElectronAppDisplayName(appPath) {
|
|
9
96
|
const trimmedPath = appPath.replace(/[\\/]+$/, '');
|
|
10
97
|
const fileName = basename(trimmedPath);
|
|
@@ -16,7 +103,80 @@ export function getElectronAppDisplayName(appPath) {
|
|
|
16
103
|
export function resolveElectronAppExecutablePath(appPath, platform = process.platform) {
|
|
17
104
|
const resolvedAppPath = resolve(appPath);
|
|
18
105
|
if (platform === 'darwin' && resolvedAppPath.toLowerCase().endsWith('.app')) {
|
|
19
|
-
|
|
106
|
+
const macOSDir = join(resolvedAppPath, 'Contents', 'MacOS');
|
|
107
|
+
// First try the expected name (app name without .app)
|
|
108
|
+
const expectedName = getElectronAppDisplayName(resolvedAppPath);
|
|
109
|
+
const expectedPath = join(macOSDir, expectedName);
|
|
110
|
+
try {
|
|
111
|
+
const stat = statSync(expectedPath);
|
|
112
|
+
if (stat.isFile()) {
|
|
113
|
+
return expectedPath;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Expected path doesn't exist, scan the directory
|
|
118
|
+
}
|
|
119
|
+
// Scan MacOS directory for the main executable
|
|
120
|
+
// Many Electron apps use "Electron" as the executable name
|
|
121
|
+
// Prefer known main executable names, filter out helper processes
|
|
122
|
+
const helperPatterns = [
|
|
123
|
+
/helper/i,
|
|
124
|
+
/crash/i,
|
|
125
|
+
/gpu/i,
|
|
126
|
+
/renderer/i,
|
|
127
|
+
/plugin/i,
|
|
128
|
+
/utility/i,
|
|
129
|
+
];
|
|
130
|
+
try {
|
|
131
|
+
const entries = readdirSync(macOSDir);
|
|
132
|
+
// Helper to check if a file is executable
|
|
133
|
+
const isExecutable = (path) => {
|
|
134
|
+
try {
|
|
135
|
+
accessSync(path, constants.X_OK);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
// First pass: look for "Electron" executable (common in Electron apps)
|
|
143
|
+
if (entries.includes('Electron')) {
|
|
144
|
+
const electronPath = join(macOSDir, 'Electron');
|
|
145
|
+
try {
|
|
146
|
+
const stat = statSync(electronPath);
|
|
147
|
+
if (stat.isFile() && isExecutable(electronPath)) {
|
|
148
|
+
return electronPath;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// Continue to next fallback
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Second pass: find first non-helper executable with execute permission
|
|
156
|
+
for (const entry of entries) {
|
|
157
|
+
// Skip hidden files, scripts, and helper executables
|
|
158
|
+
if (entry.startsWith('.') || entry.endsWith('.sh'))
|
|
159
|
+
continue;
|
|
160
|
+
if (helperPatterns.some((p) => p.test(entry)))
|
|
161
|
+
continue;
|
|
162
|
+
const entryPath = join(macOSDir, entry);
|
|
163
|
+
try {
|
|
164
|
+
const stat = statSync(entryPath);
|
|
165
|
+
if (stat.isFile() && isExecutable(entryPath)) {
|
|
166
|
+
return entryPath;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// Can't read directory, fall back to expected path
|
|
176
|
+
}
|
|
177
|
+
// Fall back to expected path even if it doesn't exist
|
|
178
|
+
// (error will be caught later)
|
|
179
|
+
return expectedPath;
|
|
20
180
|
}
|
|
21
181
|
return resolvedAppPath;
|
|
22
182
|
}
|
|
@@ -135,3 +295,67 @@ export function shouldInjectElectronOverlayTarget(target) {
|
|
|
135
295
|
return false;
|
|
136
296
|
return true;
|
|
137
297
|
}
|
|
298
|
+
/**
|
|
299
|
+
* Extract the origin from a URL, or return the URL as-is for non-standard schemes.
|
|
300
|
+
*/
|
|
301
|
+
function safeOrigin(url) {
|
|
302
|
+
try {
|
|
303
|
+
return new URL(url).origin;
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return url;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Score a target for "primary window" ranking within a group of same-origin pages.
|
|
311
|
+
* Higher score = more likely the main content window.
|
|
312
|
+
*/
|
|
313
|
+
function scoreOverlayTarget(target) {
|
|
314
|
+
let score = 0;
|
|
315
|
+
const title = target.title ?? '';
|
|
316
|
+
const url = target.url;
|
|
317
|
+
// Longer, more descriptive titles indicate content windows (e.g.
|
|
318
|
+
// "Calendar | Adobe | Microsoft Teams" vs generic "Microsoft Teams")
|
|
319
|
+
score += Math.min(title.length, 120);
|
|
320
|
+
// Penalize URLs with hash fragments that suggest hidden/auxiliary windows
|
|
321
|
+
// (e.g. Teams uses #deepLink=default&isMinimized=false for background windows)
|
|
322
|
+
if (url.includes('isMinimized=') || url.includes('deepLink=')) {
|
|
323
|
+
score -= 200;
|
|
324
|
+
}
|
|
325
|
+
// Prefer clean URLs without large hash fragments (shell pages often have them)
|
|
326
|
+
const hashLength = url.includes('#') ? url.length - url.indexOf('#') : 0;
|
|
327
|
+
score -= Math.min(hashLength, 100);
|
|
328
|
+
return score;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* From a list of injectable targets, select the best target per origin.
|
|
332
|
+
* Multi-window apps (like Teams) expose several page targets for the same origin;
|
|
333
|
+
* we only want to inject the overlay into the primary content window.
|
|
334
|
+
*/
|
|
335
|
+
export function selectBestOverlayTargets(targets) {
|
|
336
|
+
const injectable = targets.filter(shouldInjectElectronOverlayTarget);
|
|
337
|
+
// Group by origin
|
|
338
|
+
const byOrigin = new Map();
|
|
339
|
+
for (const target of injectable) {
|
|
340
|
+
const origin = safeOrigin(target.url);
|
|
341
|
+
const group = byOrigin.get(origin);
|
|
342
|
+
if (group) {
|
|
343
|
+
group.push(target);
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
byOrigin.set(origin, [target]);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
// Pick the best target from each origin group
|
|
350
|
+
const result = [];
|
|
351
|
+
for (const group of byOrigin.values()) {
|
|
352
|
+
if (group.length === 1) {
|
|
353
|
+
result.push(group[0]);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
// Sort by score descending and pick the winner
|
|
357
|
+
group.sort((a, b) => scoreOverlayTarget(b) - scoreOverlayTarget(a));
|
|
358
|
+
result.push(group[0]);
|
|
359
|
+
}
|
|
360
|
+
return result;
|
|
361
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { fileURLToPath } from 'url';
|
|
|
8
8
|
import express from 'express';
|
|
9
9
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
10
10
|
import { ElectronAppAlreadyRunningError, ElectronOverlayInjector, launchElectronApp, } from './electron-controller.js';
|
|
11
|
+
import { getElectronAppPorts } from './electron-runtime.js';
|
|
11
12
|
import { buildChromeLaunchArgs, ensureQaProfileScaffold, findChromeExecutable, resolveChromeLaunchProfile, waitForCdpPortFromStderr, } from './chrome-launch.js';
|
|
12
13
|
import { resolveCliBrowserLaunchUrl } from './launch-url.js';
|
|
13
14
|
import { parseCliRuntimeFlags } from './runtime-flags.js';
|
|
@@ -249,16 +250,33 @@ const PREFERRED_HMR_PORT = 24679;
|
|
|
249
250
|
async function main() {
|
|
250
251
|
// Resolve available ports before anything else — serve port must be known
|
|
251
252
|
// before Chrome launches (the launch URL contains it).
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
253
|
+
let SERVE_PORT;
|
|
254
|
+
let CDP_PORT;
|
|
255
|
+
let REQUESTED_CDP_PORT;
|
|
256
|
+
let usingDynamicElectronPorts = false;
|
|
257
|
+
if (ELECTRON_MODE && ELECTRON_APP && !RUNTIME_FLAGS.explicitCdpPort) {
|
|
258
|
+
// Dynamic port allocation for Electron apps (hash-based with fallback)
|
|
259
|
+
const ports = await getElectronAppPorts(ELECTRON_APP);
|
|
260
|
+
CDP_PORT = ports.cdpPort;
|
|
261
|
+
SERVE_PORT = ports.servePort;
|
|
262
|
+
REQUESTED_CDP_PORT = CDP_PORT;
|
|
263
|
+
usingDynamicElectronPorts = true;
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
SERVE_PORT = await findAvailablePort(PREFERRED_SERVE_PORT);
|
|
267
|
+
// For Chrome CDP, we pass port 0 to let Chrome pick any available port,
|
|
268
|
+
// then parse the actual port from its stderr. This avoids race conditions
|
|
269
|
+
// where Node's port probe succeeds but Chrome still can't bind the port.
|
|
270
|
+
// Electron mode keeps the preferred port (external CDP, not launched by us).
|
|
271
|
+
REQUESTED_CDP_PORT = ELECTRON_MODE ? PREFERRED_CDP_PORT : 0;
|
|
272
|
+
CDP_PORT = ELECTRON_MODE ? PREFERRED_CDP_PORT : 0;
|
|
273
|
+
}
|
|
259
274
|
const HMR_PORT = DEV_MODE ? await findAvailablePort(PREFERRED_HMR_PORT) : PREFERRED_HMR_PORT;
|
|
260
275
|
const SERVE_ORIGIN = `http://localhost:${SERVE_PORT}`;
|
|
261
|
-
if (
|
|
276
|
+
if (usingDynamicElectronPorts) {
|
|
277
|
+
console.log(`Dynamic port allocation for Electron app: CDP=${CDP_PORT}, serve=${SERVE_PORT}`);
|
|
278
|
+
}
|
|
279
|
+
else if (SERVE_PORT !== PREFERRED_SERVE_PORT) {
|
|
262
280
|
console.log(`Port ${PREFERRED_SERVE_PORT} in use, serving on port ${SERVE_PORT}`);
|
|
263
281
|
}
|
|
264
282
|
if (DEV_MODE && HMR_PORT !== PREFERRED_HMR_PORT) {
|
|
@@ -295,14 +313,49 @@ async function main() {
|
|
|
295
313
|
launchedBrowserProcess = child;
|
|
296
314
|
launchedBrowserLabel = displayName;
|
|
297
315
|
pipeChildOutput(child, 'electron-app');
|
|
316
|
+
// Track when app exits - quick exits before CDP connects indicate a problem
|
|
317
|
+
let cdpConnected = false;
|
|
318
|
+
let exitCode = null;
|
|
319
|
+
let exitResolve = null;
|
|
320
|
+
const exitPromise = new Promise((resolve) => {
|
|
321
|
+
exitResolve = resolve;
|
|
322
|
+
});
|
|
298
323
|
child.on('exit', (code) => {
|
|
324
|
+
exitCode = code;
|
|
325
|
+
exitResolve?.();
|
|
299
326
|
if (shuttingDown)
|
|
300
327
|
return;
|
|
301
|
-
|
|
302
|
-
|
|
328
|
+
if (cdpConnected) {
|
|
329
|
+
// Normal exit after we connected
|
|
330
|
+
console.log(`${displayName} exited with code ${code}`);
|
|
331
|
+
process.exit(0);
|
|
332
|
+
}
|
|
333
|
+
// If CDP not yet connected, don't exit - let waitForCDP handle it
|
|
303
334
|
});
|
|
304
335
|
console.log(`Waiting for ${displayName} CDP on port ${CDP_PORT}...`);
|
|
305
|
-
|
|
336
|
+
try {
|
|
337
|
+
// Race between CDP connection and app exit
|
|
338
|
+
await Promise.race([
|
|
339
|
+
waitForCDP(CDP_PORT, 40, 500).then(() => {
|
|
340
|
+
cdpConnected = true;
|
|
341
|
+
}),
|
|
342
|
+
exitPromise.then(() => {
|
|
343
|
+
if (!cdpConnected) {
|
|
344
|
+
throw new Error('app-exited');
|
|
345
|
+
}
|
|
346
|
+
}),
|
|
347
|
+
]);
|
|
348
|
+
}
|
|
349
|
+
catch (err) {
|
|
350
|
+
// Check if app exited quickly (likely due to disabled remote debugging fuse)
|
|
351
|
+
if (exitCode !== null) {
|
|
352
|
+
console.error(`\n${displayName} exited with code ${exitCode} before remote debugging was available.`);
|
|
353
|
+
console.error('This usually means the app has disabled remote debugging (EnableNodeCliInspectArguments fuse).');
|
|
354
|
+
console.error('Some Electron apps disable this for security. Check if there is a developer/debug build available.\n');
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
throw new Error(`Could not connect to ${displayName} CDP on port ${CDP_PORT}`);
|
|
358
|
+
}
|
|
306
359
|
console.log(`Connected to ${displayName} on CDP port ${CDP_PORT}`);
|
|
307
360
|
// Auto-discover leader's tray join URL when another instance runs on the preferred port.
|
|
308
361
|
// The leader may still be creating its tray session, so retry a few times.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";(()=>{var
|
|
1
|
+
"use strict";(()=>{var B=[{id:"chat",label:"Chat"},{id:"terminal",label:"Terminal"},{id:"files",label:"Files"},{id:"memory",label:"Memory"}],Z="slicc-hidden-tabs",V=["terminal","memory"];function tt(){try{let r=localStorage.getItem(Z);return r?new Set(JSON.parse(r)):new Set(V)}catch{return new Set(V)}}var H=B.filter(r=>!tt().has(r.id)),C="chat",lt=new Set(B.map(r=>r.id));function l(r,t=C){return r&&r.length>0?r:t}var et=["top-left","top-right","bottom-left","bottom-right"],rt="top-right",nt=6,at=12,ot=.6;function b(r,t=rt){return et.includes(r)?r:t}function T(r={}){return{open:r.open??!1,activeTab:l(r.activeTab,C),corner:b(r.corner)}}function z(r){return{...r,open:!r.open}}function _(r,t){return r.open===t?r:{...r,open:t}}function w(r,t){let e=l(t,r.activeTab);return r.activeTab===e?r:{...r,activeTab:e}}function I(r,t){let e=b(t,r.corner);return r.corner===e?r:{...r,corner:e}}function R(r,t){return r>=nt||r>=at&&t>=ot}function j({clientX:r,clientY:t,viewportWidth:e,viewportHeight:n,velocityXPxPerMs:a=0,velocityYPxPerMs:o=0,flickProjectionMs:i=180}){let s=$(r+a*i,0,e),d=$(t+o*i,0,n),u=s<e/2?"left":"right";return`${d<n/2?"top":"bottom"}-${u}`}function $(r,t,e){return Math.min(Math.max(r,t),e)}var F="slicc-electron-overlay:set-tab";var k="slicc-electron-overlay-root",U="slicc-electron-overlay",L="slicc-electron-launcher",O="slicc-electron-sidebar",v="",h=18,G="slicc-electron-overlay-launcher-corner";function X(r,t){let e=r.createElement("style");return e.textContent=t,e}function K(r){let t=r.createElementNS("http://www.w3.org/2000/svg","svg");t.setAttribute("viewBox","0 0 24 24"),t.setAttribute("aria-hidden","true");let e=[{cx:"9",cy:"7.2",r:"4.1"},{cx:"15.2",cy:"8.1",r:"4"}];for(let{cx:a,cy:o,r:i}of e){let s=r.createElementNS("http://www.w3.org/2000/svg","circle");s.setAttribute("cx",a),s.setAttribute("cy",o),s.setAttribute("r",i),t.appendChild(s)}let n=r.createElementNS("http://www.w3.org/2000/svg","path");return n.setAttribute("d","M9.8 12.4h5.1L12.4 21z"),t.appendChild(n),t}var D=`
|
|
2
2
|
:host {
|
|
3
3
|
color-scheme: dark light;
|
|
4
4
|
--s2-gray-25: #1a1a1a;
|
|
@@ -47,314 +47,93 @@
|
|
|
47
47
|
--s2-shadow-elevated: 0 10px 32px rgba(0, 0, 0, 0.14), 0 2px 10px rgba(0, 0, 0, 0.08);
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
-
`;function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
border: 1px solid rgba(255, 255, 255, 0.32);
|
|
141
|
-
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.18);
|
|
142
|
-
pointer-events: none;
|
|
143
|
-
}
|
|
144
|
-
:host([dragging]) button { cursor: grabbing; }
|
|
145
|
-
button:hover {
|
|
146
|
-
transform: translateY(-1px);
|
|
147
|
-
border-color: rgba(255, 255, 255, 0.78);
|
|
148
|
-
box-shadow:
|
|
149
|
-
0 0 0 1px rgba(255, 255, 255, 0.24) inset,
|
|
150
|
-
0 0 0 4px rgba(0, 0, 0, 0.34),
|
|
151
|
-
0 16px 34px rgba(0, 0, 0, 0.44),
|
|
152
|
-
0 0 18px color-mix(in srgb, var(--slicc-cone) 40%, transparent);
|
|
153
|
-
}
|
|
154
|
-
button:active { transform: scale(0.96); }
|
|
155
|
-
button[aria-pressed="true"] {
|
|
156
|
-
border-color: rgba(255, 255, 255, 0.84);
|
|
157
|
-
box-shadow:
|
|
158
|
-
0 0 0 1px rgba(255, 255, 255, 0.24) inset,
|
|
159
|
-
0 0 0 4px rgba(0, 0, 0, 0.36),
|
|
160
|
-
0 16px 38px rgba(0, 0, 0, 0.46),
|
|
161
|
-
0 0 22px color-mix(in srgb, var(--slicc-cone) 52%, transparent);
|
|
162
|
-
}
|
|
163
|
-
svg {
|
|
164
|
-
width: 20px;
|
|
165
|
-
height: 20px;
|
|
166
|
-
fill: currentColor;
|
|
167
|
-
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.55));
|
|
168
|
-
}
|
|
169
|
-
</style>
|
|
170
|
-
<button type="button" aria-label="Toggle SLICC overlay">
|
|
171
|
-
${D()}
|
|
172
|
-
</button>
|
|
173
|
-
`,this.button=t.querySelector("button"),this.button?.addEventListener("click",e=>{if(this.suppressClick){this.suppressClick=!1,e.preventDefault(),e.stopImmediatePropagation();return}this.dispatchEvent(new CustomEvent("slicc-overlay-toggle",{bubbles:!0,composed:!0}))}),this.button?.addEventListener("pointerdown",this.onPointerDown),this.button?.addEventListener("pointermove",this.onPointerMove),this.button?.addEventListener("pointerup",this.onPointerUp),this.button?.addEventListener("pointercancel",this.onPointerCancel)}sync(){this.button?.setAttribute("aria-pressed",String(this.hasAttribute("open")))}onPointerDown=t=>{if(!t.isPrimary||t.button!==0||!this.button)return;let e=this.getBoundingClientRect();this.pointerState={pointerId:t.pointerId,startX:t.clientX,startY:t.clientY,startLeft:e.left,startTop:e.top,width:e.width,height:e.height,lastX:t.clientX,lastY:t.clientY,lastTimestamp:t.timeStamp,velocityX:0,velocityY:0,dragging:!1},this.suppressClick=!1,this.button.setPointerCapture(t.pointerId),t.preventDefault()};onPointerMove=t=>{let e=this.pointerState;if(!e||t.pointerId!==e.pointerId)return;let n=t.clientX-e.startX,a=t.clientY-e.startY,s=Math.hypot(n,a);!e.dragging&&S(s,0)&&(e.dragging=!0,this.setAttribute("dragging",""));let u=Math.max(t.timeStamp-e.lastTimestamp,1),b=(t.clientX-e.lastX)/u,h=(t.clientY-e.lastY)/u;if(e.velocityX=e.velocityX===0?b:e.velocityX*.35+b*.65,e.velocityY=e.velocityY===0?h:e.velocityY*.35+h*.65,e.lastX=t.clientX,e.lastY=t.clientY,e.lastTimestamp=t.timeStamp,!e.dragging)return;let f=Math.max(o,window.innerWidth-e.width-o),I=Math.max(o,window.innerHeight-e.height-o),B=X(e.startLeft+n,o,f),z=X(e.startTop+a,o,I);this.style.left=`${B}px`,this.style.top=`${z}px`,this.style.right="auto",this.style.bottom="auto",t.preventDefault()};onPointerUp=t=>{this.finishPointerInteraction(t)};onPointerCancel=t=>{this.finishPointerInteraction(t,!1)};finishPointerInteraction(t,e=!0){let n=this.pointerState;if(!n||t.pointerId!==n.pointerId||!this.button)return;let a=Math.hypot(t.clientX-n.startX,t.clientY-n.startY),s=Math.hypot(n.velocityX,n.velocityY);if(e&&(n.dragging||S(a,s))){let b=Y({clientX:t.clientX,clientY:t.clientY,viewportWidth:window.innerWidth,viewportHeight:window.innerHeight,velocityXPxPerMs:n.velocityX,velocityYPxPerMs:n.velocityY});this.dispatchEvent(new CustomEvent("slicc-overlay-move",{bubbles:!0,composed:!0,detail:{corner:b}})),this.suppressClick=!0,t.preventDefault()}this.button.hasPointerCapture(t.pointerId)&&this.button.releasePointerCapture(t.pointerId),this.pointerState=null,this.removeAttribute("dragging"),this.resetDragStyles()}resetDragStyles(){this.style.left="",this.style.top="",this.style.right="",this.style.bottom=""}},C=class extends HTMLElement{static observedAttributes=["open","active-tab","app-url"];tabButtons=new Map;iframe=null;emptyState=null;currentAppUrl=p;frameLoaded=!1;lastPostedTab=null;connectedCallback(){this.shadowRoot||this.render(),this.sync()}attributeChangedCallback(){this.sync()}render(){let t=this.attachShadow({mode:"open"}),e=i(this.getAttribute("active-tab"));t.innerHTML=`
|
|
174
|
-
<style>
|
|
175
|
-
${w}
|
|
176
|
-
:host {
|
|
177
|
-
all: initial;
|
|
178
|
-
position: fixed;
|
|
179
|
-
inset: 0;
|
|
180
|
-
display: block;
|
|
181
|
-
pointer-events: none;
|
|
182
|
-
font-family: var(--s2-font-family);
|
|
183
|
-
}
|
|
184
|
-
:host([open]) {
|
|
185
|
-
pointer-events: auto;
|
|
186
|
-
}
|
|
187
|
-
*, *::before, *::after { box-sizing: border-box; }
|
|
188
|
-
.backdrop {
|
|
189
|
-
position: absolute;
|
|
190
|
-
inset: 0;
|
|
191
|
-
background: rgba(0, 0, 0, 0.18);
|
|
192
|
-
opacity: 0;
|
|
193
|
-
transition: opacity var(--s2-transition-default);
|
|
194
|
-
}
|
|
195
|
-
:host([open]) .backdrop {
|
|
196
|
-
opacity: 1;
|
|
197
|
-
}
|
|
198
|
-
.sidebar {
|
|
199
|
-
position: absolute;
|
|
200
|
-
top: 12px;
|
|
201
|
-
right: 12px;
|
|
202
|
-
bottom: 12px;
|
|
203
|
-
width: min(440px, calc(100vw - 24px));
|
|
204
|
-
display: flex;
|
|
205
|
-
flex-direction: column;
|
|
206
|
-
overflow: hidden;
|
|
207
|
-
background: color-mix(in srgb, var(--s2-bg-base) 96%, transparent);
|
|
208
|
-
color: var(--s2-content-default);
|
|
209
|
-
border: 1px solid var(--s2-border-subtle);
|
|
210
|
-
border-radius: var(--s2-radius-xl);
|
|
211
|
-
box-shadow: var(--s2-shadow-elevated);
|
|
212
|
-
transform: translateX(calc(100% + 28px));
|
|
213
|
-
transition: transform var(--s2-transition-default);
|
|
214
|
-
backdrop-filter: blur(16px);
|
|
215
|
-
}
|
|
216
|
-
:host([open]) .sidebar {
|
|
217
|
-
transform: translateX(0);
|
|
218
|
-
}
|
|
219
|
-
.header {
|
|
220
|
-
display: flex;
|
|
221
|
-
align-items: center;
|
|
222
|
-
justify-content: space-between;
|
|
223
|
-
gap: 12px;
|
|
224
|
-
padding: 14px 16px 12px;
|
|
225
|
-
border-bottom: 1px solid var(--s2-border-subtle);
|
|
226
|
-
background: color-mix(in srgb, var(--s2-bg-layer-1) 92%, transparent);
|
|
227
|
-
}
|
|
228
|
-
.header__brand {
|
|
229
|
-
display: flex;
|
|
230
|
-
align-items: center;
|
|
231
|
-
gap: 12px;
|
|
232
|
-
min-width: 0;
|
|
233
|
-
}
|
|
234
|
-
.header__logo {
|
|
235
|
-
width: 34px;
|
|
236
|
-
height: 34px;
|
|
237
|
-
border-radius: 50%;
|
|
238
|
-
display: inline-flex;
|
|
239
|
-
align-items: center;
|
|
240
|
-
justify-content: center;
|
|
241
|
-
background: linear-gradient(180deg, rgba(239, 112, 0, 0.9), rgba(239, 112, 0, 0.7));
|
|
242
|
-
color: #fff;
|
|
243
|
-
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18);
|
|
244
|
-
}
|
|
245
|
-
.header__logo svg {
|
|
246
|
-
width: 18px;
|
|
247
|
-
height: 18px;
|
|
248
|
-
fill: currentColor;
|
|
249
|
-
}
|
|
250
|
-
.header__title {
|
|
251
|
-
font-size: 15px;
|
|
252
|
-
font-weight: 700;
|
|
253
|
-
letter-spacing: 0.01em;
|
|
254
|
-
}
|
|
255
|
-
.header__subtitle {
|
|
256
|
-
font-size: 11px;
|
|
257
|
-
color: var(--s2-content-secondary);
|
|
258
|
-
}
|
|
259
|
-
.header__close {
|
|
260
|
-
appearance: none;
|
|
261
|
-
border: 1px solid var(--s2-border-subtle);
|
|
262
|
-
background: var(--s2-bg-layer-2);
|
|
263
|
-
color: var(--s2-content-default);
|
|
264
|
-
width: 32px;
|
|
265
|
-
height: 32px;
|
|
266
|
-
border-radius: 50%;
|
|
267
|
-
cursor: pointer;
|
|
268
|
-
font-size: 18px;
|
|
269
|
-
line-height: 1;
|
|
270
|
-
}
|
|
271
|
-
.tab-bar {
|
|
272
|
-
display: grid;
|
|
273
|
-
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
274
|
-
gap: 8px;
|
|
275
|
-
padding: 12px 16px;
|
|
276
|
-
border-bottom: 1px solid var(--s2-border-subtle);
|
|
277
|
-
background: color-mix(in srgb, var(--s2-bg-layer-1) 96%, transparent);
|
|
278
|
-
}
|
|
279
|
-
.tab-bar__tab {
|
|
280
|
-
appearance: none;
|
|
281
|
-
border: 1px solid var(--s2-border-subtle);
|
|
282
|
-
border-radius: var(--s2-radius-default);
|
|
283
|
-
background: var(--s2-bg-layer-2);
|
|
284
|
-
color: var(--s2-content-secondary);
|
|
285
|
-
padding: 9px 8px;
|
|
286
|
-
font-size: 12px;
|
|
287
|
-
font-weight: 600;
|
|
288
|
-
cursor: pointer;
|
|
289
|
-
transition: background var(--s2-transition-default), color var(--s2-transition-default), border-color var(--s2-transition-default);
|
|
290
|
-
}
|
|
291
|
-
.tab-bar__tab--active {
|
|
292
|
-
background: color-mix(in srgb, var(--s2-accent) 18%, var(--s2-bg-layer-2));
|
|
293
|
-
color: var(--s2-content-default);
|
|
294
|
-
border-color: color-mix(in srgb, var(--s2-accent) 45%, var(--s2-border-default));
|
|
295
|
-
}
|
|
296
|
-
.viewport {
|
|
297
|
-
position: relative;
|
|
298
|
-
flex: 1;
|
|
299
|
-
min-height: 0;
|
|
300
|
-
background: var(--s2-bg-sunken);
|
|
301
|
-
}
|
|
302
|
-
iframe {
|
|
303
|
-
border: 0;
|
|
304
|
-
width: 100%;
|
|
305
|
-
height: 100%;
|
|
306
|
-
display: block;
|
|
307
|
-
background: var(--s2-bg-base);
|
|
308
|
-
}
|
|
309
|
-
.empty-state {
|
|
310
|
-
position: absolute;
|
|
311
|
-
inset: 0;
|
|
312
|
-
display: flex;
|
|
313
|
-
align-items: center;
|
|
314
|
-
justify-content: center;
|
|
315
|
-
padding: 24px;
|
|
316
|
-
text-align: center;
|
|
317
|
-
color: var(--s2-content-secondary);
|
|
318
|
-
font-size: 13px;
|
|
319
|
-
line-height: 1.5;
|
|
320
|
-
}
|
|
321
|
-
.empty-state[hidden] {
|
|
322
|
-
display: none;
|
|
323
|
-
}
|
|
324
|
-
</style>
|
|
325
|
-
<div class="backdrop" part="backdrop"></div>
|
|
326
|
-
<aside class="sidebar" part="sidebar" aria-label="SLICC overlay sidebar">
|
|
327
|
-
<header class="header">
|
|
328
|
-
<div class="header__brand">
|
|
329
|
-
<span class="header__logo">${D()}</span>
|
|
330
|
-
<div>
|
|
331
|
-
<div class="header__title">slicc</div>
|
|
332
|
-
<div class="header__subtitle">electron float</div>
|
|
333
|
-
</div>
|
|
334
|
-
</div>
|
|
335
|
-
<button type="button" class="header__close" aria-label="Close SLICC overlay">\xD7</button>
|
|
336
|
-
</header>
|
|
337
|
-
<div class="tab-bar" role="tablist" aria-label="SLICC overlay tabs">
|
|
338
|
-
${Q(e)}
|
|
339
|
-
</div>
|
|
340
|
-
<div class="viewport">
|
|
341
|
-
<iframe title="SLICC electron float"></iframe>
|
|
342
|
-
<div class="empty-state">Starting the local SLICC runtime\u2026</div>
|
|
343
|
-
</div>
|
|
344
|
-
</aside>
|
|
345
|
-
`,t.querySelector(".backdrop")?.addEventListener("click",()=>{this.dispatchEvent(new CustomEvent("slicc-overlay-close",{bubbles:!0,composed:!0}))}),t.querySelector(".header__close")?.addEventListener("click",()=>{this.dispatchEvent(new CustomEvent("slicc-overlay-close",{bubbles:!0,composed:!0}))}),t.querySelectorAll("[data-tab]").forEach(n=>{let a=i(n.dataset.tab);this.tabButtons.set(a,n),n.addEventListener("click",()=>{this.dispatchEvent(new CustomEvent("slicc-overlay-select-tab",{bubbles:!0,composed:!0,detail:{tab:a}}))})}),this.iframe=t.querySelector("iframe"),this.emptyState=t.querySelector(".empty-state"),this.iframe?.addEventListener("load",()=>{this.frameLoaded=!0,this.postActiveTab()})}sync(){let t=i(this.getAttribute("active-tab")),e=this.getAttribute("app-url")?.trim()??p;for(let[n,a]of this.tabButtons){let s=n===t;a.classList.toggle("tab-bar__tab--active",s),a.setAttribute("aria-selected",String(s))}this.emptyState?.toggleAttribute("hidden",!!e),this.syncFrameUrl(e,t),this.postActiveTab()}syncFrameUrl(t,e){if(this.iframe){if(!t){this.currentAppUrl&&(this.currentAppUrl=p,this.frameLoaded=!1,this.lastPostedTab=null,this.iframe.removeAttribute("src"));return}t!==this.currentAppUrl&&(this.currentAppUrl=t,this.frameLoaded=!1,this.lastPostedTab=null,this.iframe.src=Z(t,e))}}postActiveTab(){if(!this.frameLoaded||!this.iframe?.contentWindow)return;let t=i(this.getAttribute("active-tab"));this.lastPostedTab!==t&&(this.iframe.contentWindow.postMessage({type:M,tab:t},"*"),this.lastPostedTab=t)}},E=class extends HTMLElement{static observedAttributes=["open","active-tab","app-url","corner"];state=g();appUrlValue=p;syncingAttributes=!1;connectedCallback(){this.state=g({open:this.hasAttribute("open"),activeTab:i(this.getAttribute("active-tab")),corner:l(this.getAttribute("corner")??A(this.ownerDocument.defaultView))}),this.appUrlValue=this.getAttribute("app-url")?.trim()??p,this.shadowRoot||this.render(),this.syncChildren(),T(this.ownerDocument.defaultView,this.state.corner),this.ownerDocument.addEventListener("keydown",this.onKeyDown,!0)}disconnectedCallback(){this.ownerDocument.removeEventListener("keydown",this.onKeyDown,!0)}attributeChangedCallback(){this.syncingAttributes||(this.state=g({open:this.hasAttribute("open"),activeTab:i(this.getAttribute("active-tab")),corner:l(this.getAttribute("corner")??A(this.ownerDocument.defaultView))}),this.appUrlValue=this.getAttribute("app-url")?.trim()??p,this.syncChildren(),T(this.ownerDocument.defaultView,this.state.corner))}get open(){return this.state.open}set open(t){this.applyState(y(this.state,t))}get activeTab(){return this.state.activeTab}set activeTab(t){this.applyState(m(this.state,t))}get corner(){return this.state.corner}set corner(t){this.applyState(x(this.state,t))}get appUrl(){return this.appUrlValue}set appUrl(t){let e=t.trim();e!==this.appUrlValue&&(this.appUrlValue=e,this.syncingAttributes=!0,e?this.setAttribute("app-url",e):this.removeAttribute("app-url"),this.syncingAttributes=!1,this.syncChildren())}toggle(){this.applyState(P(this.state))}showSidebar(){this.applyState(y(this.state,!0))}hideSidebar(){this.applyState(y(this.state,!1))}applyState(t){t.open===this.state.open&&t.activeTab===this.state.activeTab&&t.corner===this.state.corner||(this.state=t,this.syncingAttributes=!0,this.toggleAttribute("open",t.open),this.setAttribute("active-tab",t.activeTab),this.setAttribute("corner",t.corner),this.syncingAttributes=!1,this.syncChildren(),T(this.ownerDocument.defaultView,this.state.corner))}onKeyDown=t=>{t.key!=="Escape"||!this.state.open||this.hideSidebar()};render(){let t=this.attachShadow({mode:"open"});t.innerHTML=`
|
|
346
|
-
<style>
|
|
347
|
-
${w}
|
|
348
|
-
:host {
|
|
349
|
-
all: initial;
|
|
350
|
-
position: fixed;
|
|
351
|
-
inset: 0;
|
|
352
|
-
display: block;
|
|
353
|
-
pointer-events: none;
|
|
354
|
-
z-index: 2147483647;
|
|
355
|
-
contain: layout style paint;
|
|
356
|
-
}
|
|
357
|
-
</style>
|
|
358
|
-
<${c}></${c}>
|
|
359
|
-
<${d}></${d}>
|
|
360
|
-
`,t.querySelector(c)?.addEventListener("slicc-overlay-toggle",()=>this.toggle()),t.querySelector(c)?.addEventListener("slicc-overlay-move",e=>{let n=e.detail?.corner;this.applyState(x(this.state,n))}),t.querySelector(d)?.addEventListener("slicc-overlay-close",()=>this.hideSidebar()),t.querySelector(d)?.addEventListener("slicc-overlay-select-tab",e=>{let n=e.detail?.tab;this.applyState(m(this.state,n))})}syncChildren(){let t=this.shadowRoot;if(!t)return;let e=t.querySelector(c),n=t.querySelector(d);!e||!n||(e.toggleAttribute("open",this.state.open),e.setAttribute("corner",this.state.corner),n.toggleAttribute("open",this.state.open),n.setAttribute("active-tab",this.state.activeTab),this.appUrlValue?n.setAttribute("app-url",this.appUrlValue):n.removeAttribute("app-url"))}};function tt(r=customElements){r.get(c)||r.define(c,O),r.get(d)||r.define(d,C),r.get(L)||r.define(L,E)}function $(r=document,t={}){tt(r.defaultView?.customElements??customElements);let e=r.getElementById(_),n;return e instanceof E?n=e:(e?.remove(),n=r.createElement(L)),n.id=_,n.isConnected||(r.body??r.documentElement).appendChild(n),typeof t.open=="boolean"&&(n.open=t.open),t.activeTab!==void 0&&(n.activeTab=i(t.activeTab)),t.appUrl!==void 0&&(n.appUrl=t.appUrl??p),t.corner!==void 0&&(n.corner=t.corner??A(r.defaultView)),n}function H(r=document){r.getElementById(_)?.remove()}function A(r){try{return l(r?.sessionStorage.getItem(V))}catch{return l(null)}}function T(r,t){try{r?.sessionStorage.setItem(V,t)}catch{}}function X(r,t,e){return Math.min(Math.max(r,t),e)}window.__SLICC_ELECTRON_OVERLAY__={inject(r={}){$(document,r)},remove(){H(document)}};})();
|
|
50
|
+
`;function it(r,t){try{let e=new URL(r,window.location.href);return e.searchParams.set("tab",t),e.toString()}catch{return r}}var P=class extends HTMLElement{static observedAttributes=["open","corner"];button=null;pointerState=null;suppressClick=!1;connectedCallback(){this.shadowRoot||this.render(),this.sync()}attributeChangedCallback(){this.sync()}render(){let t=this.attachShadow({mode:"open"}),e=this.ownerDocument,n=h;t.appendChild(X(e,`
|
|
51
|
+
${D}
|
|
52
|
+
:host {
|
|
53
|
+
all: initial;
|
|
54
|
+
position: fixed;
|
|
55
|
+
top: ${n}px;
|
|
56
|
+
right: ${n}px;
|
|
57
|
+
z-index: 1;
|
|
58
|
+
pointer-events: auto;
|
|
59
|
+
display: block;
|
|
60
|
+
font-family: var(--s2-font-family);
|
|
61
|
+
transition: top var(--s2-transition-default), right var(--s2-transition-default), bottom var(--s2-transition-default), left var(--s2-transition-default);
|
|
62
|
+
}
|
|
63
|
+
:host([corner="top-left"]) { top: ${n}px; right: auto; bottom: auto; left: ${n}px; }
|
|
64
|
+
:host([corner="top-right"]) { top: ${n}px; right: ${n}px; bottom: auto; left: auto; }
|
|
65
|
+
:host([corner="bottom-left"]) { top: auto; right: auto; bottom: ${n}px; left: ${n}px; }
|
|
66
|
+
:host([corner="bottom-right"]) { top: auto; right: ${n}px; bottom: ${n}px; left: auto; }
|
|
67
|
+
:host([dragging]) { transition: none; }
|
|
68
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
69
|
+
button {
|
|
70
|
+
width: 44px; height: 44px; position: relative;
|
|
71
|
+
border: 1px solid rgba(255, 255, 255, 0.62);
|
|
72
|
+
border-radius: var(--s2-radius-pill);
|
|
73
|
+
background: radial-gradient(circle at 30% 28%, rgba(255, 255, 255, 0.34), transparent 34%),
|
|
74
|
+
linear-gradient(160deg, #ffb15c 0%, color-mix(in srgb, var(--slicc-cone) 92%, #ffb15c) 34%, #a34b00 72%, #38220f 100%);
|
|
75
|
+
color: #fff;
|
|
76
|
+
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.18) inset, 0 0 0 4px rgba(0, 0, 0, 0.28), 0 12px 30px rgba(0, 0, 0, 0.4), 0 2px 10px rgba(0, 0, 0, 0.24);
|
|
77
|
+
cursor: grab;
|
|
78
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
79
|
+
transition: transform var(--s2-transition-default), background var(--s2-transition-default), border-color var(--s2-transition-default), box-shadow var(--s2-transition-default);
|
|
80
|
+
backdrop-filter: blur(12px) saturate(1.05);
|
|
81
|
+
touch-action: none; user-select: none; -webkit-user-select: none;
|
|
82
|
+
}
|
|
83
|
+
button::before {
|
|
84
|
+
content: ''; position: absolute; inset: -5px; border-radius: inherit;
|
|
85
|
+
border: 1px solid rgba(255, 255, 255, 0.32); box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.18); pointer-events: none;
|
|
86
|
+
}
|
|
87
|
+
:host([dragging]) button { cursor: grabbing; }
|
|
88
|
+
button:hover {
|
|
89
|
+
transform: translateY(-1px); border-color: rgba(255, 255, 255, 0.78);
|
|
90
|
+
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.24) inset, 0 0 0 4px rgba(0, 0, 0, 0.34), 0 16px 34px rgba(0, 0, 0, 0.44), 0 0 18px color-mix(in srgb, var(--slicc-cone) 40%, transparent);
|
|
91
|
+
}
|
|
92
|
+
button:active { transform: scale(0.96); }
|
|
93
|
+
button[aria-pressed="true"] {
|
|
94
|
+
border-color: rgba(255, 255, 255, 0.84);
|
|
95
|
+
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.24) inset, 0 0 0 4px rgba(0, 0, 0, 0.36), 0 16px 38px rgba(0, 0, 0, 0.46), 0 0 22px color-mix(in srgb, var(--slicc-cone) 52%, transparent);
|
|
96
|
+
}
|
|
97
|
+
svg { width: 20px; height: 20px; fill: currentColor; filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.55)); }
|
|
98
|
+
`));let a=e.createElement("button");a.type="button",a.setAttribute("aria-label","Toggle SLICC overlay"),a.appendChild(K(e)),t.appendChild(a),this.button=a,this.button?.addEventListener("click",o=>{if(this.suppressClick){this.suppressClick=!1,o.preventDefault(),o.stopImmediatePropagation();return}this.dispatchEvent(new CustomEvent("slicc-overlay-toggle",{bubbles:!0,composed:!0}))}),this.button?.addEventListener("pointerdown",this.onPointerDown),this.button?.addEventListener("pointermove",this.onPointerMove),this.button?.addEventListener("pointerup",this.onPointerUp),this.button?.addEventListener("pointercancel",this.onPointerCancel)}sync(){this.button?.setAttribute("aria-pressed",String(this.hasAttribute("open")))}onPointerDown=t=>{if(!t.isPrimary||t.button!==0||!this.button)return;let e=this.getBoundingClientRect();this.pointerState={pointerId:t.pointerId,startX:t.clientX,startY:t.clientY,startLeft:e.left,startTop:e.top,width:e.width,height:e.height,lastX:t.clientX,lastY:t.clientY,lastTimestamp:t.timeStamp,velocityX:0,velocityY:0,dragging:!1},this.suppressClick=!1,this.button.setPointerCapture(t.pointerId),t.preventDefault()};onPointerMove=t=>{let e=this.pointerState;if(!e||t.pointerId!==e.pointerId)return;let n=t.clientX-e.startX,a=t.clientY-e.startY,o=Math.hypot(n,a);!e.dragging&&R(o,0)&&(e.dragging=!0,this.setAttribute("dragging",""));let i=Math.max(t.timeStamp-e.lastTimestamp,1),s=(t.clientX-e.lastX)/i,d=(t.clientY-e.lastY)/i;if(e.velocityX=e.velocityX===0?s:e.velocityX*.35+s*.65,e.velocityY=e.velocityY===0?d:e.velocityY*.35+d*.65,e.lastX=t.clientX,e.lastY=t.clientY,e.lastTimestamp=t.timeStamp,!e.dragging)return;let u=Math.max(h,window.innerWidth-e.width-h),g=Math.max(h,window.innerHeight-e.height-h),E=W(e.startLeft+n,h,u),p=W(e.startTop+a,h,g);this.style.left=`${E}px`,this.style.top=`${p}px`,this.style.right="auto",this.style.bottom="auto",t.preventDefault()};onPointerUp=t=>{this.finishPointerInteraction(t)};onPointerCancel=t=>{this.finishPointerInteraction(t,!1)};finishPointerInteraction(t,e=!0){let n=this.pointerState;if(!n||t.pointerId!==n.pointerId||!this.button)return;let a=Math.hypot(t.clientX-n.startX,t.clientY-n.startY),o=Math.hypot(n.velocityX,n.velocityY);if(e&&(n.dragging||R(a,o))){let s=j({clientX:t.clientX,clientY:t.clientY,viewportWidth:window.innerWidth,viewportHeight:window.innerHeight,velocityXPxPerMs:n.velocityX,velocityYPxPerMs:n.velocityY});this.dispatchEvent(new CustomEvent("slicc-overlay-move",{bubbles:!0,composed:!0,detail:{corner:s}})),this.suppressClick=!0,t.preventDefault()}this.button.hasPointerCapture(t.pointerId)&&this.button.releasePointerCapture(t.pointerId),this.pointerState=null,this.removeAttribute("dragging"),this.resetDragStyles()}resetDragStyles(){this.style.left="",this.style.top="",this.style.right="",this.style.bottom=""}},Y=class extends HTMLElement{static observedAttributes=["open","active-tab","app-url"];tabButtons=new Map;iframe=null;emptyState=null;currentAppUrl=v;frameLoaded=!1;lastPostedTab=null;connectedCallback(){this.shadowRoot||this.render(),this.sync()}attributeChangedCallback(){this.sync()}render(){let t=this.attachShadow({mode:"open"}),e=this.ownerDocument,n=l(this.getAttribute("active-tab"));t.appendChild(X(e,`
|
|
99
|
+
${D}
|
|
100
|
+
:host { all: initial; position: fixed; inset: 0; display: block; pointer-events: none; font-family: var(--s2-font-family); }
|
|
101
|
+
:host([open]) { pointer-events: auto; }
|
|
102
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
103
|
+
.backdrop { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.18); opacity: 0; transition: opacity var(--s2-transition-default); }
|
|
104
|
+
:host([open]) .backdrop { opacity: 1; }
|
|
105
|
+
.sidebar {
|
|
106
|
+
position: absolute; top: 12px; right: 12px; bottom: 12px; width: min(440px, calc(100vw - 24px));
|
|
107
|
+
display: flex; flex-direction: column; overflow: hidden;
|
|
108
|
+
background: color-mix(in srgb, var(--s2-bg-base) 96%, transparent); color: var(--s2-content-default);
|
|
109
|
+
border: 1px solid var(--s2-border-subtle); border-radius: var(--s2-radius-xl);
|
|
110
|
+
box-shadow: var(--s2-shadow-elevated); transform: translateX(calc(100% + 28px));
|
|
111
|
+
transition: transform var(--s2-transition-default); backdrop-filter: blur(16px);
|
|
112
|
+
}
|
|
113
|
+
:host([open]) .sidebar { transform: translateX(0); }
|
|
114
|
+
.header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 16px 12px; border-bottom: 1px solid var(--s2-border-subtle); background: color-mix(in srgb, var(--s2-bg-layer-1) 92%, transparent); }
|
|
115
|
+
.header__brand { display: flex; align-items: center; gap: 12px; min-width: 0; }
|
|
116
|
+
.header__logo { width: 34px; height: 34px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; background: linear-gradient(180deg, rgba(239, 112, 0, 0.9), rgba(239, 112, 0, 0.7)); color: #fff; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18); }
|
|
117
|
+
.header__logo svg { width: 18px; height: 18px; fill: currentColor; }
|
|
118
|
+
.header__title { font-size: 15px; font-weight: 700; letter-spacing: 0.01em; }
|
|
119
|
+
.header__subtitle { font-size: 11px; color: var(--s2-content-secondary); }
|
|
120
|
+
.header__close { appearance: none; border: 1px solid var(--s2-border-subtle); background: var(--s2-bg-layer-2); color: var(--s2-content-default); width: 32px; height: 32px; border-radius: 50%; cursor: pointer; font-size: 18px; line-height: 1; }
|
|
121
|
+
.tab-bar { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; padding: 12px 16px; border-bottom: 1px solid var(--s2-border-subtle); background: color-mix(in srgb, var(--s2-bg-layer-1) 96%, transparent); }
|
|
122
|
+
.tab-bar__tab { appearance: none; border: 1px solid var(--s2-border-subtle); border-radius: var(--s2-radius-default); background: var(--s2-bg-layer-2); color: var(--s2-content-secondary); padding: 9px 8px; font-size: 12px; font-weight: 600; cursor: pointer; transition: background var(--s2-transition-default), color var(--s2-transition-default), border-color var(--s2-transition-default); }
|
|
123
|
+
.tab-bar__tab--active { background: color-mix(in srgb, var(--s2-accent) 18%, var(--s2-bg-layer-2)); color: var(--s2-content-default); border-color: color-mix(in srgb, var(--s2-accent) 45%, var(--s2-border-default)); }
|
|
124
|
+
.viewport { position: relative; flex: 1; min-height: 0; background: var(--s2-bg-sunken); }
|
|
125
|
+
iframe { border: 0; width: 100%; height: 100%; display: block; background: var(--s2-bg-base); }
|
|
126
|
+
.empty-state { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; padding: 24px; text-align: center; color: var(--s2-content-secondary); font-size: 13px; line-height: 1.5; }
|
|
127
|
+
.empty-state[hidden] { display: none; }
|
|
128
|
+
`));let a=e.createElement("div");a.className="backdrop",a.setAttribute("part","backdrop"),a.addEventListener("click",()=>{this.dispatchEvent(new CustomEvent("slicc-overlay-close",{bubbles:!0,composed:!0}))}),t.appendChild(a);let o=e.createElement("aside");o.className="sidebar",o.setAttribute("part","sidebar"),o.setAttribute("aria-label","SLICC overlay sidebar");let i=e.createElement("header");i.className="header";let s=e.createElement("div");s.className="header__brand";let d=e.createElement("span");d.className="header__logo",d.appendChild(K(e)),s.appendChild(d);let u=e.createElement("div"),g=e.createElement("div");g.className="header__title",g.textContent="slicc";let E=e.createElement("div");E.className="header__subtitle",E.textContent="electron float",u.appendChild(g),u.appendChild(E),s.appendChild(u),i.appendChild(s);let p=e.createElement("button");p.type="button",p.className="header__close",p.setAttribute("aria-label","Close SLICC overlay"),p.textContent="\xD7",p.addEventListener("click",()=>{this.dispatchEvent(new CustomEvent("slicc-overlay-close",{bubbles:!0,composed:!0}))}),i.appendChild(p),o.appendChild(i);let m=e.createElement("div");m.className="tab-bar",m.setAttribute("role","tablist"),m.setAttribute("aria-label","SLICC overlay tabs");for(let{id:f,label:Q}of H){let c=e.createElement("button");c.type="button",c.className="tab-bar__tab"+(f===n?" tab-bar__tab--active":""),c.setAttribute("role","tab"),c.setAttribute("aria-selected",String(f===n)),c.dataset.tab=f,c.textContent=Q,this.tabButtons.set(f,c),c.addEventListener("click",()=>{this.dispatchEvent(new CustomEvent("slicc-overlay-select-tab",{bubbles:!0,composed:!0,detail:{tab:f}}))}),m.appendChild(c)}o.appendChild(m);let y=e.createElement("div");y.className="viewport";let x=e.createElement("iframe");x.title="SLICC electron float",this.iframe=x,x.addEventListener("load",()=>{this.frameLoaded=!0,this.postActiveTab()}),y.appendChild(x);let S=e.createElement("div");S.className="empty-state",S.textContent="Starting the local SLICC runtime\u2026",this.emptyState=S,y.appendChild(S),o.appendChild(y),t.appendChild(o),this.iframe?.addEventListener("load",()=>{this.frameLoaded=!0,this.postActiveTab()})}sync(){let t=l(this.getAttribute("active-tab")),e=this.getAttribute("app-url")?.trim()??v;for(let[n,a]of this.tabButtons){let o=n===t;a.classList.toggle("tab-bar__tab--active",o),a.setAttribute("aria-selected",String(o))}this.emptyState?.toggleAttribute("hidden",!!e),this.syncFrameUrl(e,t),this.postActiveTab()}syncFrameUrl(t,e){if(this.iframe){if(!t){this.currentAppUrl&&(this.currentAppUrl=v,this.frameLoaded=!1,this.lastPostedTab=null,this.iframe.removeAttribute("src"));return}t!==this.currentAppUrl&&(this.currentAppUrl=t,this.frameLoaded=!1,this.lastPostedTab=null,this.iframe.src=it(t,e))}}postActiveTab(){if(!this.frameLoaded||!this.iframe?.contentWindow)return;let t=l(this.getAttribute("active-tab"));this.lastPostedTab!==t&&(this.iframe.contentWindow.postMessage({type:F,tab:t},"*"),this.lastPostedTab=t)}},A=class extends HTMLElement{static observedAttributes=["open","active-tab","app-url","corner"];state=T();appUrlValue=v;syncingAttributes=!1;connectedCallback(){this.state=T({open:this.hasAttribute("open"),activeTab:l(this.getAttribute("active-tab")),corner:b(this.getAttribute("corner")??M(this.ownerDocument.defaultView))}),this.appUrlValue=this.getAttribute("app-url")?.trim()??v,this.shadowRoot||this.render(),this.syncChildren(),N(this.ownerDocument.defaultView,this.state.corner),this.ownerDocument.addEventListener("keydown",this.onKeyDown,!0)}disconnectedCallback(){this.ownerDocument.removeEventListener("keydown",this.onKeyDown,!0)}attributeChangedCallback(){this.syncingAttributes||(this.state=T({open:this.hasAttribute("open"),activeTab:l(this.getAttribute("active-tab")),corner:b(this.getAttribute("corner")??M(this.ownerDocument.defaultView))}),this.appUrlValue=this.getAttribute("app-url")?.trim()??v,this.syncChildren(),N(this.ownerDocument.defaultView,this.state.corner))}get open(){return this.state.open}set open(t){this.applyState(_(this.state,t))}get activeTab(){return this.state.activeTab}set activeTab(t){this.applyState(w(this.state,t))}get corner(){return this.state.corner}set corner(t){this.applyState(I(this.state,t))}get appUrl(){return this.appUrlValue}set appUrl(t){let e=t.trim();e!==this.appUrlValue&&(this.appUrlValue=e,this.syncingAttributes=!0,e?this.setAttribute("app-url",e):this.removeAttribute("app-url"),this.syncingAttributes=!1,this.syncChildren())}toggle(){this.applyState(z(this.state))}showSidebar(){this.applyState(_(this.state,!0))}hideSidebar(){this.applyState(_(this.state,!1))}applyState(t){t.open===this.state.open&&t.activeTab===this.state.activeTab&&t.corner===this.state.corner||(this.state=t,this.syncingAttributes=!0,this.toggleAttribute("open",t.open),this.setAttribute("active-tab",t.activeTab),this.setAttribute("corner",t.corner),this.syncingAttributes=!1,this.syncChildren(),N(this.ownerDocument.defaultView,this.state.corner))}onKeyDown=t=>{t.key!=="Escape"||!this.state.open||this.hideSidebar()};render(){let t=this.attachShadow({mode:"open"}),e=this.ownerDocument;t.appendChild(X(e,`
|
|
129
|
+
${D}
|
|
130
|
+
:host {
|
|
131
|
+
all: initial;
|
|
132
|
+
position: fixed;
|
|
133
|
+
inset: 0;
|
|
134
|
+
display: block;
|
|
135
|
+
pointer-events: none;
|
|
136
|
+
z-index: 2147483647;
|
|
137
|
+
contain: layout style paint;
|
|
138
|
+
}
|
|
139
|
+
`));let n=e.createElement(L);n.addEventListener("slicc-overlay-toggle",()=>this.toggle()),n.addEventListener("slicc-overlay-move",o=>{let i=o.detail?.corner;this.applyState(I(this.state,i))}),t.appendChild(n);let a=e.createElement(O);a.addEventListener("slicc-overlay-close",()=>this.hideSidebar()),a.addEventListener("slicc-overlay-select-tab",o=>{let i=o.detail?.tab;this.applyState(w(this.state,i))}),t.appendChild(a)}syncChildren(){let t=this.shadowRoot;if(!t)return;let e=t.querySelector(L),n=t.querySelector(O);!e||!n||(e.toggleAttribute("open",this.state.open),e.setAttribute("corner",this.state.corner),n.toggleAttribute("open",this.state.open),n.setAttribute("active-tab",this.state.activeTab),this.appUrlValue?n.setAttribute("app-url",this.appUrlValue):n.removeAttribute("app-url"))}};function st(r=customElements){r.get(L)||r.define(L,P),r.get(O)||r.define(O,Y),r.get(U)||r.define(U,A)}function q(r=document,t={}){st(r.defaultView?.customElements??customElements);let e=r.getElementById(k),n;return e instanceof A?n=e:(e?.remove(),n=r.createElement(U)),n.id=k,n.isConnected||(r.body??r.documentElement).appendChild(n),typeof t.open=="boolean"&&(n.open=t.open),t.activeTab!==void 0&&(n.activeTab=l(t.activeTab)),t.appUrl!==void 0&&(n.appUrl=t.appUrl??v),t.corner!==void 0&&(n.corner=t.corner??M(r.defaultView)),n}function J(r=document){r.getElementById(k)?.remove()}function M(r){try{return b(r?.sessionStorage.getItem(G))}catch{return b(null)}}function N(r,t){try{r?.sessionStorage.setItem(G,t)}catch{}}function W(r,t,e){return Math.min(Math.max(r,t),e)}window.__SLICC_ELECTRON_OVERLAY__={inject(r={}){try{q(document,r)}catch(t){console.error("[slicc-overlay] Injection failed:",t)}},remove(){try{J(document)}catch(r){console.error("[slicc-overlay] Removal failed:",r)}}};})();
|