mirror of
https://github.com/microsoft/playwright-mcp.git
synced 2026-02-01 20:23:38 +00:00
chore: monorepo (#1325)
This commit is contained in:
222
packages/extension/src/background.ts
Normal file
222
packages/extension/src/background.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RelayConnection, debugLog } from './relayConnection';
|
||||
|
||||
type PageMessage = {
|
||||
type: 'connectToMCPRelay';
|
||||
mcpRelayUrl: string;
|
||||
} | {
|
||||
type: 'getTabs';
|
||||
} | {
|
||||
type: 'connectToTab';
|
||||
tabId?: number;
|
||||
windowId?: number;
|
||||
mcpRelayUrl: string;
|
||||
} | {
|
||||
type: 'getConnectionStatus';
|
||||
} | {
|
||||
type: 'disconnect';
|
||||
};
|
||||
|
||||
class TabShareExtension {
|
||||
private _activeConnection: RelayConnection | undefined;
|
||||
private _connectedTabId: number | null = null;
|
||||
private _pendingTabSelection = new Map<number, { connection: RelayConnection, timerId?: number }>();
|
||||
|
||||
constructor() {
|
||||
chrome.tabs.onRemoved.addListener(this._onTabRemoved.bind(this));
|
||||
chrome.tabs.onUpdated.addListener(this._onTabUpdated.bind(this));
|
||||
chrome.tabs.onActivated.addListener(this._onTabActivated.bind(this));
|
||||
chrome.runtime.onMessage.addListener(this._onMessage.bind(this));
|
||||
chrome.action.onClicked.addListener(this._onActionClicked.bind(this));
|
||||
}
|
||||
|
||||
// Promise-based message handling is not supported in Chrome: https://issues.chromium.org/issues/40753031
|
||||
private _onMessage(message: PageMessage, sender: chrome.runtime.MessageSender, sendResponse: (response: any) => void) {
|
||||
switch (message.type) {
|
||||
case 'connectToMCPRelay':
|
||||
this._connectToRelay(sender.tab!.id!, message.mcpRelayUrl).then(
|
||||
() => sendResponse({ success: true }),
|
||||
(error: any) => sendResponse({ success: false, error: error.message }));
|
||||
return true;
|
||||
case 'getTabs':
|
||||
this._getTabs().then(
|
||||
tabs => sendResponse({ success: true, tabs, currentTabId: sender.tab?.id }),
|
||||
(error: any) => sendResponse({ success: false, error: error.message }));
|
||||
return true;
|
||||
case 'connectToTab':
|
||||
const tabId = message.tabId || sender.tab?.id!;
|
||||
const windowId = message.windowId || sender.tab?.windowId!;
|
||||
this._connectTab(sender.tab!.id!, tabId, windowId, message.mcpRelayUrl!).then(
|
||||
() => sendResponse({ success: true }),
|
||||
(error: any) => sendResponse({ success: false, error: error.message }));
|
||||
return true; // Return true to indicate that the response will be sent asynchronously
|
||||
case 'getConnectionStatus':
|
||||
sendResponse({
|
||||
connectedTabId: this._connectedTabId
|
||||
});
|
||||
return false;
|
||||
case 'disconnect':
|
||||
this._disconnect().then(
|
||||
() => sendResponse({ success: true }),
|
||||
(error: any) => sendResponse({ success: false, error: error.message }));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async _connectToRelay(selectorTabId: number, mcpRelayUrl: string): Promise<void> {
|
||||
try {
|
||||
debugLog(`Connecting to relay at ${mcpRelayUrl}`);
|
||||
const socket = new WebSocket(mcpRelayUrl);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
socket.onopen = () => resolve();
|
||||
socket.onerror = () => reject(new Error('WebSocket error'));
|
||||
setTimeout(() => reject(new Error('Connection timeout')), 5000);
|
||||
});
|
||||
|
||||
const connection = new RelayConnection(socket);
|
||||
connection.onclose = () => {
|
||||
debugLog('Connection closed');
|
||||
this._pendingTabSelection.delete(selectorTabId);
|
||||
// TODO: show error in the selector tab?
|
||||
};
|
||||
this._pendingTabSelection.set(selectorTabId, { connection });
|
||||
debugLog(`Connected to MCP relay`);
|
||||
} catch (error: any) {
|
||||
const message = `Failed to connect to MCP relay: ${error.message}`;
|
||||
debugLog(message);
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
private async _connectTab(selectorTabId: number, tabId: number, windowId: number, mcpRelayUrl: string): Promise<void> {
|
||||
try {
|
||||
debugLog(`Connecting tab ${tabId} to relay at ${mcpRelayUrl}`);
|
||||
try {
|
||||
this._activeConnection?.close('Another connection is requested');
|
||||
} catch (error: any) {
|
||||
debugLog(`Error closing active connection:`, error);
|
||||
}
|
||||
await this._setConnectedTabId(null);
|
||||
|
||||
this._activeConnection = this._pendingTabSelection.get(selectorTabId)?.connection;
|
||||
if (!this._activeConnection)
|
||||
throw new Error('No active MCP relay connection');
|
||||
this._pendingTabSelection.delete(selectorTabId);
|
||||
|
||||
this._activeConnection.setTabId(tabId);
|
||||
this._activeConnection.onclose = () => {
|
||||
debugLog('MCP connection closed');
|
||||
this._activeConnection = undefined;
|
||||
void this._setConnectedTabId(null);
|
||||
};
|
||||
|
||||
await Promise.all([
|
||||
this._setConnectedTabId(tabId),
|
||||
chrome.tabs.update(tabId, { active: true }),
|
||||
chrome.windows.update(windowId, { focused: true }),
|
||||
]);
|
||||
debugLog(`Connected to MCP bridge`);
|
||||
} catch (error: any) {
|
||||
await this._setConnectedTabId(null);
|
||||
debugLog(`Failed to connect tab ${tabId}:`, error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async _setConnectedTabId(tabId: number | null): Promise<void> {
|
||||
const oldTabId = this._connectedTabId;
|
||||
this._connectedTabId = tabId;
|
||||
if (oldTabId && oldTabId !== tabId)
|
||||
await this._updateBadge(oldTabId, { text: '' });
|
||||
if (tabId)
|
||||
await this._updateBadge(tabId, { text: '✓', color: '#4CAF50', title: 'Connected to MCP client' });
|
||||
}
|
||||
|
||||
private async _updateBadge(tabId: number, { text, color, title }: { text: string; color?: string, title?: string }): Promise<void> {
|
||||
try {
|
||||
await chrome.action.setBadgeText({ tabId, text });
|
||||
await chrome.action.setTitle({ tabId, title: title || '' });
|
||||
if (color)
|
||||
await chrome.action.setBadgeBackgroundColor({ tabId, color });
|
||||
} catch (error: any) {
|
||||
// Ignore errors as the tab may be closed already.
|
||||
}
|
||||
}
|
||||
|
||||
private async _onTabRemoved(tabId: number): Promise<void> {
|
||||
const pendingConnection = this._pendingTabSelection.get(tabId)?.connection;
|
||||
if (pendingConnection) {
|
||||
this._pendingTabSelection.delete(tabId);
|
||||
pendingConnection.close('Browser tab closed');
|
||||
return;
|
||||
}
|
||||
if (this._connectedTabId !== tabId)
|
||||
return;
|
||||
this._activeConnection?.close('Browser tab closed');
|
||||
this._activeConnection = undefined;
|
||||
this._connectedTabId = null;
|
||||
}
|
||||
|
||||
private _onTabActivated(activeInfo: chrome.tabs.TabActiveInfo) {
|
||||
for (const [tabId, pending] of this._pendingTabSelection) {
|
||||
if (tabId === activeInfo.tabId) {
|
||||
if (pending.timerId) {
|
||||
clearTimeout(pending.timerId);
|
||||
pending.timerId = undefined;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!pending.timerId) {
|
||||
pending.timerId = setTimeout(() => {
|
||||
const existed = this._pendingTabSelection.delete(tabId);
|
||||
if (existed) {
|
||||
pending.connection.close('Tab has been inactive for 5 seconds');
|
||||
chrome.tabs.sendMessage(tabId, { type: 'connectionTimeout' });
|
||||
}
|
||||
}, 5000);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _onTabUpdated(tabId: number, changeInfo: chrome.tabs.TabChangeInfo, tab: chrome.tabs.Tab) {
|
||||
if (this._connectedTabId === tabId)
|
||||
void this._setConnectedTabId(tabId);
|
||||
}
|
||||
|
||||
private async _getTabs(): Promise<chrome.tabs.Tab[]> {
|
||||
const tabs = await chrome.tabs.query({});
|
||||
return tabs.filter(tab => tab.url && !['chrome:', 'edge:', 'devtools:'].some(scheme => tab.url!.startsWith(scheme)));
|
||||
}
|
||||
|
||||
private async _onActionClicked(): Promise<void> {
|
||||
await chrome.tabs.create({
|
||||
url: chrome.runtime.getURL('status.html'),
|
||||
active: true
|
||||
});
|
||||
}
|
||||
|
||||
private async _disconnect(): Promise<void> {
|
||||
this._activeConnection?.close('User disconnected');
|
||||
this._activeConnection = undefined;
|
||||
await this._setConnectedTabId(null);
|
||||
}
|
||||
}
|
||||
|
||||
new TabShareExtension();
|
||||
178
packages/extension/src/relayConnection.ts
Normal file
178
packages/extension/src/relayConnection.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export function debugLog(...args: unknown[]): void {
|
||||
const enabled = true;
|
||||
if (enabled) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[Extension]', ...args);
|
||||
}
|
||||
}
|
||||
|
||||
type ProtocolCommand = {
|
||||
id: number;
|
||||
method: string;
|
||||
params?: any;
|
||||
};
|
||||
|
||||
type ProtocolResponse = {
|
||||
id?: number;
|
||||
method?: string;
|
||||
params?: any;
|
||||
result?: any;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export class RelayConnection {
|
||||
private _debuggee: chrome.debugger.Debuggee;
|
||||
private _ws: WebSocket;
|
||||
private _eventListener: (source: chrome.debugger.DebuggerSession, method: string, params: any) => void;
|
||||
private _detachListener: (source: chrome.debugger.Debuggee, reason: string) => void;
|
||||
private _tabPromise: Promise<void>;
|
||||
private _tabPromiseResolve!: () => void;
|
||||
private _closed = false;
|
||||
|
||||
onclose?: () => void;
|
||||
|
||||
constructor(ws: WebSocket) {
|
||||
this._debuggee = { };
|
||||
this._tabPromise = new Promise(resolve => this._tabPromiseResolve = resolve);
|
||||
this._ws = ws;
|
||||
this._ws.onmessage = this._onMessage.bind(this);
|
||||
this._ws.onclose = () => this._onClose();
|
||||
// Store listeners for cleanup
|
||||
this._eventListener = this._onDebuggerEvent.bind(this);
|
||||
this._detachListener = this._onDebuggerDetach.bind(this);
|
||||
chrome.debugger.onEvent.addListener(this._eventListener);
|
||||
chrome.debugger.onDetach.addListener(this._detachListener);
|
||||
}
|
||||
|
||||
// Either setTabId or close is called after creating the connection.
|
||||
setTabId(tabId: number): void {
|
||||
this._debuggee = { tabId };
|
||||
this._tabPromiseResolve();
|
||||
}
|
||||
|
||||
close(message: string): void {
|
||||
this._ws.close(1000, message);
|
||||
// ws.onclose is called asynchronously, so we call it here to avoid forwarding
|
||||
// CDP events to the closed connection.
|
||||
this._onClose();
|
||||
}
|
||||
|
||||
private _onClose() {
|
||||
if (this._closed)
|
||||
return;
|
||||
this._closed = true;
|
||||
chrome.debugger.onEvent.removeListener(this._eventListener);
|
||||
chrome.debugger.onDetach.removeListener(this._detachListener);
|
||||
chrome.debugger.detach(this._debuggee).catch(() => {});
|
||||
this.onclose?.();
|
||||
}
|
||||
|
||||
private _onDebuggerEvent(source: chrome.debugger.DebuggerSession, method: string, params: any): void {
|
||||
if (source.tabId !== this._debuggee.tabId)
|
||||
return;
|
||||
debugLog('Forwarding CDP event:', method, params);
|
||||
const sessionId = source.sessionId;
|
||||
this._sendMessage({
|
||||
method: 'forwardCDPEvent',
|
||||
params: {
|
||||
sessionId,
|
||||
method,
|
||||
params,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _onDebuggerDetach(source: chrome.debugger.Debuggee, reason: string): void {
|
||||
if (source.tabId !== this._debuggee.tabId)
|
||||
return;
|
||||
this.close(`Debugger detached: ${reason}`);
|
||||
this._debuggee = { };
|
||||
}
|
||||
|
||||
private _onMessage(event: MessageEvent): void {
|
||||
this._onMessageAsync(event).catch(e => debugLog('Error handling message:', e));
|
||||
}
|
||||
|
||||
private async _onMessageAsync(event: MessageEvent): Promise<void> {
|
||||
let message: ProtocolCommand;
|
||||
try {
|
||||
message = JSON.parse(event.data);
|
||||
} catch (error: any) {
|
||||
debugLog('Error parsing message:', error);
|
||||
this._sendError(-32700, `Error parsing message: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
debugLog('Received message:', message);
|
||||
|
||||
const response: ProtocolResponse = {
|
||||
id: message.id,
|
||||
};
|
||||
try {
|
||||
response.result = await this._handleCommand(message);
|
||||
} catch (error: any) {
|
||||
debugLog('Error handling command:', error);
|
||||
response.error = error.message;
|
||||
}
|
||||
debugLog('Sending response:', response);
|
||||
this._sendMessage(response);
|
||||
}
|
||||
|
||||
private async _handleCommand(message: ProtocolCommand): Promise<any> {
|
||||
if (message.method === 'attachToTab') {
|
||||
await this._tabPromise;
|
||||
debugLog('Attaching debugger to tab:', this._debuggee);
|
||||
await chrome.debugger.attach(this._debuggee, '1.3');
|
||||
const result: any = await chrome.debugger.sendCommand(this._debuggee, 'Target.getTargetInfo');
|
||||
return {
|
||||
targetInfo: result?.targetInfo,
|
||||
};
|
||||
}
|
||||
if (!this._debuggee.tabId)
|
||||
throw new Error('No tab is connected. Please go to the Playwright MCP extension and select the tab you want to connect to.');
|
||||
if (message.method === 'forwardCDPCommand') {
|
||||
const { sessionId, method, params } = message.params;
|
||||
debugLog('CDP command:', method, params);
|
||||
const debuggerSession: chrome.debugger.DebuggerSession = {
|
||||
...this._debuggee,
|
||||
sessionId,
|
||||
};
|
||||
// Forward CDP command to chrome.debugger
|
||||
return await chrome.debugger.sendCommand(
|
||||
debuggerSession,
|
||||
method,
|
||||
params
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _sendError(code: number, message: string): void {
|
||||
this._sendMessage({
|
||||
error: {
|
||||
code,
|
||||
message,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _sendMessage(message: any): void {
|
||||
if (this._ws.readyState === WebSocket.OPEN)
|
||||
this._ws.send(JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
142
packages/extension/src/ui/authToken.css
Normal file
142
packages/extension/src/ui/authToken.css
Normal file
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.auth-token-section {
|
||||
margin: 16px 0;
|
||||
padding: 16px;
|
||||
background-color: #f6f8fa;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.auth-token-description {
|
||||
font-size: 12px;
|
||||
color: #656d76;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.auth-token-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background-color: #ffffff;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.auth-token-code {
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
font-size: 12px;
|
||||
color: #1f2328;
|
||||
border: none;
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.auth-token-refresh {
|
||||
flex: none;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--color-fg-muted);
|
||||
background: transparent;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.auth-token-refresh svg {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.auth-token-refresh:not(:disabled):hover {
|
||||
background-color: var(--color-btn-selected-bg);
|
||||
}
|
||||
|
||||
.auth-token-example-section {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.auth-token-example-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 8px 0;
|
||||
font-size: 12px;
|
||||
color: #656d76;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.auth-token-example-toggle:hover {
|
||||
color: #1f2328;
|
||||
}
|
||||
|
||||
.auth-token-chevron {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform: rotate(-90deg);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.auth-token-chevron.expanded {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
.auth-token-chevron svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.auth-token-chevron .octicon {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.auth-token-example-content {
|
||||
margin-top: 12px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.auth-token-example-description {
|
||||
font-size: 12px;
|
||||
color: #656d76;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.auth-token-example-config {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
background-color: #ffffff;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.auth-token-example-code {
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
font-size: 11px;
|
||||
color: #1f2328;
|
||||
white-space: pre;
|
||||
flex: 1;
|
||||
line-height: 1.4;
|
||||
}
|
||||
118
packages/extension/src/ui/authToken.tsx
Normal file
118
packages/extension/src/ui/authToken.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { CopyToClipboard } from './copyToClipboard';
|
||||
import * as icons from './icons';
|
||||
import './authToken.css';
|
||||
|
||||
export const AuthTokenSection: React.FC<{}> = ({}) => {
|
||||
const [authToken, setAuthToken] = useState<string>(getOrCreateAuthToken);
|
||||
const [isExampleExpanded, setIsExampleExpanded] = useState<boolean>(false);
|
||||
|
||||
const onRegenerateToken = useCallback(() => {
|
||||
const newToken = generateAuthToken();
|
||||
localStorage.setItem('auth-token', newToken);
|
||||
setAuthToken(newToken);
|
||||
}, []);
|
||||
|
||||
const toggleExample = useCallback(() => {
|
||||
setIsExampleExpanded(!isExampleExpanded);
|
||||
}, [isExampleExpanded]);
|
||||
|
||||
return (
|
||||
<div className='auth-token-section'>
|
||||
<div className='auth-token-description'>
|
||||
Set this environment variable to bypass the connection dialog:
|
||||
</div>
|
||||
<div className='auth-token-container'>
|
||||
<code className='auth-token-code'>{authTokenCode(authToken)}</code>
|
||||
<button className='auth-token-refresh' title='Generate new token' aria-label='Generate new token'onClick={onRegenerateToken}>{icons.refresh()}</button>
|
||||
<CopyToClipboard value={authTokenCode(authToken)} />
|
||||
</div>
|
||||
|
||||
<div className='auth-token-example-section'>
|
||||
<button
|
||||
className='auth-token-example-toggle'
|
||||
onClick={toggleExample}
|
||||
aria-expanded={isExampleExpanded}
|
||||
title={isExampleExpanded ? 'Hide example config' : 'Show example config'}
|
||||
>
|
||||
<span className={`auth-token-chevron ${isExampleExpanded ? 'expanded' : ''}`}>
|
||||
{icons.chevronDown()}
|
||||
</span>
|
||||
Example MCP server configuration
|
||||
</button>
|
||||
|
||||
{isExampleExpanded && (
|
||||
<div className='auth-token-example-content'>
|
||||
<div className='auth-token-example-description'>
|
||||
Add this configuration to your MCP client (e.g., VS Code) to connect to the Playwright MCP Bridge:
|
||||
</div>
|
||||
<div className='auth-token-example-config'>
|
||||
<code className='auth-token-example-code'>{exampleConfig(authToken)}</code>
|
||||
<CopyToClipboard value={exampleConfig(authToken)} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function authTokenCode(authToken: string) {
|
||||
return `PLAYWRIGHT_MCP_EXTENSION_TOKEN=${authToken}`;
|
||||
}
|
||||
|
||||
function exampleConfig(authToken: string) {
|
||||
return `{
|
||||
"mcpServers": {
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--extension"],
|
||||
"env": {
|
||||
"PLAYWRIGHT_MCP_EXTENSION_TOKEN":
|
||||
"${authToken}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
}
|
||||
|
||||
function generateAuthToken(): string {
|
||||
// Generate a cryptographically secure random token
|
||||
const array = new Uint8Array(32);
|
||||
crypto.getRandomValues(array);
|
||||
// Convert to base64 and make it URL-safe
|
||||
return btoa(String.fromCharCode.apply(null, Array.from(array)))
|
||||
.replace(/[+/=]/g, match => {
|
||||
switch (match) {
|
||||
case '+': return '-';
|
||||
case '/': return '_';
|
||||
case '=': return '';
|
||||
default: return match;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const getOrCreateAuthToken = (): string => {
|
||||
let token = localStorage.getItem('auth-token');
|
||||
if (!token) {
|
||||
token = generateAuthToken();
|
||||
localStorage.setItem('auth-token', token);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
891
packages/extension/src/ui/colors.css
Normal file
891
packages/extension/src/ui/colors.css
Normal file
@@ -0,0 +1,891 @@
|
||||
/* The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021 GitHub Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE. */
|
||||
|
||||
:root {
|
||||
--color-canvas-default-transparent: rgba(255,255,255,0);
|
||||
--color-marketing-icon-primary: #218bff;
|
||||
--color-marketing-icon-secondary: #54aeff;
|
||||
--color-diff-blob-addition-num-text: #24292f;
|
||||
--color-diff-blob-addition-fg: #24292f;
|
||||
--color-diff-blob-addition-num-bg: #CCFFD8;
|
||||
--color-diff-blob-addition-line-bg: #E6FFEC;
|
||||
--color-diff-blob-addition-word-bg: #ABF2BC;
|
||||
--color-diff-blob-deletion-num-text: #24292f;
|
||||
--color-diff-blob-deletion-fg: #24292f;
|
||||
--color-diff-blob-deletion-num-bg: #FFD7D5;
|
||||
--color-diff-blob-deletion-line-bg: #FFEBE9;
|
||||
--color-diff-blob-deletion-word-bg: rgba(255,129,130,0.4);
|
||||
--color-diff-blob-hunk-num-bg: rgba(84,174,255,0.4);
|
||||
--color-diff-blob-expander-icon: #57606a;
|
||||
--color-diff-blob-selected-line-highlight-mix-blend-mode: multiply;
|
||||
--color-diffstat-deletion-border: rgba(27,31,36,0.15);
|
||||
--color-diffstat-addition-border: rgba(27,31,36,0.15);
|
||||
--color-diffstat-addition-bg: #2da44e;
|
||||
--color-search-keyword-hl: #fff8c5;
|
||||
--color-prettylights-syntax-comment: #6e7781;
|
||||
--color-prettylights-syntax-constant: #0550ae;
|
||||
--color-prettylights-syntax-entity: #8250df;
|
||||
--color-prettylights-syntax-storage-modifier-import: #24292f;
|
||||
--color-prettylights-syntax-entity-tag: #116329;
|
||||
--color-prettylights-syntax-keyword: #cf222e;
|
||||
--color-prettylights-syntax-string: #0a3069;
|
||||
--color-prettylights-syntax-variable: #953800;
|
||||
--color-prettylights-syntax-brackethighlighter-unmatched: #82071e;
|
||||
--color-prettylights-syntax-invalid-illegal-text: #f6f8fa;
|
||||
--color-prettylights-syntax-invalid-illegal-bg: #82071e;
|
||||
--color-prettylights-syntax-carriage-return-text: #f6f8fa;
|
||||
--color-prettylights-syntax-carriage-return-bg: #cf222e;
|
||||
--color-prettylights-syntax-string-regexp: #116329;
|
||||
--color-prettylights-syntax-markup-list: #3b2300;
|
||||
--color-prettylights-syntax-markup-heading: #0550ae;
|
||||
--color-prettylights-syntax-markup-italic: #24292f;
|
||||
--color-prettylights-syntax-markup-bold: #24292f;
|
||||
--color-prettylights-syntax-markup-deleted-text: #82071e;
|
||||
--color-prettylights-syntax-markup-deleted-bg: #FFEBE9;
|
||||
--color-prettylights-syntax-markup-inserted-text: #116329;
|
||||
--color-prettylights-syntax-markup-inserted-bg: #dafbe1;
|
||||
--color-prettylights-syntax-markup-changed-text: #953800;
|
||||
--color-prettylights-syntax-markup-changed-bg: #ffd8b5;
|
||||
--color-prettylights-syntax-markup-ignored-text: #eaeef2;
|
||||
--color-prettylights-syntax-markup-ignored-bg: #0550ae;
|
||||
--color-prettylights-syntax-meta-diff-range: #8250df;
|
||||
--color-prettylights-syntax-brackethighlighter-angle: #57606a;
|
||||
--color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f;
|
||||
--color-prettylights-syntax-constant-other-reference-link: #0a3069;
|
||||
--color-codemirror-text: #24292f;
|
||||
--color-codemirror-bg: #ffffff;
|
||||
--color-codemirror-gutters-bg: #ffffff;
|
||||
--color-codemirror-guttermarker-text: #ffffff;
|
||||
--color-codemirror-guttermarker-subtle-text: #6e7781;
|
||||
--color-codemirror-linenumber-text: #57606a;
|
||||
--color-codemirror-cursor: #24292f;
|
||||
--color-codemirror-selection-bg: rgba(84,174,255,0.4);
|
||||
--color-codemirror-activeline-bg: rgba(234,238,242,0.5);
|
||||
--color-codemirror-matchingbracket-text: #24292f;
|
||||
--color-codemirror-lines-bg: #ffffff;
|
||||
--color-codemirror-syntax-comment: #24292f;
|
||||
--color-codemirror-syntax-constant: #0550ae;
|
||||
--color-codemirror-syntax-entity: #8250df;
|
||||
--color-codemirror-syntax-keyword: #cf222e;
|
||||
--color-codemirror-syntax-storage: #cf222e;
|
||||
--color-codemirror-syntax-string: #0a3069;
|
||||
--color-codemirror-syntax-support: #0550ae;
|
||||
--color-codemirror-syntax-variable: #953800;
|
||||
--color-checks-bg: #24292f;
|
||||
--color-checks-run-border-width: 0px;
|
||||
--color-checks-container-border-width: 0px;
|
||||
--color-checks-text-primary: #f6f8fa;
|
||||
--color-checks-text-secondary: #8c959f;
|
||||
--color-checks-text-link: #54aeff;
|
||||
--color-checks-btn-icon: #afb8c1;
|
||||
--color-checks-btn-hover-icon: #f6f8fa;
|
||||
--color-checks-btn-hover-bg: rgba(255,255,255,0.125);
|
||||
--color-checks-input-text: #eaeef2;
|
||||
--color-checks-input-placeholder-text: #8c959f;
|
||||
--color-checks-input-focus-text: #8c959f;
|
||||
--color-checks-input-bg: #32383f;
|
||||
--color-checks-input-shadow: none;
|
||||
--color-checks-donut-error: #fa4549;
|
||||
--color-checks-donut-pending: #bf8700;
|
||||
--color-checks-donut-success: #2da44e;
|
||||
--color-checks-donut-neutral: #afb8c1;
|
||||
--color-checks-dropdown-text: #afb8c1;
|
||||
--color-checks-dropdown-bg: #32383f;
|
||||
--color-checks-dropdown-border: #424a53;
|
||||
--color-checks-dropdown-shadow: rgba(27,31,36,0.3);
|
||||
--color-checks-dropdown-hover-text: #f6f8fa;
|
||||
--color-checks-dropdown-hover-bg: #424a53;
|
||||
--color-checks-dropdown-btn-hover-text: #f6f8fa;
|
||||
--color-checks-dropdown-btn-hover-bg: #32383f;
|
||||
--color-checks-scrollbar-thumb-bg: #57606a;
|
||||
--color-checks-header-label-text: #d0d7de;
|
||||
--color-checks-header-label-open-text: #f6f8fa;
|
||||
--color-checks-header-border: #32383f;
|
||||
--color-checks-header-icon: #8c959f;
|
||||
--color-checks-line-text: #d0d7de;
|
||||
--color-checks-line-num-text: rgba(140,149,159,0.75);
|
||||
--color-checks-line-timestamp-text: #8c959f;
|
||||
--color-checks-line-hover-bg: #32383f;
|
||||
--color-checks-line-selected-bg: rgba(33,139,255,0.15);
|
||||
--color-checks-line-selected-num-text: #54aeff;
|
||||
--color-checks-line-dt-fm-text: #24292f;
|
||||
--color-checks-line-dt-fm-bg: #9a6700;
|
||||
--color-checks-gate-bg: rgba(125,78,0,0.15);
|
||||
--color-checks-gate-text: #d0d7de;
|
||||
--color-checks-gate-waiting-text: #afb8c1;
|
||||
--color-checks-step-header-open-bg: #32383f;
|
||||
--color-checks-step-error-text: #ff8182;
|
||||
--color-checks-step-warning-text: #d4a72c;
|
||||
--color-checks-logline-text: #8c959f;
|
||||
--color-checks-logline-num-text: rgba(140,149,159,0.75);
|
||||
--color-checks-logline-debug-text: #c297ff;
|
||||
--color-checks-logline-error-text: #d0d7de;
|
||||
--color-checks-logline-error-num-text: #ff8182;
|
||||
--color-checks-logline-error-bg: rgba(164,14,38,0.15);
|
||||
--color-checks-logline-warning-text: #d0d7de;
|
||||
--color-checks-logline-warning-num-text: #d4a72c;
|
||||
--color-checks-logline-warning-bg: rgba(125,78,0,0.15);
|
||||
--color-checks-logline-command-text: #54aeff;
|
||||
--color-checks-logline-section-text: #4ac26b;
|
||||
--color-checks-ansi-black: #24292f;
|
||||
--color-checks-ansi-black-bright: #32383f;
|
||||
--color-checks-ansi-white: #d0d7de;
|
||||
--color-checks-ansi-white-bright: #d0d7de;
|
||||
--color-checks-ansi-gray: #8c959f;
|
||||
--color-checks-ansi-red: #ff8182;
|
||||
--color-checks-ansi-red-bright: #ffaba8;
|
||||
--color-checks-ansi-green: #4ac26b;
|
||||
--color-checks-ansi-green-bright: #6fdd8b;
|
||||
--color-checks-ansi-yellow: #d4a72c;
|
||||
--color-checks-ansi-yellow-bright: #eac54f;
|
||||
--color-checks-ansi-blue: #54aeff;
|
||||
--color-checks-ansi-blue-bright: #80ccff;
|
||||
--color-checks-ansi-magenta: #c297ff;
|
||||
--color-checks-ansi-magenta-bright: #d8b9ff;
|
||||
--color-checks-ansi-cyan: #76e3ea;
|
||||
--color-checks-ansi-cyan-bright: #b3f0ff;
|
||||
--color-project-header-bg: #24292f;
|
||||
--color-project-sidebar-bg: #ffffff;
|
||||
--color-project-gradient-in: #ffffff;
|
||||
--color-project-gradient-out: rgba(255,255,255,0);
|
||||
--color-mktg-success: rgba(36,146,67,1);
|
||||
--color-mktg-info: rgba(19,119,234,1);
|
||||
--color-mktg-bg-shade-gradient-top: rgba(27,31,36,0.065);
|
||||
--color-mktg-bg-shade-gradient-bottom: rgba(27,31,36,0);
|
||||
--color-mktg-btn-bg-top: hsla(228,82%,66%,1);
|
||||
--color-mktg-btn-bg-bottom: #4969ed;
|
||||
--color-mktg-btn-bg-overlay-top: hsla(228,74%,59%,1);
|
||||
--color-mktg-btn-bg-overlay-bottom: #3355e0;
|
||||
--color-mktg-btn-text: #ffffff;
|
||||
--color-mktg-btn-primary-bg-top: hsla(137,56%,46%,1);
|
||||
--color-mktg-btn-primary-bg-bottom: #2ea44f;
|
||||
--color-mktg-btn-primary-bg-overlay-top: hsla(134,60%,38%,1);
|
||||
--color-mktg-btn-primary-bg-overlay-bottom: #22863a;
|
||||
--color-mktg-btn-primary-text: #ffffff;
|
||||
--color-mktg-btn-enterprise-bg-top: hsla(249,100%,72%,1);
|
||||
--color-mktg-btn-enterprise-bg-bottom: #6f57ff;
|
||||
--color-mktg-btn-enterprise-bg-overlay-top: hsla(248,65%,63%,1);
|
||||
--color-mktg-btn-enterprise-bg-overlay-bottom: #614eda;
|
||||
--color-mktg-btn-enterprise-text: #ffffff;
|
||||
--color-mktg-btn-outline-text: #4969ed;
|
||||
--color-mktg-btn-outline-border: rgba(73,105,237,0.3);
|
||||
--color-mktg-btn-outline-hover-text: #3355e0;
|
||||
--color-mktg-btn-outline-hover-border: rgba(51,85,224,0.5);
|
||||
--color-mktg-btn-outline-focus-border: #4969ed;
|
||||
--color-mktg-btn-outline-focus-border-inset: rgba(73,105,237,0.5);
|
||||
--color-mktg-btn-dark-text: #ffffff;
|
||||
--color-mktg-btn-dark-border: rgba(255,255,255,0.3);
|
||||
--color-mktg-btn-dark-hover-text: #ffffff;
|
||||
--color-mktg-btn-dark-hover-border: rgba(255,255,255,0.5);
|
||||
--color-mktg-btn-dark-focus-border: #ffffff;
|
||||
--color-mktg-btn-dark-focus-border-inset: rgba(255,255,255,0.5);
|
||||
--color-avatar-bg: #ffffff;
|
||||
--color-avatar-border: rgba(27,31,36,0.15);
|
||||
--color-avatar-stack-fade: #afb8c1;
|
||||
--color-avatar-stack-fade-more: #d0d7de;
|
||||
--color-avatar-child-shadow: -2px -2px 0 rgba(255,255,255,0.8);
|
||||
--color-topic-tag-border: rgba(0,0,0,0);
|
||||
--color-select-menu-backdrop-border: rgba(0,0,0,0);
|
||||
--color-select-menu-tap-highlight: rgba(175,184,193,0.5);
|
||||
--color-select-menu-tap-focus-bg: #b6e3ff;
|
||||
--color-overlay-shadow: 0 1px 3px rgba(27,31,36,0.12), 0 8px 24px rgba(66,74,83,0.12);
|
||||
--color-header-text: rgba(255,255,255,0.7);
|
||||
--color-header-bg: #24292f;
|
||||
--color-header-logo: #ffffff;
|
||||
--color-header-search-bg: #24292f;
|
||||
--color-header-search-border: #57606a;
|
||||
--color-sidenav-selected-bg: #ffffff;
|
||||
--color-menu-bg-active: rgba(0,0,0,0);
|
||||
--color-control-transparent-bg-hover: #818b981a;
|
||||
--color-input-disabled-bg: rgba(175,184,193,0.2);
|
||||
--color-timeline-badge-bg: #eaeef2;
|
||||
--color-ansi-black: #24292f;
|
||||
--color-ansi-black-bright: #57606a;
|
||||
--color-ansi-white: #6e7781;
|
||||
--color-ansi-white-bright: #8c959f;
|
||||
--color-ansi-gray: #6e7781;
|
||||
--color-ansi-red: #cf222e;
|
||||
--color-ansi-red-bright: #a40e26;
|
||||
--color-ansi-green: #116329;
|
||||
--color-ansi-green-bright: #1a7f37;
|
||||
--color-ansi-yellow: #4d2d00;
|
||||
--color-ansi-yellow-bright: #633c01;
|
||||
--color-ansi-blue: #0969da;
|
||||
--color-ansi-blue-bright: #218bff;
|
||||
--color-ansi-magenta: #8250df;
|
||||
--color-ansi-magenta-bright: #a475f9;
|
||||
--color-ansi-cyan: #1b7c83;
|
||||
--color-ansi-cyan-bright: #3192aa;
|
||||
--color-btn-text: #24292f;
|
||||
--color-btn-bg: #f6f8fa;
|
||||
--color-btn-border: rgba(27,31,36,0.15);
|
||||
--color-btn-shadow: 0 1px 0 rgba(27,31,36,0.04);
|
||||
--color-btn-inset-shadow: inset 0 1px 0 rgba(255,255,255,0.25);
|
||||
--color-btn-hover-bg: #f3f4f6;
|
||||
--color-btn-hover-border: rgba(27,31,36,0.15);
|
||||
--color-btn-active-bg: hsla(220,14%,93%,1);
|
||||
--color-btn-active-border: rgba(27,31,36,0.15);
|
||||
--color-btn-selected-bg: hsla(220,14%,94%,1);
|
||||
--color-btn-focus-bg: #f6f8fa;
|
||||
--color-btn-focus-border: rgba(27,31,36,0.15);
|
||||
--color-btn-focus-shadow: 0 0 0 3px rgba(9,105,218,0.3);
|
||||
--color-btn-shadow-active: inset 0 0.15em 0.3em rgba(27,31,36,0.15);
|
||||
--color-btn-shadow-input-focus: 0 0 0 0.2em rgba(9,105,218,0.3);
|
||||
--color-btn-counter-bg: rgba(27,31,36,0.08);
|
||||
--color-btn-primary-text: #ffffff;
|
||||
--color-btn-primary-bg: #2da44e;
|
||||
--color-btn-primary-border: rgba(27,31,36,0.15);
|
||||
--color-btn-primary-shadow: 0 1px 0 rgba(27,31,36,0.1);
|
||||
--color-btn-primary-inset-shadow: inset 0 1px 0 rgba(255,255,255,0.03);
|
||||
--color-btn-primary-hover-bg: #2c974b;
|
||||
--color-btn-primary-hover-border: rgba(27,31,36,0.15);
|
||||
--color-btn-primary-selected-bg: hsla(137,55%,36%,1);
|
||||
--color-btn-primary-selected-shadow: inset 0 1px 0 rgba(0,45,17,0.2);
|
||||
--color-btn-primary-disabled-text: rgba(255,255,255,0.8);
|
||||
--color-btn-primary-disabled-bg: #94d3a2;
|
||||
--color-btn-primary-disabled-border: rgba(27,31,36,0.15);
|
||||
--color-btn-primary-focus-bg: #2da44e;
|
||||
--color-btn-primary-focus-border: rgba(27,31,36,0.15);
|
||||
--color-btn-primary-focus-shadow: 0 0 0 3px rgba(45,164,78,0.4);
|
||||
--color-btn-primary-icon: rgba(255,255,255,0.8);
|
||||
--color-btn-primary-counter-bg: rgba(255,255,255,0.2);
|
||||
--color-btn-outline-text: #0969da;
|
||||
--color-btn-outline-hover-text: #ffffff;
|
||||
--color-btn-outline-hover-bg: #0969da;
|
||||
--color-btn-outline-hover-border: rgba(27,31,36,0.15);
|
||||
--color-btn-outline-hover-shadow: 0 1px 0 rgba(27,31,36,0.1);
|
||||
--color-btn-outline-hover-inset-shadow: inset 0 1px 0 rgba(255,255,255,0.03);
|
||||
--color-btn-outline-hover-counter-bg: rgba(255,255,255,0.2);
|
||||
--color-btn-outline-selected-text: #ffffff;
|
||||
--color-btn-outline-selected-bg: hsla(212,92%,42%,1);
|
||||
--color-btn-outline-selected-border: rgba(27,31,36,0.15);
|
||||
--color-btn-outline-selected-shadow: inset 0 1px 0 rgba(0,33,85,0.2);
|
||||
--color-btn-outline-disabled-text: rgba(9,105,218,0.5);
|
||||
--color-btn-outline-disabled-bg: #f6f8fa;
|
||||
--color-btn-outline-disabled-counter-bg: rgba(9,105,218,0.05);
|
||||
--color-btn-outline-focus-border: rgba(27,31,36,0.15);
|
||||
--color-btn-outline-focus-shadow: 0 0 0 3px rgba(5,80,174,0.4);
|
||||
--color-btn-outline-counter-bg: rgba(9,105,218,0.1);
|
||||
--color-btn-danger-text: #cf222e;
|
||||
--color-btn-danger-hover-text: #ffffff;
|
||||
--color-btn-danger-hover-bg: #a40e26;
|
||||
--color-btn-danger-hover-border: rgba(27,31,36,0.15);
|
||||
--color-btn-danger-hover-shadow: 0 1px 0 rgba(27,31,36,0.1);
|
||||
--color-btn-danger-hover-inset-shadow: inset 0 1px 0 rgba(255,255,255,0.03);
|
||||
--color-btn-danger-hover-counter-bg: rgba(255,255,255,0.2);
|
||||
--color-btn-danger-selected-text: #ffffff;
|
||||
--color-btn-danger-selected-bg: hsla(356,72%,44%,1);
|
||||
--color-btn-danger-selected-border: rgba(27,31,36,0.15);
|
||||
--color-btn-danger-selected-shadow: inset 0 1px 0 rgba(76,0,20,0.2);
|
||||
--color-btn-danger-disabled-text: rgba(207,34,46,0.5);
|
||||
--color-btn-danger-disabled-bg: #f6f8fa;
|
||||
--color-btn-danger-disabled-counter-bg: rgba(207,34,46,0.05);
|
||||
--color-btn-danger-focus-border: rgba(27,31,36,0.15);
|
||||
--color-btn-danger-focus-shadow: 0 0 0 3px rgba(164,14,38,0.4);
|
||||
--color-btn-danger-counter-bg: rgba(207,34,46,0.1);
|
||||
--color-btn-danger-icon: #cf222e;
|
||||
--color-btn-danger-hover-icon: #ffffff;
|
||||
--color-underlinenav-icon: #6e7781;
|
||||
--color-underlinenav-border-hover: rgba(175,184,193,0.2);
|
||||
--color-fg-default: #24292f;
|
||||
--color-fg-muted: #57606a;
|
||||
--color-fg-subtle: #6e7781;
|
||||
--color-fg-on-emphasis: #ffffff;
|
||||
--color-canvas-default: #ffffff;
|
||||
--color-canvas-overlay: #ffffff;
|
||||
--color-canvas-inset: #f6f8fa;
|
||||
--color-canvas-subtle: #f6f8fa;
|
||||
--color-border-default: #d0d7de;
|
||||
--color-border-muted: hsla(210,18%,87%,1);
|
||||
--color-border-subtle: rgba(27,31,36,0.15);
|
||||
--color-shadow-small: 0 1px 0 rgba(27,31,36,0.04);
|
||||
--color-shadow-medium: 0 3px 6px rgba(140,149,159,0.15);
|
||||
--color-shadow-large: 0 8px 24px rgba(140,149,159,0.2);
|
||||
--color-shadow-extra-large: 0 12px 28px rgba(140,149,159,0.3);
|
||||
--color-neutral-emphasis-plus: #24292f;
|
||||
--color-neutral-emphasis: #6e7781;
|
||||
--color-neutral-muted: rgba(175,184,193,0.2);
|
||||
--color-neutral-subtle: rgba(234,238,242,0.5);
|
||||
--color-accent-fg: #0969da;
|
||||
--color-accent-emphasis: #0969da;
|
||||
--color-accent-muted: rgba(84,174,255,0.4);
|
||||
--color-accent-subtle: #ddf4ff;
|
||||
--color-success-fg: #1a7f37;
|
||||
--color-success-emphasis: #2da44e;
|
||||
--color-success-muted: rgba(74,194,107,0.4);
|
||||
--color-success-subtle: #dafbe1;
|
||||
--color-attention-fg: #9a6700;
|
||||
--color-attention-emphasis: #bf8700;
|
||||
--color-attention-muted: rgba(212,167,44,0.4);
|
||||
--color-attention-subtle: #fff8c5;
|
||||
--color-severe-fg: #bc4c00;
|
||||
--color-severe-emphasis: #bc4c00;
|
||||
--color-severe-muted: rgba(251,143,68,0.4);
|
||||
--color-severe-subtle: #fff1e5;
|
||||
--color-danger-fg: #cf222e;
|
||||
--color-danger-emphasis: #cf222e;
|
||||
--color-danger-muted: rgba(255,129,130,0.4);
|
||||
--color-danger-subtle: #FFEBE9;
|
||||
--color-done-fg: #8250df;
|
||||
--color-done-emphasis: #8250df;
|
||||
--color-done-muted: rgba(194,151,255,0.4);
|
||||
--color-done-subtle: #fbefff;
|
||||
--color-sponsors-fg: #bf3989;
|
||||
--color-sponsors-emphasis: #bf3989;
|
||||
--color-sponsors-muted: rgba(255,128,200,0.4);
|
||||
--color-sponsors-subtle: #ffeff7;
|
||||
--color-primer-canvas-backdrop: rgba(27,31,36,0.5);
|
||||
--color-primer-canvas-sticky: rgba(255,255,255,0.95);
|
||||
--color-primer-border-active: #FD8C73;
|
||||
--color-primer-border-contrast: rgba(27,31,36,0.1);
|
||||
--color-primer-shadow-highlight: inset 0 1px 0 rgba(255,255,255,0.25);
|
||||
--color-primer-shadow-inset: inset 0 1px 0 rgba(208,215,222,0.2);
|
||||
--color-primer-shadow-focus: 0 0 0 3px rgba(9,105,218,0.3);
|
||||
--color-scale-black: #1b1f24;
|
||||
--color-scale-white: #ffffff;
|
||||
--color-scale-gray-0: #f6f8fa;
|
||||
--color-scale-gray-1: #eaeef2;
|
||||
--color-scale-gray-2: #d0d7de;
|
||||
--color-scale-gray-3: #afb8c1;
|
||||
--color-scale-gray-4: #8c959f;
|
||||
--color-scale-gray-5: #6e7781;
|
||||
--color-scale-gray-6: #57606a;
|
||||
--color-scale-gray-7: #424a53;
|
||||
--color-scale-gray-8: #32383f;
|
||||
--color-scale-gray-9: #24292f;
|
||||
--color-scale-blue-0: #ddf4ff;
|
||||
--color-scale-blue-1: #b6e3ff;
|
||||
--color-scale-blue-2: #80ccff;
|
||||
--color-scale-blue-3: #54aeff;
|
||||
--color-scale-blue-4: #218bff;
|
||||
--color-scale-blue-5: #0969da;
|
||||
--color-scale-blue-6: #0550ae;
|
||||
--color-scale-blue-7: #033d8b;
|
||||
--color-scale-blue-8: #0a3069;
|
||||
--color-scale-blue-9: #002155;
|
||||
--color-scale-green-0: #dafbe1;
|
||||
--color-scale-green-1: #aceebb;
|
||||
--color-scale-green-2: #6fdd8b;
|
||||
--color-scale-green-3: #4ac26b;
|
||||
--color-scale-green-4: #2da44e;
|
||||
--color-scale-green-5: #1a7f37;
|
||||
--color-scale-green-6: #116329;
|
||||
--color-scale-green-7: #044f1e;
|
||||
--color-scale-green-8: #003d16;
|
||||
--color-scale-green-9: #002d11;
|
||||
--color-scale-yellow-0: #fff8c5;
|
||||
--color-scale-yellow-1: #fae17d;
|
||||
--color-scale-yellow-2: #eac54f;
|
||||
--color-scale-yellow-3: #d4a72c;
|
||||
--color-scale-yellow-4: #bf8700;
|
||||
--color-scale-yellow-5: #9a6700;
|
||||
--color-scale-yellow-6: #7d4e00;
|
||||
--color-scale-yellow-7: #633c01;
|
||||
--color-scale-yellow-8: #4d2d00;
|
||||
--color-scale-yellow-9: #3b2300;
|
||||
--color-scale-orange-0: #fff1e5;
|
||||
--color-scale-orange-1: #ffd8b5;
|
||||
--color-scale-orange-2: #ffb77c;
|
||||
--color-scale-orange-3: #fb8f44;
|
||||
--color-scale-orange-4: #e16f24;
|
||||
--color-scale-orange-5: #bc4c00;
|
||||
--color-scale-orange-6: #953800;
|
||||
--color-scale-orange-7: #762c00;
|
||||
--color-scale-orange-8: #5c2200;
|
||||
--color-scale-orange-9: #471700;
|
||||
--color-scale-red-0: #FFEBE9;
|
||||
--color-scale-red-1: #ffcecb;
|
||||
--color-scale-red-2: #ffaba8;
|
||||
--color-scale-red-3: #ff8182;
|
||||
--color-scale-red-4: #fa4549;
|
||||
--color-scale-red-5: #cf222e;
|
||||
--color-scale-red-6: #a40e26;
|
||||
--color-scale-red-7: #82071e;
|
||||
--color-scale-red-8: #660018;
|
||||
--color-scale-red-9: #4c0014;
|
||||
--color-scale-purple-0: #fbefff;
|
||||
--color-scale-purple-1: #ecd8ff;
|
||||
--color-scale-purple-2: #d8b9ff;
|
||||
--color-scale-purple-3: #c297ff;
|
||||
--color-scale-purple-4: #a475f9;
|
||||
--color-scale-purple-5: #8250df;
|
||||
--color-scale-purple-6: #6639ba;
|
||||
--color-scale-purple-7: #512a97;
|
||||
--color-scale-purple-8: #3e1f79;
|
||||
--color-scale-purple-9: #2e1461;
|
||||
--color-scale-pink-0: #ffeff7;
|
||||
--color-scale-pink-1: #ffd3eb;
|
||||
--color-scale-pink-2: #ffadda;
|
||||
--color-scale-pink-3: #ff80c8;
|
||||
--color-scale-pink-4: #e85aad;
|
||||
--color-scale-pink-5: #bf3989;
|
||||
--color-scale-pink-6: #99286e;
|
||||
--color-scale-pink-7: #772057;
|
||||
--color-scale-pink-8: #611347;
|
||||
--color-scale-pink-9: #4d0336;
|
||||
--color-scale-coral-0: #FFF0EB;
|
||||
--color-scale-coral-1: #FFD6CC;
|
||||
--color-scale-coral-2: #FFB4A1;
|
||||
--color-scale-coral-3: #FD8C73;
|
||||
--color-scale-coral-4: #EC6547;
|
||||
--color-scale-coral-5: #C4432B;
|
||||
--color-scale-coral-6: #9E2F1C;
|
||||
--color-scale-coral-7: #801F0F;
|
||||
--color-scale-coral-8: #691105;
|
||||
--color-scale-coral-9: #510901
|
||||
}
|
||||
|
||||
@media(prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-canvas-default-transparent: rgba(13,17,23,0);
|
||||
--color-marketing-icon-primary: #79c0ff;
|
||||
--color-marketing-icon-secondary: #1f6feb;
|
||||
--color-diff-blob-addition-num-text: #c9d1d9;
|
||||
--color-diff-blob-addition-fg: #c9d1d9;
|
||||
--color-diff-blob-addition-num-bg: rgba(63,185,80,0.3);
|
||||
--color-diff-blob-addition-line-bg: rgba(46,160,67,0.15);
|
||||
--color-diff-blob-addition-word-bg: rgba(46,160,67,0.4);
|
||||
--color-diff-blob-deletion-num-text: #c9d1d9;
|
||||
--color-diff-blob-deletion-fg: #c9d1d9;
|
||||
--color-diff-blob-deletion-num-bg: rgba(248,81,73,0.3);
|
||||
--color-diff-blob-deletion-line-bg: rgba(248,81,73,0.15);
|
||||
--color-diff-blob-deletion-word-bg: rgba(248,81,73,0.4);
|
||||
--color-diff-blob-hunk-num-bg: rgba(56,139,253,0.4);
|
||||
--color-diff-blob-expander-icon: #8b949e;
|
||||
--color-diff-blob-selected-line-highlight-mix-blend-mode: screen;
|
||||
--color-diffstat-deletion-border: rgba(240,246,252,0.1);
|
||||
--color-diffstat-addition-border: rgba(240,246,252,0.1);
|
||||
--color-diffstat-addition-bg: #3fb950;
|
||||
--color-search-keyword-hl: rgba(210,153,34,0.4);
|
||||
--color-prettylights-syntax-comment: #8b949e;
|
||||
--color-prettylights-syntax-constant: #79c0ff;
|
||||
--color-prettylights-syntax-entity: #d2a8ff;
|
||||
--color-prettylights-syntax-storage-modifier-import: #c9d1d9;
|
||||
--color-prettylights-syntax-entity-tag: #7ee787;
|
||||
--color-prettylights-syntax-keyword: #ff7b72;
|
||||
--color-prettylights-syntax-string: #a5d6ff;
|
||||
--color-prettylights-syntax-variable: #ffa657;
|
||||
--color-prettylights-syntax-brackethighlighter-unmatched: #f85149;
|
||||
--color-prettylights-syntax-invalid-illegal-text: #f0f6fc;
|
||||
--color-prettylights-syntax-invalid-illegal-bg: #8e1519;
|
||||
--color-prettylights-syntax-carriage-return-text: #f0f6fc;
|
||||
--color-prettylights-syntax-carriage-return-bg: #b62324;
|
||||
--color-prettylights-syntax-string-regexp: #7ee787;
|
||||
--color-prettylights-syntax-markup-list: #f2cc60;
|
||||
--color-prettylights-syntax-markup-heading: #1f6feb;
|
||||
--color-prettylights-syntax-markup-italic: #c9d1d9;
|
||||
--color-prettylights-syntax-markup-bold: #c9d1d9;
|
||||
--color-prettylights-syntax-markup-deleted-text: #ffdcd7;
|
||||
--color-prettylights-syntax-markup-deleted-bg: #67060c;
|
||||
--color-prettylights-syntax-markup-inserted-text: #aff5b4;
|
||||
--color-prettylights-syntax-markup-inserted-bg: #033a16;
|
||||
--color-prettylights-syntax-markup-changed-text: #ffdfb6;
|
||||
--color-prettylights-syntax-markup-changed-bg: #5a1e02;
|
||||
--color-prettylights-syntax-markup-ignored-text: #c9d1d9;
|
||||
--color-prettylights-syntax-markup-ignored-bg: #1158c7;
|
||||
--color-prettylights-syntax-meta-diff-range: #d2a8ff;
|
||||
--color-prettylights-syntax-brackethighlighter-angle: #8b949e;
|
||||
--color-prettylights-syntax-sublimelinter-gutter-mark: #484f58;
|
||||
--color-prettylights-syntax-constant-other-reference-link: #a5d6ff;
|
||||
--color-codemirror-text: #c9d1d9;
|
||||
--color-codemirror-bg: #0d1117;
|
||||
--color-codemirror-gutters-bg: #0d1117;
|
||||
--color-codemirror-guttermarker-text: #0d1117;
|
||||
--color-codemirror-guttermarker-subtle-text: #484f58;
|
||||
--color-codemirror-linenumber-text: #8b949e;
|
||||
--color-codemirror-cursor: #c9d1d9;
|
||||
--color-codemirror-selection-bg: rgba(56,139,253,0.4);
|
||||
--color-codemirror-activeline-bg: rgba(110,118,129,0.1);
|
||||
--color-codemirror-matchingbracket-text: #c9d1d9;
|
||||
--color-codemirror-lines-bg: #0d1117;
|
||||
--color-codemirror-syntax-comment: #8b949e;
|
||||
--color-codemirror-syntax-constant: #79c0ff;
|
||||
--color-codemirror-syntax-entity: #d2a8ff;
|
||||
--color-codemirror-syntax-keyword: #ff7b72;
|
||||
--color-codemirror-syntax-storage: #ff7b72;
|
||||
--color-codemirror-syntax-string: #a5d6ff;
|
||||
--color-codemirror-syntax-support: #79c0ff;
|
||||
--color-codemirror-syntax-variable: #ffa657;
|
||||
--color-checks-bg: #010409;
|
||||
--color-checks-run-border-width: 1px;
|
||||
--color-checks-container-border-width: 1px;
|
||||
--color-checks-text-primary: #c9d1d9;
|
||||
--color-checks-text-secondary: #8b949e;
|
||||
--color-checks-text-link: #58a6ff;
|
||||
--color-checks-btn-icon: #8b949e;
|
||||
--color-checks-btn-hover-icon: #c9d1d9;
|
||||
--color-checks-btn-hover-bg: rgba(110,118,129,0.1);
|
||||
--color-checks-input-text: #8b949e;
|
||||
--color-checks-input-placeholder-text: #484f58;
|
||||
--color-checks-input-focus-text: #c9d1d9;
|
||||
--color-checks-input-bg: #161b22;
|
||||
--color-checks-input-shadow: none;
|
||||
--color-checks-donut-error: #f85149;
|
||||
--color-checks-donut-pending: #d29922;
|
||||
--color-checks-donut-success: #2ea043;
|
||||
--color-checks-donut-neutral: #8b949e;
|
||||
--color-checks-dropdown-text: #c9d1d9;
|
||||
--color-checks-dropdown-bg: #161b22;
|
||||
--color-checks-dropdown-border: #30363d;
|
||||
--color-checks-dropdown-shadow: rgba(1,4,9,0.3);
|
||||
--color-checks-dropdown-hover-text: #c9d1d9;
|
||||
--color-checks-dropdown-hover-bg: rgba(110,118,129,0.1);
|
||||
--color-checks-dropdown-btn-hover-text: #c9d1d9;
|
||||
--color-checks-dropdown-btn-hover-bg: rgba(110,118,129,0.1);
|
||||
--color-checks-scrollbar-thumb-bg: rgba(110,118,129,0.4);
|
||||
--color-checks-header-label-text: #8b949e;
|
||||
--color-checks-header-label-open-text: #c9d1d9;
|
||||
--color-checks-header-border: #21262d;
|
||||
--color-checks-header-icon: #8b949e;
|
||||
--color-checks-line-text: #8b949e;
|
||||
--color-checks-line-num-text: #484f58;
|
||||
--color-checks-line-timestamp-text: #484f58;
|
||||
--color-checks-line-hover-bg: rgba(110,118,129,0.1);
|
||||
--color-checks-line-selected-bg: rgba(56,139,253,0.15);
|
||||
--color-checks-line-selected-num-text: #58a6ff;
|
||||
--color-checks-line-dt-fm-text: #f0f6fc;
|
||||
--color-checks-line-dt-fm-bg: #9e6a03;
|
||||
--color-checks-gate-bg: rgba(187,128,9,0.15);
|
||||
--color-checks-gate-text: #8b949e;
|
||||
--color-checks-gate-waiting-text: #d29922;
|
||||
--color-checks-step-header-open-bg: #161b22;
|
||||
--color-checks-step-error-text: #f85149;
|
||||
--color-checks-step-warning-text: #d29922;
|
||||
--color-checks-logline-text: #8b949e;
|
||||
--color-checks-logline-num-text: #484f58;
|
||||
--color-checks-logline-debug-text: #a371f7;
|
||||
--color-checks-logline-error-text: #8b949e;
|
||||
--color-checks-logline-error-num-text: #484f58;
|
||||
--color-checks-logline-error-bg: rgba(248,81,73,0.15);
|
||||
--color-checks-logline-warning-text: #8b949e;
|
||||
--color-checks-logline-warning-num-text: #d29922;
|
||||
--color-checks-logline-warning-bg: rgba(187,128,9,0.15);
|
||||
--color-checks-logline-command-text: #58a6ff;
|
||||
--color-checks-logline-section-text: #3fb950;
|
||||
--color-checks-ansi-black: #0d1117;
|
||||
--color-checks-ansi-black-bright: #161b22;
|
||||
--color-checks-ansi-white: #b1bac4;
|
||||
--color-checks-ansi-white-bright: #b1bac4;
|
||||
--color-checks-ansi-gray: #6e7681;
|
||||
--color-checks-ansi-red: #ff7b72;
|
||||
--color-checks-ansi-red-bright: #ffa198;
|
||||
--color-checks-ansi-green: #3fb950;
|
||||
--color-checks-ansi-green-bright: #56d364;
|
||||
--color-checks-ansi-yellow: #d29922;
|
||||
--color-checks-ansi-yellow-bright: #e3b341;
|
||||
--color-checks-ansi-blue: #58a6ff;
|
||||
--color-checks-ansi-blue-bright: #79c0ff;
|
||||
--color-checks-ansi-magenta: #bc8cff;
|
||||
--color-checks-ansi-magenta-bright: #d2a8ff;
|
||||
--color-checks-ansi-cyan: #76e3ea;
|
||||
--color-checks-ansi-cyan-bright: #b3f0ff;
|
||||
--color-project-header-bg: #0d1117;
|
||||
--color-project-sidebar-bg: #161b22;
|
||||
--color-project-gradient-in: #161b22;
|
||||
--color-project-gradient-out: rgba(22,27,34,0);
|
||||
--color-mktg-success: rgba(41,147,61,1);
|
||||
--color-mktg-info: rgba(42,123,243,1);
|
||||
--color-mktg-bg-shade-gradient-top: rgba(1,4,9,0.065);
|
||||
--color-mktg-bg-shade-gradient-bottom: rgba(1,4,9,0);
|
||||
--color-mktg-btn-bg-top: hsla(228,82%,66%,1);
|
||||
--color-mktg-btn-bg-bottom: #4969ed;
|
||||
--color-mktg-btn-bg-overlay-top: hsla(228,74%,59%,1);
|
||||
--color-mktg-btn-bg-overlay-bottom: #3355e0;
|
||||
--color-mktg-btn-text: #f0f6fc;
|
||||
--color-mktg-btn-primary-bg-top: hsla(137,56%,46%,1);
|
||||
--color-mktg-btn-primary-bg-bottom: #2ea44f;
|
||||
--color-mktg-btn-primary-bg-overlay-top: hsla(134,60%,38%,1);
|
||||
--color-mktg-btn-primary-bg-overlay-bottom: #22863a;
|
||||
--color-mktg-btn-primary-text: #f0f6fc;
|
||||
--color-mktg-btn-enterprise-bg-top: hsla(249,100%,72%,1);
|
||||
--color-mktg-btn-enterprise-bg-bottom: #6f57ff;
|
||||
--color-mktg-btn-enterprise-bg-overlay-top: hsla(248,65%,63%,1);
|
||||
--color-mktg-btn-enterprise-bg-overlay-bottom: #614eda;
|
||||
--color-mktg-btn-enterprise-text: #f0f6fc;
|
||||
--color-mktg-btn-outline-text: #f0f6fc;
|
||||
--color-mktg-btn-outline-border: rgba(240,246,252,0.3);
|
||||
--color-mktg-btn-outline-hover-text: #f0f6fc;
|
||||
--color-mktg-btn-outline-hover-border: rgba(240,246,252,0.5);
|
||||
--color-mktg-btn-outline-focus-border: #f0f6fc;
|
||||
--color-mktg-btn-outline-focus-border-inset: rgba(240,246,252,0.5);
|
||||
--color-mktg-btn-dark-text: #f0f6fc;
|
||||
--color-mktg-btn-dark-border: rgba(240,246,252,0.3);
|
||||
--color-mktg-btn-dark-hover-text: #f0f6fc;
|
||||
--color-mktg-btn-dark-hover-border: rgba(240,246,252,0.5);
|
||||
--color-mktg-btn-dark-focus-border: #f0f6fc;
|
||||
--color-mktg-btn-dark-focus-border-inset: rgba(240,246,252,0.5);
|
||||
--color-avatar-bg: rgba(240,246,252,0.1);
|
||||
--color-avatar-border: rgba(240,246,252,0.1);
|
||||
--color-avatar-stack-fade: #30363d;
|
||||
--color-avatar-stack-fade-more: #21262d;
|
||||
--color-avatar-child-shadow: -2px -2px 0 #0d1117;
|
||||
--color-topic-tag-border: rgba(0,0,0,0);
|
||||
--color-select-menu-backdrop-border: #484f58;
|
||||
--color-select-menu-tap-highlight: rgba(48,54,61,0.5);
|
||||
--color-select-menu-tap-focus-bg: #0c2d6b;
|
||||
--color-overlay-shadow: 0 0 0 1px #30363d, 0 16px 32px rgba(1,4,9,0.85);
|
||||
--color-header-text: rgba(240,246,252,0.7);
|
||||
--color-header-bg: #161b22;
|
||||
--color-header-logo: #f0f6fc;
|
||||
--color-header-search-bg: #0d1117;
|
||||
--color-header-search-border: #30363d;
|
||||
--color-sidenav-selected-bg: #21262d;
|
||||
--color-menu-bg-active: #161b22;
|
||||
--color-control-transparent-bg-hover: #656c7633;
|
||||
--color-input-disabled-bg: rgba(110,118,129,0);
|
||||
--color-timeline-badge-bg: #21262d;
|
||||
--color-ansi-black: #484f58;
|
||||
--color-ansi-black-bright: #6e7681;
|
||||
--color-ansi-white: #b1bac4;
|
||||
--color-ansi-white-bright: #f0f6fc;
|
||||
--color-ansi-gray: #6e7681;
|
||||
--color-ansi-red: #ff7b72;
|
||||
--color-ansi-red-bright: #ffa198;
|
||||
--color-ansi-green: #3fb950;
|
||||
--color-ansi-green-bright: #56d364;
|
||||
--color-ansi-yellow: #d29922;
|
||||
--color-ansi-yellow-bright: #e3b341;
|
||||
--color-ansi-blue: #58a6ff;
|
||||
--color-ansi-blue-bright: #79c0ff;
|
||||
--color-ansi-magenta: #bc8cff;
|
||||
--color-ansi-magenta-bright: #d2a8ff;
|
||||
--color-ansi-cyan: #39c5cf;
|
||||
--color-ansi-cyan-bright: #56d4dd;
|
||||
--color-btn-text: #c9d1d9;
|
||||
--color-btn-bg: #21262d;
|
||||
--color-btn-border: rgba(240,246,252,0.1);
|
||||
--color-btn-shadow: 0 0 transparent;
|
||||
--color-btn-inset-shadow: 0 0 transparent;
|
||||
--color-btn-hover-bg: #30363d;
|
||||
--color-btn-hover-border: #8b949e;
|
||||
--color-btn-active-bg: hsla(212,12%,18%,1);
|
||||
--color-btn-active-border: #6e7681;
|
||||
--color-btn-selected-bg: #161b22;
|
||||
--color-btn-focus-bg: #21262d;
|
||||
--color-btn-focus-border: #8b949e;
|
||||
--color-btn-focus-shadow: 0 0 0 3px rgba(139,148,158,0.3);
|
||||
--color-btn-shadow-active: inset 0 0.15em 0.3em rgba(1,4,9,0.15);
|
||||
--color-btn-shadow-input-focus: 0 0 0 0.2em rgba(31,111,235,0.3);
|
||||
--color-btn-counter-bg: #30363d;
|
||||
--color-btn-primary-text: #ffffff;
|
||||
--color-btn-primary-bg: #238636;
|
||||
--color-btn-primary-border: rgba(240,246,252,0.1);
|
||||
--color-btn-primary-shadow: 0 0 transparent;
|
||||
--color-btn-primary-inset-shadow: 0 0 transparent;
|
||||
--color-btn-primary-hover-bg: #2ea043;
|
||||
--color-btn-primary-hover-border: rgba(240,246,252,0.1);
|
||||
--color-btn-primary-selected-bg: #238636;
|
||||
--color-btn-primary-selected-shadow: 0 0 transparent;
|
||||
--color-btn-primary-disabled-text: rgba(240,246,252,0.5);
|
||||
--color-btn-primary-disabled-bg: rgba(35,134,54,0.6);
|
||||
--color-btn-primary-disabled-border: rgba(240,246,252,0.1);
|
||||
--color-btn-primary-focus-bg: #238636;
|
||||
--color-btn-primary-focus-border: rgba(240,246,252,0.1);
|
||||
--color-btn-primary-focus-shadow: 0 0 0 3px rgba(46,164,79,0.4);
|
||||
--color-btn-primary-icon: #f0f6fc;
|
||||
--color-btn-primary-counter-bg: rgba(240,246,252,0.2);
|
||||
--color-btn-outline-text: #58a6ff;
|
||||
--color-btn-outline-hover-text: #58a6ff;
|
||||
--color-btn-outline-hover-bg: #30363d;
|
||||
--color-btn-outline-hover-border: rgba(240,246,252,0.1);
|
||||
--color-btn-outline-hover-shadow: 0 1px 0 rgba(1,4,9,0.1);
|
||||
--color-btn-outline-hover-inset-shadow: inset 0 1px 0 rgba(240,246,252,0.03);
|
||||
--color-btn-outline-hover-counter-bg: rgba(240,246,252,0.2);
|
||||
--color-btn-outline-selected-text: #f0f6fc;
|
||||
--color-btn-outline-selected-bg: #0d419d;
|
||||
--color-btn-outline-selected-border: rgba(240,246,252,0.1);
|
||||
--color-btn-outline-selected-shadow: 0 0 transparent;
|
||||
--color-btn-outline-disabled-text: rgba(88,166,255,0.5);
|
||||
--color-btn-outline-disabled-bg: #0d1117;
|
||||
--color-btn-outline-disabled-counter-bg: rgba(31,111,235,0.05);
|
||||
--color-btn-outline-focus-border: rgba(240,246,252,0.1);
|
||||
--color-btn-outline-focus-shadow: 0 0 0 3px rgba(17,88,199,0.4);
|
||||
--color-btn-outline-counter-bg: rgba(31,111,235,0.1);
|
||||
--color-btn-danger-text: #f85149;
|
||||
--color-btn-danger-hover-text: #f0f6fc;
|
||||
--color-btn-danger-hover-bg: #da3633;
|
||||
--color-btn-danger-hover-border: #f85149;
|
||||
--color-btn-danger-hover-shadow: 0 0 transparent;
|
||||
--color-btn-danger-hover-inset-shadow: 0 0 transparent;
|
||||
--color-btn-danger-hover-icon: #f0f6fc;
|
||||
--color-btn-danger-hover-counter-bg: rgba(255,255,255,0.2);
|
||||
--color-btn-danger-selected-text: #ffffff;
|
||||
--color-btn-danger-selected-bg: #b62324;
|
||||
--color-btn-danger-selected-border: #ff7b72;
|
||||
--color-btn-danger-selected-shadow: 0 0 transparent;
|
||||
--color-btn-danger-disabled-text: rgba(248,81,73,0.5);
|
||||
--color-btn-danger-disabled-bg: #0d1117;
|
||||
--color-btn-danger-disabled-counter-bg: rgba(218,54,51,0.05);
|
||||
--color-btn-danger-focus-border: #f85149;
|
||||
--color-btn-danger-focus-shadow: 0 0 0 3px rgba(248,81,73,0.4);
|
||||
--color-btn-danger-counter-bg: rgba(218,54,51,0.1);
|
||||
--color-btn-danger-icon: #f85149;
|
||||
--color-underlinenav-icon: #484f58;
|
||||
--color-underlinenav-border-hover: rgba(110,118,129,0.4);
|
||||
--color-fg-default: #c9d1d9;
|
||||
--color-fg-muted: #8b949e;
|
||||
--color-fg-subtle: #484f58;
|
||||
--color-fg-on-emphasis: #f0f6fc;
|
||||
--color-canvas-default: #0d1117;
|
||||
--color-canvas-overlay: #161b22;
|
||||
--color-canvas-inset: #010409;
|
||||
--color-canvas-subtle: #161b22;
|
||||
--color-border-default: #30363d;
|
||||
--color-border-muted: #21262d;
|
||||
--color-border-subtle: rgba(240,246,252,0.1);
|
||||
--color-shadow-small: 0 0 transparent;
|
||||
--color-shadow-medium: 0 3px 6px #010409;
|
||||
--color-shadow-large: 0 8px 24px #010409;
|
||||
--color-shadow-extra-large: 0 12px 48px #010409;
|
||||
--color-neutral-emphasis-plus: #6e7681;
|
||||
--color-neutral-emphasis: #6e7681;
|
||||
--color-neutral-muted: rgba(110,118,129,0.4);
|
||||
--color-neutral-subtle: rgba(110,118,129,0.1);
|
||||
--color-accent-fg: #58a6ff;
|
||||
--color-accent-emphasis: #1f6feb;
|
||||
--color-accent-muted: rgba(56,139,253,0.4);
|
||||
--color-accent-subtle: rgba(56,139,253,0.15);
|
||||
--color-success-fg: #3fb950;
|
||||
--color-success-emphasis: #238636;
|
||||
--color-success-muted: rgba(46,160,67,0.4);
|
||||
--color-success-subtle: rgba(46,160,67,0.15);
|
||||
--color-attention-fg: #d29922;
|
||||
--color-attention-emphasis: #9e6a03;
|
||||
--color-attention-muted: rgba(187,128,9,0.4);
|
||||
--color-attention-subtle: rgba(187,128,9,0.15);
|
||||
--color-severe-fg: #db6d28;
|
||||
--color-severe-emphasis: #bd561d;
|
||||
--color-severe-muted: rgba(219,109,40,0.4);
|
||||
--color-severe-subtle: rgba(219,109,40,0.15);
|
||||
--color-danger-fg: #f85149;
|
||||
--color-danger-emphasis: #da3633;
|
||||
--color-danger-muted: rgba(248,81,73,0.4);
|
||||
--color-danger-subtle: rgba(248,81,73,0.15);
|
||||
--color-done-fg: #a371f7;
|
||||
--color-done-emphasis: #8957e5;
|
||||
--color-done-muted: rgba(163,113,247,0.4);
|
||||
--color-done-subtle: rgba(163,113,247,0.15);
|
||||
--color-sponsors-fg: #db61a2;
|
||||
--color-sponsors-emphasis: #bf4b8a;
|
||||
--color-sponsors-muted: rgba(219,97,162,0.4);
|
||||
--color-sponsors-subtle: rgba(219,97,162,0.15);
|
||||
--color-primer-canvas-backdrop: rgba(1,4,9,0.8);
|
||||
--color-primer-canvas-sticky: rgba(13,17,23,0.95);
|
||||
--color-primer-border-active: #F78166;
|
||||
--color-primer-border-contrast: rgba(240,246,252,0.2);
|
||||
--color-primer-shadow-highlight: 0 0 transparent;
|
||||
--color-primer-shadow-inset: 0 0 transparent;
|
||||
--color-primer-shadow-focus: 0 0 0 3px #0c2d6b;
|
||||
--color-scale-black: #010409;
|
||||
--color-scale-white: #f0f6fc;
|
||||
--color-scale-gray-0: #f0f6fc;
|
||||
--color-scale-gray-1: #c9d1d9;
|
||||
--color-scale-gray-2: #b1bac4;
|
||||
--color-scale-gray-3: #8b949e;
|
||||
--color-scale-gray-4: #6e7681;
|
||||
--color-scale-gray-5: #484f58;
|
||||
--color-scale-gray-6: #30363d;
|
||||
--color-scale-gray-7: #21262d;
|
||||
--color-scale-gray-8: #161b22;
|
||||
--color-scale-gray-9: #0d1117;
|
||||
--color-scale-blue-0: #cae8ff;
|
||||
--color-scale-blue-1: #a5d6ff;
|
||||
--color-scale-blue-2: #79c0ff;
|
||||
--color-scale-blue-3: #58a6ff;
|
||||
--color-scale-blue-4: #388bfd;
|
||||
--color-scale-blue-5: #1f6feb;
|
||||
--color-scale-blue-6: #1158c7;
|
||||
--color-scale-blue-7: #0d419d;
|
||||
--color-scale-blue-8: #0c2d6b;
|
||||
--color-scale-blue-9: #051d4d;
|
||||
--color-scale-green-0: #aff5b4;
|
||||
--color-scale-green-1: #7ee787;
|
||||
--color-scale-green-2: #56d364;
|
||||
--color-scale-green-3: #3fb950;
|
||||
--color-scale-green-4: #2ea043;
|
||||
--color-scale-green-5: #238636;
|
||||
--color-scale-green-6: #196c2e;
|
||||
--color-scale-green-7: #0f5323;
|
||||
--color-scale-green-8: #033a16;
|
||||
--color-scale-green-9: #04260f;
|
||||
--color-scale-yellow-0: #f8e3a1;
|
||||
--color-scale-yellow-1: #f2cc60;
|
||||
--color-scale-yellow-2: #e3b341;
|
||||
--color-scale-yellow-3: #d29922;
|
||||
--color-scale-yellow-4: #bb8009;
|
||||
--color-scale-yellow-5: #9e6a03;
|
||||
--color-scale-yellow-6: #845306;
|
||||
--color-scale-yellow-7: #693e00;
|
||||
--color-scale-yellow-8: #4b2900;
|
||||
--color-scale-yellow-9: #341a00;
|
||||
--color-scale-orange-0: #ffdfb6;
|
||||
--color-scale-orange-1: #ffc680;
|
||||
--color-scale-orange-2: #ffa657;
|
||||
--color-scale-orange-3: #f0883e;
|
||||
--color-scale-orange-4: #db6d28;
|
||||
--color-scale-orange-5: #bd561d;
|
||||
--color-scale-orange-6: #9b4215;
|
||||
--color-scale-orange-7: #762d0a;
|
||||
--color-scale-orange-8: #5a1e02;
|
||||
--color-scale-orange-9: #3d1300;
|
||||
--color-scale-red-0: #ffdcd7;
|
||||
--color-scale-red-1: #ffc1ba;
|
||||
--color-scale-red-2: #ffa198;
|
||||
--color-scale-red-3: #ff7b72;
|
||||
--color-scale-red-4: #f85149;
|
||||
--color-scale-red-5: #da3633;
|
||||
--color-scale-red-6: #b62324;
|
||||
--color-scale-red-7: #8e1519;
|
||||
--color-scale-red-8: #67060c;
|
||||
--color-scale-red-9: #490202;
|
||||
--color-scale-purple-0: #eddeff;
|
||||
--color-scale-purple-1: #e2c5ff;
|
||||
--color-scale-purple-2: #d2a8ff;
|
||||
--color-scale-purple-3: #bc8cff;
|
||||
--color-scale-purple-4: #a371f7;
|
||||
--color-scale-purple-5: #8957e5;
|
||||
--color-scale-purple-6: #6e40c9;
|
||||
--color-scale-purple-7: #553098;
|
||||
--color-scale-purple-8: #3c1e70;
|
||||
--color-scale-purple-9: #271052;
|
||||
--color-scale-pink-0: #ffdaec;
|
||||
--color-scale-pink-1: #ffbedd;
|
||||
--color-scale-pink-2: #ff9bce;
|
||||
--color-scale-pink-3: #f778ba;
|
||||
--color-scale-pink-4: #db61a2;
|
||||
--color-scale-pink-5: #bf4b8a;
|
||||
--color-scale-pink-6: #9e3670;
|
||||
--color-scale-pink-7: #7d2457;
|
||||
--color-scale-pink-8: #5e103e;
|
||||
--color-scale-pink-9: #42062a;
|
||||
--color-scale-coral-0: #FFDDD2;
|
||||
--color-scale-coral-1: #FFC2B2;
|
||||
--color-scale-coral-2: #FFA28B;
|
||||
--color-scale-coral-3: #F78166;
|
||||
--color-scale-coral-4: #EA6045;
|
||||
--color-scale-coral-5: #CF462D;
|
||||
--color-scale-coral-6: #AC3220;
|
||||
--color-scale-coral-7: #872012;
|
||||
--color-scale-coral-8: #640D04;
|
||||
--color-scale-coral-9: #460701
|
||||
}
|
||||
}
|
||||
262
packages/extension/src/ui/connect.css
Normal file
262
packages/extension/src/ui/connect.css
Normal file
@@ -0,0 +1,262 @@
|
||||
/*
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Base styles */
|
||||
.app-container {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
|
||||
background-color: #ffffff;
|
||||
color: #1f2328;
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
min-height: 100vh;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Status Banner */
|
||||
.status-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.status-banner {
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status-banner.connected {
|
||||
color: #1f2328;
|
||||
}
|
||||
|
||||
.status-banner.connected::before {
|
||||
content: "\2705";
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.status-banner.error {
|
||||
color: #1f2328;
|
||||
}
|
||||
|
||||
.status-banner.error::before {
|
||||
content: "\274C";
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.button-container {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
margin-right: 8px;
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
.button.primary {
|
||||
background-color: #f8f9fa;
|
||||
color: #3c4043;
|
||||
border: 1px solid #dadce0;
|
||||
}
|
||||
|
||||
.button.primary:hover {
|
||||
background-color: #f1f3f4;
|
||||
border-color: #dadce0;
|
||||
box-shadow: 0 1px 2px 0 rgba(60,64,67,.1);
|
||||
}
|
||||
|
||||
.button.default {
|
||||
background-color: #f6f8fa;
|
||||
color: #24292f;
|
||||
}
|
||||
|
||||
.button.default:hover {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
.button.reject {
|
||||
background-color: #da3633;
|
||||
color: #ffffff;
|
||||
border: 1px solid #da3633;
|
||||
}
|
||||
|
||||
.button.reject:hover {
|
||||
background-color: #c73836;
|
||||
border-color: #c73836;
|
||||
}
|
||||
|
||||
/* Tab selection */
|
||||
.tab-section-title {
|
||||
padding-left: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
margin-bottom: 12px;
|
||||
color: #656d76;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
background-color: #ffffff;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.tab-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.tab-item.selected {
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
|
||||
.tab-item.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.tab-radio {
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-favicon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tab-title {
|
||||
font-weight: 500;
|
||||
color: #1f2328;
|
||||
margin-bottom: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tab-url {
|
||||
font-size: 12px;
|
||||
color: #656d76;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Link-style button */
|
||||
.link-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #0066cc;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
/* Auth token section */
|
||||
.auth-token-section {
|
||||
margin: 16px 0;
|
||||
padding: 16px;
|
||||
background-color: #f6f8fa;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.auth-token-description {
|
||||
font-size: 12px;
|
||||
color: #656d76;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.auth-token-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background-color: #ffffff;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.auth-token-code {
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
font-size: 12px;
|
||||
color: #1f2328;
|
||||
border: none;
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.auth-token-refresh {
|
||||
flex: none;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--color-fg-muted);
|
||||
background: transparent;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.auth-token-refresh svg {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.auth-token-refresh:not(:disabled):hover {
|
||||
background-color: var(--color-btn-selected-bg);
|
||||
}
|
||||
29
packages/extension/src/ui/connect.html
Normal file
29
packages/extension/src/ui/connect.html
Normal file
@@ -0,0 +1,29 @@
|
||||
<!--
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Playwright MCP extension</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="../../icons/icon-32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="../../icons/icon-16.png">
|
||||
<link rel="stylesheet" href="connect.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="connect.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
263
packages/extension/src/ui/connect.tsx
Normal file
263
packages/extension/src/ui/connect.tsx
Normal file
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Button, TabItem } from './tabItem';
|
||||
import { AuthTokenSection, getOrCreateAuthToken } from './authToken';
|
||||
|
||||
import type { TabInfo } from './tabItem';
|
||||
|
||||
type Status =
|
||||
| { type: 'connecting'; message: string }
|
||||
| { type: 'connected'; message: string }
|
||||
| { type: 'error'; message: string }
|
||||
| { type: 'error'; versionMismatch: { extensionVersion: string; } };
|
||||
|
||||
const SUPPORTED_PROTOCOL_VERSION = 1;
|
||||
|
||||
const ConnectApp: React.FC = () => {
|
||||
const [tabs, setTabs] = useState<TabInfo[]>([]);
|
||||
const [status, setStatus] = useState<Status | null>(null);
|
||||
const [showButtons, setShowButtons] = useState(true);
|
||||
const [showTabList, setShowTabList] = useState(true);
|
||||
const [clientInfo, setClientInfo] = useState('unknown');
|
||||
const [mcpRelayUrl, setMcpRelayUrl] = useState('');
|
||||
const [newTab, setNewTab] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const runAsync = async () => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const relayUrl = params.get('mcpRelayUrl');
|
||||
|
||||
if (!relayUrl) {
|
||||
handleReject('Missing mcpRelayUrl parameter in URL.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const host = new URL(relayUrl).hostname;
|
||||
if (host !== '127.0.0.1' && host !== '[::1]') {
|
||||
handleReject(`MCP extension only allows loopback connections (127.0.0.1 or [::1]). Received host: ${host}`);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
handleReject(`Invalid mcpRelayUrl parameter in URL: ${relayUrl}. ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
setMcpRelayUrl(relayUrl);
|
||||
|
||||
try {
|
||||
const client = JSON.parse(params.get('client') || '{}');
|
||||
const info = `${client.name}/${client.version}`;
|
||||
setClientInfo(info);
|
||||
setStatus({
|
||||
type: 'connecting',
|
||||
message: `🎭 Playwright MCP started from "${info}" is trying to connect. Do you want to continue?`
|
||||
});
|
||||
} catch (e) {
|
||||
setStatus({ type: 'error', message: 'Failed to parse client version.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedVersion = parseInt(params.get('protocolVersion') ?? '', 10);
|
||||
const requiredVersion = isNaN(parsedVersion) ? 1 : parsedVersion;
|
||||
if (requiredVersion > SUPPORTED_PROTOCOL_VERSION) {
|
||||
const extensionVersion = chrome.runtime.getManifest().version;
|
||||
setShowButtons(false);
|
||||
setShowTabList(false);
|
||||
setStatus({
|
||||
type: 'error',
|
||||
versionMismatch: {
|
||||
extensionVersion,
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const expectedToken = getOrCreateAuthToken();
|
||||
const token = params.get('token');
|
||||
if (token === expectedToken) {
|
||||
await connectToMCPRelay(relayUrl);
|
||||
await handleConnectToTab();
|
||||
return;
|
||||
}
|
||||
if (token) {
|
||||
handleReject('Invalid token provided.');
|
||||
return;
|
||||
}
|
||||
|
||||
await connectToMCPRelay(relayUrl);
|
||||
|
||||
// If this is a browser_navigate command, hide the tab list and show simple allow/reject
|
||||
if (params.get('newTab') === 'true') {
|
||||
setNewTab(true);
|
||||
setShowTabList(false);
|
||||
} else {
|
||||
await loadTabs();
|
||||
}
|
||||
};
|
||||
void runAsync();
|
||||
}, []);
|
||||
|
||||
const handleReject = useCallback((message: string) => {
|
||||
setShowButtons(false);
|
||||
setShowTabList(false);
|
||||
setStatus({ type: 'error', message });
|
||||
}, []);
|
||||
|
||||
const connectToMCPRelay = useCallback(async (mcpRelayUrl: string) => {
|
||||
const response = await chrome.runtime.sendMessage({ type: 'connectToMCPRelay', mcpRelayUrl });
|
||||
if (!response.success)
|
||||
handleReject(response.error);
|
||||
}, [handleReject]);
|
||||
|
||||
const loadTabs = useCallback(async () => {
|
||||
const response = await chrome.runtime.sendMessage({ type: 'getTabs' });
|
||||
if (response.success)
|
||||
setTabs(response.tabs);
|
||||
else
|
||||
setStatus({ type: 'error', message: 'Failed to load tabs: ' + response.error });
|
||||
}, []);
|
||||
|
||||
const handleConnectToTab = useCallback(async (tab?: TabInfo) => {
|
||||
setShowButtons(false);
|
||||
setShowTabList(false);
|
||||
|
||||
try {
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
type: 'connectToTab',
|
||||
mcpRelayUrl,
|
||||
tabId: tab?.id,
|
||||
windowId: tab?.windowId,
|
||||
});
|
||||
|
||||
if (response?.success) {
|
||||
setStatus({ type: 'connected', message: `MCP client "${clientInfo}" connected.` });
|
||||
} else {
|
||||
setStatus({
|
||||
type: 'error',
|
||||
message: response?.error || `MCP client "${clientInfo}" failed to connect.`
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
setStatus({
|
||||
type: 'error',
|
||||
message: `MCP client "${clientInfo}" failed to connect: ${e}`
|
||||
});
|
||||
}
|
||||
}, [clientInfo, mcpRelayUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (message: any) => {
|
||||
if (message.type === 'connectionTimeout')
|
||||
handleReject('Connection timed out.');
|
||||
};
|
||||
chrome.runtime.onMessage.addListener(listener);
|
||||
return () => {
|
||||
chrome.runtime.onMessage.removeListener(listener);
|
||||
};
|
||||
}, [handleReject]);
|
||||
|
||||
return (
|
||||
<div className='app-container'>
|
||||
<div className='content-wrapper'>
|
||||
{status && (
|
||||
<div className='status-container'>
|
||||
<StatusBanner status={status} />
|
||||
{showButtons && (
|
||||
<div className='button-container'>
|
||||
{newTab ? (
|
||||
<>
|
||||
<Button variant='primary' onClick={() => handleConnectToTab()}>
|
||||
Allow
|
||||
</Button>
|
||||
<Button variant='reject' onClick={() => handleReject('Connection rejected. This tab can be closed.')}>
|
||||
Reject
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button variant='reject' onClick={() => handleReject('Connection rejected. This tab can be closed.')}>
|
||||
Reject
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status?.type === 'connecting' && (
|
||||
<AuthTokenSection />
|
||||
)}
|
||||
|
||||
{showTabList && (
|
||||
<div>
|
||||
<div className='tab-section-title'>
|
||||
Select page to expose to MCP server:
|
||||
</div>
|
||||
<div>
|
||||
{tabs.map(tab => (
|
||||
<TabItem
|
||||
key={tab.id}
|
||||
tab={tab}
|
||||
button={
|
||||
<Button variant='primary' onClick={() => handleConnectToTab(tab)}>
|
||||
Connect
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const VersionMismatchError: React.FC<{ extensionVersion: string }> = ({ extensionVersion }) => {
|
||||
const readmeUrl = 'https://github.com/microsoft/playwright-mcp/blob/main/extension/README.md';
|
||||
const latestReleaseUrl = 'https://github.com/microsoft/playwright-mcp/releases/latest';
|
||||
return (
|
||||
<div>
|
||||
Playwright MCP version trying to connect requires newer extension version (current version: {extensionVersion}).{' '}
|
||||
<a href={latestReleaseUrl}>Click here</a> to download latest version of the extension, then drag and drop it into the Chrome Extensions page.{' '}
|
||||
See <a href={readmeUrl} target='_blank' rel='noopener noreferrer'>installation instructions</a> for more details.
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StatusBanner: React.FC<{ status: Status }> = ({ status }) => {
|
||||
return (
|
||||
<div className={`status-banner ${status.type}`}>
|
||||
{'versionMismatch' in status ? (
|
||||
<VersionMismatchError
|
||||
extensionVersion={status.versionMismatch.extensionVersion}
|
||||
/>
|
||||
) : (
|
||||
status.message
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Initialize the React app
|
||||
const container = document.getElementById('root');
|
||||
if (container) {
|
||||
const root = createRoot(container);
|
||||
root.render(<ConnectApp />);
|
||||
}
|
||||
39
packages/extension/src/ui/copyToClipboard.css
Normal file
39
packages/extension/src/ui/copyToClipboard.css
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.copy-icon {
|
||||
flex: none;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--color-fg-muted);
|
||||
background: transparent;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.copy-icon svg {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.copy-icon:not(:disabled):hover {
|
||||
background-color: var(--color-btn-selected-bg);
|
||||
}
|
||||
54
packages/extension/src/ui/copyToClipboard.tsx
Normal file
54
packages/extension/src/ui/copyToClipboard.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import * as icons from './icons';
|
||||
import './copyToClipboard.css';
|
||||
|
||||
type CopyToClipboardProps = {
|
||||
value: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A copy to clipboard button.
|
||||
*/
|
||||
export const CopyToClipboard: React.FunctionComponent<CopyToClipboardProps> = ({ value }) => {
|
||||
type IconType = 'copy' | 'check' | 'cross';
|
||||
const [icon, setIcon] = React.useState<IconType>('copy');
|
||||
|
||||
React.useEffect(() => {
|
||||
setIcon('copy');
|
||||
}, [value]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (icon === 'check') {
|
||||
const timeout = setTimeout(() => {
|
||||
setIcon('copy');
|
||||
}, 3000);
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [icon]);
|
||||
|
||||
const handleCopy = React.useCallback(() => {
|
||||
navigator.clipboard.writeText(value).then(() => {
|
||||
setIcon('check');
|
||||
}, () => {
|
||||
setIcon('cross');
|
||||
});
|
||||
}, [value]);
|
||||
const iconElement = icon === 'check' ? icons.check() : icon === 'cross' ? icons.cross() : icons.copy();
|
||||
return <button className='copy-icon' title='Copy to clipboard' aria-label='Copy to clipboard' onClick={handleCopy}>{iconElement}</button>;
|
||||
};
|
||||
32
packages/extension/src/ui/icons.css
Normal file
32
packages/extension/src/ui/icons.css
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.octicon {
|
||||
display: inline-block;
|
||||
overflow: visible !important;
|
||||
vertical-align: text-bottom;
|
||||
fill: currentColor;
|
||||
margin-right: 7px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.color-icon-success {
|
||||
color: var(--color-success-fg) !important;
|
||||
}
|
||||
|
||||
.color-text-danger {
|
||||
color: var(--color-danger-fg) !important;
|
||||
}
|
||||
49
packages/extension/src/ui/icons.tsx
Normal file
49
packages/extension/src/ui/icons.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import './icons.css';
|
||||
import './colors.css';
|
||||
|
||||
export const cross = () => {
|
||||
return <svg className='octicon color-text-danger' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'>
|
||||
<path fillRule='evenodd' d='M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z'></path>
|
||||
</svg>;
|
||||
};
|
||||
|
||||
export const check = () => {
|
||||
return <svg aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16' data-view-component='true' className='octicon color-icon-success'>
|
||||
<path fillRule='evenodd' d='M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z'></path>
|
||||
</svg>;
|
||||
};
|
||||
|
||||
export const copy = () => {
|
||||
return <svg className='octicon' viewBox='0 0 16 16' width='16' height='16' aria-hidden='true'>
|
||||
<path d='M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z'></path>
|
||||
<path d='M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z'></path>
|
||||
</svg>;
|
||||
};
|
||||
|
||||
export const refresh = () => {
|
||||
return <svg className='octicon' viewBox="0 0 16 16" width="16" height="16" aria-hidden='true'>
|
||||
<path d="M1.705 8.005a.75.75 0 0 1 .834.656 5.5 5.5 0 0 0 9.592 2.97l-1.204-1.204a.25.25 0 0 1 .177-.427h3.646a.25.25 0 0 1 .25.25v3.646a.25.25 0 0 1-.427.177l-1.38-1.38A7.002 7.002 0 0 1 1.05 8.84a.75.75 0 0 1 .656-.834ZM8 2.5a5.487 5.487 0 0 0-4.131 1.869l1.204 1.204A.25.25 0 0 1 4.896 6H1.25A.25.25 0 0 1 1 5.75V2.104a.25.25 0 0 1 .427-.177l1.38 1.38A7.002 7.002 0 0 1 14.95 7.16a.75.75 0 0 1-1.49.178A5.5 5.5 0 0 0 8 2.5Z"></path>
|
||||
</svg>;
|
||||
};
|
||||
|
||||
export const chevronDown = () => {
|
||||
return <svg className='octicon' viewBox="0 0 16 16" width="16" height="16" aria-hidden='true'>
|
||||
<path d="M12.78 5.22a.749.749 0 0 1 0 1.06l-4.25 4.25a.749.749 0 0 1-1.06 0L3.22 6.28a.749.749 0 1 1 1.06-1.06L8 8.939l3.72-3.719a.749.749 0 0 1 1.06 0Z"></path>
|
||||
</svg>;
|
||||
};
|
||||
13
packages/extension/src/ui/status.html
Normal file
13
packages/extension/src/ui/status.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Playwright MCP Bridge Status</title>
|
||||
<link rel="stylesheet" href="connect.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="status.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
112
packages/extension/src/ui/status.tsx
Normal file
112
packages/extension/src/ui/status.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Button, TabItem } from './tabItem';
|
||||
|
||||
import type { TabInfo } from './tabItem';
|
||||
import { AuthTokenSection } from './authToken';
|
||||
|
||||
interface ConnectionStatus {
|
||||
isConnected: boolean;
|
||||
connectedTabId: number | null;
|
||||
connectedTab?: TabInfo;
|
||||
}
|
||||
|
||||
const StatusApp: React.FC = () => {
|
||||
const [status, setStatus] = useState<ConnectionStatus>({
|
||||
isConnected: false,
|
||||
connectedTabId: null
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
void loadStatus();
|
||||
}, []);
|
||||
|
||||
const loadStatus = async () => {
|
||||
// Get current connection status from background script
|
||||
const { connectedTabId } = await chrome.runtime.sendMessage({ type: 'getConnectionStatus' });
|
||||
if (connectedTabId) {
|
||||
const tab = await chrome.tabs.get(connectedTabId);
|
||||
setStatus({
|
||||
isConnected: true,
|
||||
connectedTabId,
|
||||
connectedTab: {
|
||||
id: tab.id!,
|
||||
windowId: tab.windowId!,
|
||||
title: tab.title!,
|
||||
url: tab.url!,
|
||||
favIconUrl: tab.favIconUrl
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setStatus({
|
||||
isConnected: false,
|
||||
connectedTabId: null
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const openConnectedTab = async () => {
|
||||
if (!status.connectedTabId)
|
||||
return;
|
||||
await chrome.tabs.update(status.connectedTabId, { active: true });
|
||||
window.close();
|
||||
};
|
||||
|
||||
const disconnect = async () => {
|
||||
await chrome.runtime.sendMessage({ type: 'disconnect' });
|
||||
window.close();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='app-container'>
|
||||
<div className='content-wrapper'>
|
||||
{status.isConnected && status.connectedTab ? (
|
||||
<div>
|
||||
<div className='tab-section-title'>
|
||||
Page with connected MCP client:
|
||||
</div>
|
||||
<div>
|
||||
<TabItem
|
||||
tab={status.connectedTab}
|
||||
button={
|
||||
<Button variant='primary' onClick={disconnect}>
|
||||
Disconnect
|
||||
</Button>
|
||||
}
|
||||
onClick={openConnectedTab}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className='status-banner'>
|
||||
No MCP clients are currently connected.
|
||||
</div>
|
||||
)}
|
||||
<AuthTokenSection />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Initialize the React app
|
||||
const container = document.getElementById('root');
|
||||
if (container) {
|
||||
const root = createRoot(container);
|
||||
root.render(<StatusApp />);
|
||||
}
|
||||
67
packages/extension/src/ui/tabItem.tsx
Normal file
67
packages/extension/src/ui/tabItem.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export interface TabInfo {
|
||||
id: number;
|
||||
windowId: number;
|
||||
title: string;
|
||||
url: string;
|
||||
favIconUrl?: string;
|
||||
}
|
||||
|
||||
export const Button: React.FC<{ variant: 'primary' | 'default' | 'reject'; onClick: () => void; children: React.ReactNode }> = ({
|
||||
variant,
|
||||
onClick,
|
||||
children
|
||||
}) => {
|
||||
return (
|
||||
<button className={`button ${variant}`} onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export interface TabItemProps {
|
||||
tab: TabInfo;
|
||||
onClick?: () => void;
|
||||
button?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const TabItem: React.FC<TabItemProps> = ({
|
||||
tab,
|
||||
onClick,
|
||||
button
|
||||
}) => {
|
||||
return (
|
||||
<div className='tab-item' onClick={onClick} style={onClick ? { cursor: 'pointer' } : undefined}>
|
||||
<img
|
||||
src={tab.favIconUrl || 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><rect width="16" height="16" fill="%23f6f8fa"/></svg>'}
|
||||
alt=''
|
||||
className='tab-favicon'
|
||||
/>
|
||||
<div className='tab-content'>
|
||||
<div className='tab-title'>
|
||||
{tab.title || 'Untitled'}
|
||||
</div>
|
||||
<div className='tab-url'>{tab.url}</div>
|
||||
</div>
|
||||
{button}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
4
packages/extension/src/ui/tsconfig.json
Normal file
4
packages/extension/src/ui/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
// Help VSCode to find right tsconfig file.
|
||||
{
|
||||
"extends": "../../tsconfig.ui.json"
|
||||
}
|
||||
Reference in New Issue
Block a user