agent-zero/plugins/_browser/webui/browser-panel.html
Alessandro 370ac9b878 Make Browser dockable and stabilize canvas interaction
Extend Browser into a reusable panel that can run in either the Universal Canvas or the floating modal. Add canvas registration, dock/undock behavior, and keep the existing modal path working as a fallback.

Stabilize tab switching with viewer tokens and stale-frame rejection, prevent command snapshots from crossing active tabs, and keep tab changes responsive.

Improve canvas navigation and scrolling by making screencast polling non-blocking and removing page-settle waits from wheel input, so the visible frame updates promptly without stretch/catch-up artifacts.

Polish Browser busy feedback with a spinner-only status affordance to avoid misleading “updating browser” copy.
2026-04-26 17:09:21 +02:00

923 lines
30 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, xAttrs($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)"
:disabled="$store.browserPage.isBusy()"
@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"
:disabled="$store.browserPage.isBusy()"
@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 || $store.browserPage.isBusy()">
<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 || $store.browserPage.isBusy()">
<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 || $store.browserPage.isBusy()">
<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()"
:disabled="$store.browserPage.isBusy()"
placeholder="https://example.com" autocomplete="off" />
</form>
</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)">
<div class="browser-status" x-show="$store.browserPage.isBusy()">
<span class="material-symbols-outlined spinning">progress_activity</span>
<span x-text="$store.browserPage.loadingMessage()">Connecting browser...</span>
</div>
<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.isBusy()">
<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>x-component,
.modal-inner.browser-modal .modal-bd.browser-modal-body>x-component>div,
.modal-inner.browser-modal .modal-bd.browser-modal-body>div {
display: flex;
flex: 1 1 auto;
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
}
.modal-inner.browser-modal .browser-panel {
height: 100%;
max-height: 100%;
overflow: hidden;
}
.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;
position: relative;
}
.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: 6px;
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;
position: relative;
background: #fff;
outline: none;
}
.browser-frame {
flex: 1 1 auto;
display: block;
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
object-fit: fill;
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-status {
position: absolute;
top: 10px;
left: 10px;
z-index: 5;
width: max-content;
max-width: calc(100% - 20px);
min-height: 32px;
padding: 7px 10px;
border: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent);
border-radius: 7px;
background: color-mix(in srgb, var(--color-background) 92%, transparent);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
pointer-events: none;
}
.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>