mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-05-17 12:31:20 +00:00
Replace the Browser viewer’s screenshot polling with CDP screencast streaming for much smoother navigation. The runtime now starts/stops CDP screencasts cleanly, acknowledges frames, drops stale frames, and keeps the WebSocket payload compatible with the existing viewer. Also fixes modal viewport sizing by sending the initial stage dimensions on subscribe, applying CDP emulation sizing before the first frame, avoiding image stretching, and increasing screencast JPEG quality to 92. Regression coverage was added for the screencast path, frame ack/drop behavior, viewport sizing, and UI rendering assumptions. -- Still needs thorough performance audit and optimization --
889 lines
29 KiB
HTML
889 lines
29 KiB
HTML
<html class="browser-modal">
|
|
|
|
<head>
|
|
<title>Browser</title>
|
|
<script type="module">
|
|
import { store } from "/plugins/_browser/webui/browser-store.js";
|
|
</script>
|
|
</head>
|
|
|
|
<body class="browser-modal-body">
|
|
<div x-data>
|
|
<template x-if="$store.browserPage">
|
|
<div class="browser-panel" x-create="$store.browserPage.onOpen($el)" x-destroy="$store.browserPage.cleanup()"
|
|
@keydown.window="$store.browserPage.sendKey($event)">
|
|
<div class="browser-meta">
|
|
<div class="browser-meta-top">
|
|
<div class="browser-session-tabs" role="tablist" aria-label="Browser sessions">
|
|
<template x-for="browser in $store.browserPage.browsers" :key="browser.id">
|
|
<div class="browser-tab-shell" :class="{ 'is-active': $store.browserPage.isActiveBrowser(browser) }">
|
|
<button type="button" class="browser-tab" role="tab"
|
|
:aria-selected="$store.browserPage.isActiveBrowser(browser).toString()"
|
|
:title="$store.browserPage.browserTabLabel(browser)"
|
|
@click="$store.browserPage.selectBrowser(browser.id)">
|
|
<span class="material-symbols-outlined browser-tab-icon" aria-hidden="true">language</span>
|
|
<span class="browser-tab-title" x-text="$store.browserPage.browserTabTitle(browser)"></span>
|
|
</button>
|
|
<button type="button" class="browser-tab-close"
|
|
:title="'Close ' + $store.browserPage.browserTabLabel(browser)"
|
|
:aria-label="'Close ' + $store.browserPage.browserTabLabel(browser)"
|
|
@click.stop="$confirmClick($event, () => $store.browserPage.command('close', { browser_id: browser.id }))">
|
|
<span class="material-symbols-outlined">close</span>
|
|
</button>
|
|
</div>
|
|
</template>
|
|
<button type="button" class="browser-new-tab" title="New Browser" aria-label="New Browser"
|
|
@click="$store.browserPage.openNewBrowser()">
|
|
<span class="material-symbols-outlined">add</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="browser-session-controls">
|
|
<div class="browser-extension-menu" @click.outside="$store.browserPage.closeExtensionsMenu()"
|
|
@keydown.escape.window="$store.browserPage.closeExtensionsMenu()">
|
|
<button type="button" class="btn btn-icon-action browser-extensions" title="Browser extensions"
|
|
aria-label="Browser extensions" @click.stop="$store.browserPage.toggleExtensionsMenu()"
|
|
:aria-expanded="$store.browserPage.extensionMenuOpen.toString()"
|
|
:class="{ 'is-active': $store.browserPage.status?.extensions?.active }">
|
|
<span class="material-symbols-outlined">extension</span>
|
|
</button>
|
|
<div class="browser-extension-dropdown" x-show="$store.browserPage.extensionMenuOpen" x-transition
|
|
style="display: none;">
|
|
<div class="browser-extension-preset">
|
|
<label for="browser-model-preset">Browser LLM Preset</label>
|
|
<select id="browser-model-preset" x-model="$store.browserPage.modelPreset"
|
|
@change="$store.browserPage.setBrowserModelPreset($event.target.value)"
|
|
:disabled="$store.browserPage.modelPresetSaving">
|
|
<option value="">Default Main Model</option>
|
|
<template x-for="preset in $store.browserPage.modelPresetOptions" :key="preset.name">
|
|
<option :value="preset.name" x-text="preset.label"></option>
|
|
</template>
|
|
</select>
|
|
<div class="browser-extension-preset-summary" x-text="$store.browserPage.modelPresetSummary()">
|
|
</div>
|
|
</div>
|
|
<div class="browser-extension-section-title">Chrome Extensions</div>
|
|
<button type="button" class="dropdown-item" @click="$store.browserPage.createExtensionWithAgent()">
|
|
<span class="material-symbols-outlined">add_circle</span>
|
|
<span>Create New Extension with A0</span>
|
|
</button>
|
|
<div class="browser-extension-url">
|
|
<label for="browser-extension-url">Input a Chrome Web Store URL</label>
|
|
<input id="browser-extension-url" type="url" x-model="$store.browserPage.extensionInstallUrl"
|
|
@keydown.enter.prevent="$store.browserPage.installExtensionFromUrl()"
|
|
placeholder="https://chromewebstore.google.com/detail/..." />
|
|
<div class="browser-extension-url-actions">
|
|
<button type="button" class="btn btn-ok" @click="$store.browserPage.installExtensionFromUrl()"
|
|
:disabled="$store.browserPage.extensionActionLoading">
|
|
<span class="material-symbols-outlined"
|
|
:class="{ spinning: $store.browserPage.extensionActionLoading }"
|
|
x-text="$store.browserPage.extensionActionLoading ? 'progress_activity' : 'download'"></span>
|
|
<span>Install URL</span>
|
|
</button>
|
|
<button type="button" class="btn btn-field"
|
|
@click="$store.browserPage.askAgentInstallExtension()">
|
|
<span class="material-symbols-outlined">psychology_alt</span>
|
|
<span x-text="$store.browserPage.extensionAssistantActionLabel()">Scan with A0</span>
|
|
</button>
|
|
</div>
|
|
<div class="browser-extension-warning" x-show="$store.browserPage.hasExtensionInstallUrl()"
|
|
x-transition style="display: none;">
|
|
<span class="material-symbols-outlined">warning</span>
|
|
<span>
|
|
Extensions run inside the Docker browser sandbox, but malicious or buggy extensions can still
|
|
damage that environment. Review what you install.
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="browser-extension-list"
|
|
x-show="$store.browserPage.extensionsListLoading || $store.browserPage.extensionsList.length"
|
|
style="display: none;">
|
|
<div class="browser-extension-list-header">
|
|
<span>Installed extensions</span>
|
|
<span class="material-symbols-outlined spinning"
|
|
x-show="$store.browserPage.extensionsListLoading">progress_activity</span>
|
|
</div>
|
|
<template x-for="extension in $store.browserPage.extensionsList" :key="extension.path">
|
|
<label class="browser-extension-row" :title="extension.path">
|
|
<span class="browser-extension-row-text">
|
|
<span class="browser-extension-name" x-text="extension.name || 'Unnamed extension'"></span>
|
|
<span class="browser-extension-meta"
|
|
x-text="$store.browserPage.extensionVersionLabel(extension)"></span>
|
|
</span>
|
|
<span class="browser-extension-toggle">
|
|
<input type="checkbox" :checked="extension.enabled"
|
|
:disabled="$store.browserPage.extensionToggleLoadingPath === extension.path"
|
|
@change="$store.browserPage.setExtensionEnabled(extension, $event.target.checked, $event.target)" />
|
|
<span class="browser-extension-switch"></span>
|
|
</span>
|
|
</label>
|
|
</template>
|
|
</div>
|
|
<button type="button" class="dropdown-item" @click="$store.browserPage.openExtensionsSettings()">
|
|
<span class="material-symbols-outlined">tune</span>
|
|
<span>Settings</span>
|
|
</button>
|
|
<div class="browser-extension-message" x-show="$store.browserPage.extensionActionMessage"
|
|
x-text="$store.browserPage.extensionActionMessage"></div>
|
|
<div class="browser-extension-error" x-show="$store.browserPage.extensionActionError"
|
|
x-text="$store.browserPage.extensionActionError"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="browser-toolbar">
|
|
<div class="browser-navigation">
|
|
<button class="btn btn-icon-action" title="Back" @click="$store.browserPage.command('back')"
|
|
:disabled="!$store.browserPage.activeBrowserId">
|
|
<span class="material-symbols-outlined">arrow_back</span>
|
|
</button>
|
|
<button class="btn btn-icon-action" title="Forward" @click="$store.browserPage.command('forward')"
|
|
:disabled="!$store.browserPage.activeBrowserId">
|
|
<span class="material-symbols-outlined">arrow_forward</span>
|
|
</button>
|
|
<button class="btn btn-icon-action" title="Reload" @click="$store.browserPage.command('reload')"
|
|
:disabled="!$store.browserPage.activeBrowserId">
|
|
<span class="material-symbols-outlined">refresh</span>
|
|
</button>
|
|
</div>
|
|
|
|
<form class="browser-address-form" @submit.prevent="$store.browserPage.go()">
|
|
<span class="material-symbols-outlined browser-address-icon">language</span>
|
|
<input class="browser-address" x-model="$store.browserPage.address"
|
|
@focus="$store.browserPage.onAddressFocus()" @blur="$store.browserPage.onAddressBlur()"
|
|
placeholder="https://example.com" autocomplete="off" />
|
|
</form>
|
|
</div>
|
|
|
|
<div class="browser-status" x-show="$store.browserPage.loading">
|
|
<span class="material-symbols-outlined spinning">progress_activity</span>
|
|
<span x-text="$store.browserPage.loadingMessage()">Connecting browser...</span>
|
|
</div>
|
|
<div class="browser-error" x-show="$store.browserPage.error" x-text="$store.browserPage.error"></div>
|
|
|
|
<div class="browser-stage" tabindex="0" @click="$el.focus()"
|
|
@wheel.prevent="$store.browserPage.sendWheel($event)">
|
|
<template x-if="$store.browserPage.frameSrc">
|
|
<img class="browser-frame" :src="$store.browserPage.frameSrc"
|
|
@click="$store.browserPage.sendMouse('click', $event)"
|
|
@mousemove.throttle.250ms="$store.browserPage.sendMouse('move', $event)" draggable="false" />
|
|
</template>
|
|
<template x-if="!$store.browserPage.frameSrc && !$store.browserPage.loading">
|
|
<div class="browser-empty">
|
|
<span class="material-symbols-outlined">captive_portal</span>
|
|
<button class="btn btn-field" @click="$store.browserPage.command('open', { url: 'about:blank' })">Open
|
|
Browser</button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<style>
|
|
.modal-inner.browser-modal {
|
|
box-sizing: border-box;
|
|
container-type: inline-size;
|
|
width: min(78vw, 1120px);
|
|
height: min(88vh, 900px);
|
|
min-width: min(320px, calc(100vw - 16px));
|
|
min-height: min(480px, calc(100vh - 16px));
|
|
max-width: calc(100vw - 16px);
|
|
max-height: calc(100vh - 16px);
|
|
resize: both;
|
|
border: 1px solid color-mix(in srgb, var(--color-border) 75%, transparent);
|
|
border-radius: 7px;
|
|
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.32);
|
|
background: color-mix(in srgb, var(--color-background) 94%, #000 6%);
|
|
}
|
|
|
|
.modal.modal-floating {
|
|
pointer-events: none;
|
|
}
|
|
|
|
.modal.modal-floating .modal-inner {
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.modal-inner.browser-modal .modal-header {
|
|
min-height: 34px;
|
|
padding: 0.35rem 0.75rem 0.35rem 1rem;
|
|
cursor: move;
|
|
user-select: none;
|
|
background: color-mix(in srgb, var(--color-background) 92%, #000 8%);
|
|
border-bottom: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent);
|
|
}
|
|
|
|
.modal-inner.browser-modal .modal-title {
|
|
font-size: 0.95rem;
|
|
letter-spacing: 0;
|
|
}
|
|
|
|
.modal-inner.browser-modal .modal-close {
|
|
font-size: 1.35rem;
|
|
line-height: 1;
|
|
}
|
|
|
|
.modal-inner.browser-modal .modal-scroll {
|
|
flex: 1 1 auto;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
padding: 0;
|
|
}
|
|
|
|
.modal-inner.browser-modal .modal-bd.browser-modal-body {
|
|
box-sizing: border-box;
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
padding: 0;
|
|
min-height: 0;
|
|
}
|
|
|
|
.modal-inner.browser-modal .modal-bd.browser-modal-body>div[x-data] {
|
|
display: flex;
|
|
flex: 1 1 auto;
|
|
min-height: 0;
|
|
}
|
|
|
|
.browser-panel {
|
|
--browser-chrome-surface: color-mix(in srgb, var(--color-background) 92%, #000 8%);
|
|
--browser-chrome-border: color-mix(in srgb, var(--color-border) 58%, transparent);
|
|
--browser-tab-hover-border: color-mix(in srgb, var(--color-border) 78%, transparent);
|
|
--browser-control-size: 34px;
|
|
--browser-address-height: 34px;
|
|
--browser-tab-height: 34px;
|
|
--browser-tab-close-size: 28px;
|
|
--browser-control-radius: 0.55rem;
|
|
box-sizing: border-box;
|
|
display: flex;
|
|
flex: 1 1 auto;
|
|
flex-direction: column;
|
|
gap: 0;
|
|
height: 100%;
|
|
min-height: 0;
|
|
}
|
|
|
|
.browser-toolbar {
|
|
display: grid;
|
|
grid-template-columns: auto minmax(0, 1fr);
|
|
grid-template-areas: "nav address";
|
|
gap: 8px;
|
|
align-items: center;
|
|
padding: 8px 10px 9px;
|
|
border-bottom: 0;
|
|
background: transparent;
|
|
}
|
|
|
|
.browser-navigation {
|
|
grid-area: nav;
|
|
display: flex;
|
|
gap: 6px;
|
|
}
|
|
|
|
.browser-panel .btn-icon-action,
|
|
.browser-new-tab {
|
|
width: var(--browser-control-size);
|
|
min-width: var(--browser-control-size);
|
|
height: var(--browser-control-size);
|
|
min-height: var(--browser-control-size);
|
|
border-radius: var(--browser-control-radius);
|
|
}
|
|
|
|
.browser-panel .btn-icon-action {
|
|
color: color-mix(in srgb, var(--color-text) 72%, var(--color-primary) 28%);
|
|
background: color-mix(in srgb, var(--color-background) 26%, transparent);
|
|
border-color: var(--browser-chrome-border);
|
|
}
|
|
|
|
.browser-panel .btn-icon-action:hover:not(:disabled) {
|
|
color: var(--color-text);
|
|
border-color: color-mix(in srgb, var(--color-primary) 34%, var(--browser-chrome-border));
|
|
background: color-mix(in srgb, var(--color-background-hover) 62%, transparent);
|
|
box-shadow: none;
|
|
}
|
|
|
|
.browser-panel .btn-icon-action .material-symbols-outlined {
|
|
font-size: 1.12rem;
|
|
}
|
|
|
|
.browser-address-form {
|
|
grid-area: address;
|
|
min-width: 0;
|
|
position: relative;
|
|
margin: 0;
|
|
}
|
|
|
|
.browser-address-icon {
|
|
position: absolute;
|
|
left: 10px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
font-size: 18px;
|
|
opacity: 0.58;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.browser-address {
|
|
width: 100%;
|
|
min-height: var(--browser-address-height);
|
|
padding: 5px 10px 5px 34px;
|
|
border-radius: var(--browser-control-radius);
|
|
border: 1px solid var(--browser-chrome-border);
|
|
background: var(--color-input);
|
|
color: var(--color-text);
|
|
font: inherit;
|
|
}
|
|
|
|
.browser-meta {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr);
|
|
gap: 0;
|
|
padding: 7px 10px 0;
|
|
border-bottom: 1px solid var(--browser-chrome-border);
|
|
background: var(--browser-chrome-surface);
|
|
}
|
|
|
|
.browser-meta-top {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
gap: 10px;
|
|
align-items: end;
|
|
}
|
|
|
|
.browser-session-tabs {
|
|
display: flex;
|
|
align-items: end;
|
|
gap: 4px;
|
|
min-width: 0;
|
|
overflow-x: auto;
|
|
padding: 1px 0 0;
|
|
scrollbar-width: thin;
|
|
overflow-y: hidden;
|
|
}
|
|
|
|
.browser-session-tabs::-webkit-scrollbar {
|
|
height: 4px;
|
|
}
|
|
|
|
.browser-session-tabs::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
.browser-session-tabs::-webkit-scrollbar-thumb {
|
|
background: color-mix(in srgb, var(--color-border) 78%, transparent);
|
|
border-radius: 999px;
|
|
}
|
|
|
|
.browser-new-tab {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
border: 1px solid transparent;
|
|
color: var(--color-text);
|
|
cursor: pointer;
|
|
transition: background-color 0.18s cubic-bezier(0.4, 0, 0.2, 1),
|
|
border-color 0.18s cubic-bezier(0.4, 0, 0.2, 1),
|
|
color 0.18s cubic-bezier(0.4, 0, 0.2, 1),
|
|
opacity 0.18s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.browser-tab-shell {
|
|
flex: 0 1 210px;
|
|
position: relative;
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) var(--browser-tab-close-size);
|
|
align-items: center;
|
|
gap: 3px;
|
|
min-width: 128px;
|
|
max-width: 250px;
|
|
height: var(--browser-tab-height);
|
|
padding: 0 7px 0 10px;
|
|
border: 1px solid transparent;
|
|
border-radius: var(--browser-control-radius) var(--browser-control-radius) 0 0;
|
|
background: transparent;
|
|
opacity: 0.72;
|
|
transition: border-color 0.18s cubic-bezier(0.4, 0, 0.2, 1),
|
|
color 0.18s cubic-bezier(0.4, 0, 0.2, 1),
|
|
opacity 0.18s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.browser-tab-shell:hover,
|
|
.browser-tab-shell:focus-within {
|
|
border-color: var(--browser-tab-hover-border);
|
|
opacity: 0.94;
|
|
}
|
|
|
|
.browser-tab-shell.is-active {
|
|
z-index: 2;
|
|
margin-bottom: -1px;
|
|
border-color: var(--browser-chrome-border);
|
|
background: transparent;
|
|
opacity: 1;
|
|
box-shadow: none;
|
|
}
|
|
|
|
.browser-tab {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: flex-start;
|
|
gap: 8px;
|
|
min-width: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
padding: 0;
|
|
border: 0;
|
|
border-radius: 0;
|
|
background: transparent;
|
|
color: inherit;
|
|
cursor: pointer;
|
|
font: inherit;
|
|
text-align: left;
|
|
}
|
|
|
|
.browser-tab:hover {
|
|
background: transparent;
|
|
}
|
|
|
|
.browser-tab:focus-visible,
|
|
.browser-tab-close:focus-visible {
|
|
outline: 1px solid color-mix(in srgb, var(--color-primary) 70%, transparent);
|
|
outline-offset: 1px;
|
|
}
|
|
|
|
.browser-tab-icon {
|
|
flex: 0 0 auto;
|
|
color: color-mix(in srgb, var(--color-text) 72%, var(--color-primary) 28%);
|
|
font-size: 1.04rem;
|
|
line-height: 1;
|
|
}
|
|
|
|
.browser-tab-title {
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
font-size: 0.88rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.browser-tab-close {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: var(--browser-tab-close-size);
|
|
min-width: var(--browser-tab-close-size);
|
|
height: var(--browser-tab-close-size);
|
|
min-height: var(--browser-tab-close-size);
|
|
padding: 0;
|
|
border: 0;
|
|
border-radius: 50%;
|
|
background: transparent;
|
|
color: color-mix(in srgb, var(--color-text) 52%, var(--color-primary) 48%);
|
|
cursor: pointer;
|
|
opacity: 0.72;
|
|
transition: background-color 0.18s cubic-bezier(0.4, 0, 0.2, 1),
|
|
color 0.18s cubic-bezier(0.4, 0, 0.2, 1),
|
|
opacity 0.18s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.browser-tab-close:hover,
|
|
.browser-tab-close.confirming {
|
|
background: color-mix(in srgb, var(--color-background-hover) 70%, transparent);
|
|
color: var(--color-text);
|
|
opacity: 1;
|
|
}
|
|
|
|
.browser-tab-close .material-symbols-outlined {
|
|
font-size: 0.82rem;
|
|
line-height: 1;
|
|
}
|
|
|
|
.browser-new-tab {
|
|
flex: 0 0 var(--browser-control-size);
|
|
justify-content: center;
|
|
padding: 0;
|
|
border-color: transparent;
|
|
border-radius: var(--browser-control-radius);
|
|
background: transparent;
|
|
color: color-mix(in srgb, var(--color-text) 62%, var(--color-primary) 38%);
|
|
}
|
|
|
|
.browser-new-tab:hover {
|
|
background: color-mix(in srgb, var(--color-background-hover) 58%, transparent);
|
|
color: var(--color-text);
|
|
}
|
|
|
|
.browser-new-tab .material-symbols-outlined {
|
|
font-size: 1.18rem;
|
|
}
|
|
|
|
.browser-session-controls {
|
|
display: flex;
|
|
align-items: end;
|
|
gap: 6px;
|
|
min-width: 0;
|
|
padding-bottom: 1px;
|
|
}
|
|
|
|
.browser-session-controls .browser-extensions.is-active {
|
|
color: #2e7d32;
|
|
}
|
|
|
|
.browser-extension-menu {
|
|
position: relative;
|
|
display: flex;
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
.browser-extension-dropdown {
|
|
position: absolute;
|
|
top: calc(100% + 6px);
|
|
right: 0;
|
|
z-index: 40;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 7px;
|
|
width: min(360px, calc(100vw - 24px));
|
|
max-height: min(72vh, 560px);
|
|
overflow-y: auto;
|
|
padding: 10px;
|
|
border: 1px solid color-mix(in srgb, var(--color-border) 78%, transparent);
|
|
border-radius: 7px;
|
|
background: var(--color-background);
|
|
box-shadow: 0 16px 38px rgba(0, 0, 0, 0.28);
|
|
}
|
|
|
|
.browser-extension-dropdown .dropdown-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
width: 100%;
|
|
min-height: 34px;
|
|
padding: 7px 9px;
|
|
border: 0;
|
|
border-radius: 6px;
|
|
background: transparent;
|
|
color: var(--color-text);
|
|
font-weight: 600;
|
|
text-align: left;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.browser-extension-dropdown .dropdown-item:hover {
|
|
background: color-mix(in srgb, var(--color-panel) 82%, transparent);
|
|
}
|
|
|
|
.browser-extension-warning,
|
|
.browser-extension-message,
|
|
.browser-extension-error {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 8px;
|
|
padding: 9px 10px;
|
|
border-radius: 7px;
|
|
font-size: 0.8rem;
|
|
line-height: 1.35;
|
|
}
|
|
|
|
.browser-extension-warning {
|
|
border: 1px solid color-mix(in srgb, #d97706 42%, var(--color-border));
|
|
background: color-mix(in srgb, #d97706 14%, var(--color-background));
|
|
color: color-mix(in srgb, var(--color-text) 86%, #92400e);
|
|
}
|
|
|
|
.browser-extension-warning .material-symbols-outlined {
|
|
color: #b45309;
|
|
font-size: 19px;
|
|
}
|
|
|
|
.browser-extension-url {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 7px;
|
|
padding: 8px;
|
|
border: 1px solid color-mix(in srgb, var(--color-border) 58%, transparent);
|
|
border-radius: 7px;
|
|
background: var(--color-panel);
|
|
}
|
|
|
|
.browser-extension-url label {
|
|
font-size: 0.76rem;
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
.browser-extension-url input {
|
|
min-width: 0;
|
|
min-height: 32px;
|
|
padding: 6px 8px;
|
|
border: 1px solid color-mix(in srgb, var(--color-border) 72%, transparent);
|
|
border-radius: 6px;
|
|
background: var(--color-input);
|
|
color: var(--color-text);
|
|
}
|
|
|
|
.browser-extension-url-actions {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 7px;
|
|
}
|
|
|
|
.browser-extension-url-actions .btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 6px;
|
|
width: 100%;
|
|
min-width: 0;
|
|
min-height: 30px;
|
|
}
|
|
|
|
.browser-extension-preset,
|
|
.browser-extension-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 7px;
|
|
}
|
|
|
|
.browser-extension-section-title {
|
|
padding: 6px 2px 0;
|
|
color: var(--color-text-secondary);
|
|
font-size: 0.76rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.browser-extension-preset label,
|
|
.browser-extension-list-header {
|
|
font-size: 0.76rem;
|
|
font-weight: 650;
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
.browser-extension-preset select {
|
|
width: 100%;
|
|
min-height: 32px;
|
|
padding: 5px 8px;
|
|
border: 1px solid color-mix(in srgb, var(--color-border) 72%, transparent);
|
|
border-radius: 6px;
|
|
background: var(--color-input);
|
|
color: var(--color-text);
|
|
}
|
|
|
|
.browser-extension-preset-summary,
|
|
.browser-extension-meta {
|
|
color: var(--color-text-secondary);
|
|
font-size: 0.75rem;
|
|
padding-left: var(--spacing-xxs);
|
|
}
|
|
|
|
.browser-extension-list-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
}
|
|
|
|
.browser-extension-list-header .material-symbols-outlined {
|
|
font-size: 16px;
|
|
}
|
|
|
|
.browser-extension-row {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
align-items: center;
|
|
gap: 10px;
|
|
min-height: 34px;
|
|
padding: 5px 0;
|
|
}
|
|
|
|
.browser-extension-row+.browser-extension-row {
|
|
border-top: 1px solid color-mix(in srgb, var(--color-border) 42%, transparent);
|
|
}
|
|
|
|
.browser-extension-row-text {
|
|
display: flex;
|
|
min-width: 0;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
.browser-extension-name {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
font-size: 0.82rem;
|
|
font-weight: 650;
|
|
color: var(--color-text);
|
|
}
|
|
|
|
.browser-extension-toggle {
|
|
position: relative;
|
|
display: inline-flex;
|
|
flex: 0 0 auto;
|
|
align-items: center;
|
|
width: 38px;
|
|
height: 22px;
|
|
}
|
|
|
|
.browser-extension-toggle input {
|
|
position: absolute;
|
|
inset: 0;
|
|
margin: 0;
|
|
opacity: 0;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.browser-extension-toggle input:disabled {
|
|
cursor: wait;
|
|
}
|
|
|
|
.browser-extension-switch {
|
|
width: 100%;
|
|
height: 100%;
|
|
border-radius: 999px;
|
|
background: color-mix(in srgb, var(--color-border) 78%, transparent);
|
|
transition: background-color 0.18s cubic-bezier(0.4, 0, 0.2, 1),
|
|
opacity 0.18s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.browser-extension-switch::after {
|
|
content: "";
|
|
position: absolute;
|
|
top: 3px;
|
|
left: 3px;
|
|
width: 16px;
|
|
height: 16px;
|
|
border-radius: 50%;
|
|
background: var(--color-background);
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.24);
|
|
transition: transform 0.18s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.browser-extension-toggle input:checked+.browser-extension-switch {
|
|
background: color-mix(in srgb, var(--color-primary) 76%, #16a34a);
|
|
}
|
|
|
|
.browser-extension-toggle input:checked+.browser-extension-switch::after {
|
|
transform: translateX(16px);
|
|
}
|
|
|
|
.browser-extension-toggle input:disabled+.browser-extension-switch {
|
|
opacity: 0.58;
|
|
}
|
|
|
|
.browser-extension-message {
|
|
background: color-mix(in srgb, #15803d 12%, var(--color-background));
|
|
color: color-mix(in srgb, var(--color-text) 88%, #166534);
|
|
}
|
|
|
|
.browser-extension-error {
|
|
background: color-mix(in srgb, #be123c 12%, var(--color-background));
|
|
color: #9f1239;
|
|
}
|
|
|
|
.browser-stage {
|
|
flex: 1 1 auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
background: #fff;
|
|
outline: none;
|
|
}
|
|
|
|
.browser-frame {
|
|
flex: 0 0 auto;
|
|
display: block;
|
|
width: 100%;
|
|
height: auto;
|
|
min-width: 0;
|
|
image-rendering: auto;
|
|
user-select: none;
|
|
background: #fff;
|
|
}
|
|
|
|
.browser-status,
|
|
.browser-error,
|
|
.browser-empty {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
min-height: 42px;
|
|
font-size: 0.88rem;
|
|
}
|
|
|
|
.browser-status,
|
|
.browser-error {
|
|
padding: 0 12px;
|
|
}
|
|
|
|
.browser-modal .spinning,
|
|
.browser-panel .spinning {
|
|
display: inline-block;
|
|
transform-origin: center;
|
|
animation: browser-spin 0.8s linear infinite;
|
|
}
|
|
|
|
@keyframes browser-spin {
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
.browser-error {
|
|
color: #9f1239;
|
|
}
|
|
|
|
.browser-empty {
|
|
display: grid;
|
|
flex: 1 1 auto;
|
|
width: 100%;
|
|
min-height: 0;
|
|
justify-items: center;
|
|
align-content: center;
|
|
text-align: center;
|
|
padding: 24px;
|
|
color: var(--color-text);
|
|
background: var(--color-background);
|
|
}
|
|
|
|
@container (max-width: 460px) {
|
|
.browser-meta-top {
|
|
grid-template-columns: minmax(0, 1fr);
|
|
}
|
|
|
|
.browser-session-controls {
|
|
width: 100%;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.browser-session-tabs {
|
|
width: 100%;
|
|
}
|
|
|
|
.browser-tab-shell {
|
|
flex-basis: 170px;
|
|
min-width: 142px;
|
|
}
|
|
|
|
.browser-new-tab,
|
|
.browser-session-controls .btn-icon-action {
|
|
width: var(--browser-control-size);
|
|
height: var(--browser-control-size);
|
|
}
|
|
|
|
.browser-extension-dropdown {
|
|
right: 0;
|
|
left: auto;
|
|
width: min(296px, calc(100vw - 72px));
|
|
}
|
|
|
|
.browser-address {
|
|
min-height: var(--browser-address-height);
|
|
}
|
|
}
|
|
</style>
|
|
</body>
|
|
|
|
</html>
|