mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-05-19 16:31:30 +00:00
Improve canvas and modal surface controls
Remove the fixed right-canvas width limits so the panel can shrink to zero and grow across the available workspace. Add Browser/Desktop surface-switch buttons to modal headers using the same registered surface metadata as the canvas controls, while preserving modal-mode preference and dock-to-canvas behavior. Add regression coverage for the unlimited canvas sizing and modal surface switcher controls.
This commit is contained in:
parent
78570e5689
commit
3e07099d56
6 changed files with 162 additions and 16 deletions
|
|
@ -670,6 +670,7 @@ def test_browser_and_desktop_surface_buttons_remember_latest_window_mode():
|
|||
encoding="utf-8"
|
||||
)
|
||||
modals_js = (PROJECT_ROOT / "webui" / "js" / "modals.js").read_text(encoding="utf-8")
|
||||
modals_css = (PROJECT_ROOT / "webui" / "css" / "modals.css").read_text(encoding="utf-8")
|
||||
|
||||
assert "surfaceModes: {}" in canvas_store
|
||||
assert "recordSurfaceMode(surfaceId" in canvas_store
|
||||
|
|
@ -685,11 +686,21 @@ def test_browser_and_desktop_surface_buttons_remember_latest_window_mode():
|
|||
assert '@click="$store.rightCanvas.open(surface.id)"' in canvas_html
|
||||
|
||||
assert 'rightCanvasStore.recordSurfaceMode?.(metadata.surfaceId, "modal")' in modals_js
|
||||
assert "configureModalSurfaceSwitcher" in modals_js
|
||||
assert "modal-surface-switcher" in modals_js
|
||||
assert "modal-surface-button" in modals_js
|
||||
assert "rightCanvasStore.panelSurfaces" in modals_js
|
||||
assert 'rightCanvasStore.recordSurfaceMode?.(surface.id, "modal")' in modals_js
|
||||
assert "await closeModal(modal.path)" in modals_js
|
||||
assert "modalRequiresExplicitClose" in modals_js
|
||||
assert '"plugins/_browser/webui/main.html"' in modals_js
|
||||
assert '"plugins/_office/webui/main.html"' in modals_js
|
||||
assert "&& !modalRequiresExplicitClose(newModal)" in modals_js
|
||||
assert "if (modalRequiresExplicitClose(modalStack[modalStack.length - 1])) return;" in modals_js
|
||||
assert ".modal-surface-switcher" in modals_css
|
||||
assert ".modal-surface-button.is-active" in modals_css
|
||||
assert ".modal-surface-image" in modals_css
|
||||
assert "grid-auto-flow: column" in modals_css
|
||||
|
||||
|
||||
def test_browser_tool_does_not_auto_open_canvas_policy_is_documented():
|
||||
|
|
|
|||
|
|
@ -416,7 +416,12 @@ def test_right_canvas_requires_explicit_open_and_is_absent_on_mobile():
|
|||
assert "right-canvas-resize-end" in canvas_store
|
||||
assert "dispatchResizeEvent" in canvas_store
|
||||
assert "this.isOpen = false;" in canvas_store
|
||||
assert "wasMobileMode && this.width <= MIN_WIDTH" in canvas_store
|
||||
assert "wasMobileMode && this.width < MIN_WIDTH" in canvas_store
|
||||
assert "const MIN_WIDTH = 0" in canvas_store
|
||||
assert "const MAX_WIDTH" not in canvas_store
|
||||
assert "0.58" not in canvas_store
|
||||
assert "min(900px, 58vw)" not in canvas_css
|
||||
assert "max-width: none" in canvas_css
|
||||
assert "if (this.isMobileMode && !surface.actionOnly)" in canvas_store
|
||||
assert "if (this.isMobileMode)" in canvas_store
|
||||
assert "shouldRender()" in canvas_store
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ import { callJsExtensions } from "/js/extensions.js";
|
|||
|
||||
const STORAGE_KEY = "a0.rightCanvas";
|
||||
const DEFAULT_WIDTH = 720;
|
||||
const MIN_WIDTH = 420;
|
||||
const MAX_WIDTH = 900;
|
||||
const MIN_WIDTH = 0;
|
||||
const DESKTOP_BREAKPOINT = 1200;
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
const SURFACE_MODE_CANVAS = "canvas";
|
||||
|
|
@ -18,6 +17,12 @@ function viewportWidth() {
|
|||
return Math.max(document.documentElement.clientWidth || 0, globalThis.innerWidth || 0);
|
||||
}
|
||||
|
||||
function normalizeWidth(value, fallback = DEFAULT_WIDTH) {
|
||||
if (value === null || value === undefined || value === "") return fallback;
|
||||
const width = Number(value);
|
||||
return Number.isFinite(width) ? Math.max(MIN_WIDTH, Math.round(width)) : fallback;
|
||||
}
|
||||
|
||||
function normalizeSurfaceMode(mode = "") {
|
||||
return mode === SURFACE_MODE_MODAL ? SURFACE_MODE_MODAL : SURFACE_MODE_CANVAS;
|
||||
}
|
||||
|
|
@ -306,15 +311,22 @@ const model = {
|
|||
|
||||
setWidth(px, options = {}) {
|
||||
const { persist = true } = options;
|
||||
const max = this.maxWidth();
|
||||
const next = clamp(Number(px) || DEFAULT_WIDTH, MIN_WIDTH, max);
|
||||
const next = clamp(normalizeWidth(px), MIN_WIDTH, this.maxWidth());
|
||||
this.width = next;
|
||||
this.applyLayoutState();
|
||||
if (persist) this.persist();
|
||||
},
|
||||
|
||||
maxWidth() {
|
||||
return Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, Math.floor(viewportWidth() * 0.58)));
|
||||
if (this.isOverlayMode) {
|
||||
return Math.max(MIN_WIDTH, viewportWidth() - 44);
|
||||
}
|
||||
|
||||
const container = this._rootElement?.closest(".container");
|
||||
const rightPanel = document.getElementById("right-panel");
|
||||
const containerRight = container?.getBoundingClientRect().right ?? viewportWidth();
|
||||
const panelLeft = rightPanel?.getBoundingClientRect().left ?? 0;
|
||||
return Math.max(MIN_WIDTH, Math.floor(containerRight - panelLeft));
|
||||
},
|
||||
|
||||
defaultWidth() {
|
||||
|
|
@ -387,7 +399,7 @@ const model = {
|
|||
normalizeSurfaceMode(mode),
|
||||
]),
|
||||
);
|
||||
if (saved.width) this.width = Number(saved.width);
|
||||
if (Number.isFinite(Number(saved.width))) this.width = Number(saved.width);
|
||||
} catch (error) {
|
||||
console.warn("Could not restore right canvas state", error);
|
||||
}
|
||||
|
|
@ -409,7 +421,7 @@ const model = {
|
|||
surface.close?.({ ...payload, reason: "mobile" });
|
||||
}, 0);
|
||||
}
|
||||
} else if (wasMobileMode && this.width <= MIN_WIDTH) {
|
||||
} else if (wasMobileMode && this.width < MIN_WIDTH) {
|
||||
this.width = this.defaultWidth();
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ body.right-canvas-resizing {
|
|||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
height: 100%;
|
||||
min-width: 52px;
|
||||
max-width: min(900px, 58vw);
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
overflow: hidden;
|
||||
border-left: 1px solid var(--right-canvas-border);
|
||||
background: var(--right-canvas-chrome);
|
||||
|
|
|
|||
|
|
@ -91,7 +91,9 @@ the old and the new system. */
|
|||
/* Modal Header */
|
||||
.modal-header {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: auto;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
|
|
@ -150,7 +152,14 @@ the old and the new system. */
|
|||
background: color-mix(in srgb, var(--color-background-hover) 72%, transparent);
|
||||
}
|
||||
|
||||
.modal-dock-button {
|
||||
.modal-surface-switcher {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.modal-dock-button,
|
||||
.modal-surface-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -168,16 +177,27 @@ the old and the new system. */
|
|||
transition: background-color 0.16s ease, border-color 0.16s ease, opacity 0.16s ease;
|
||||
}
|
||||
|
||||
.modal-dock-button:hover {
|
||||
.modal-dock-button:hover,
|
||||
.modal-surface-button:hover,
|
||||
.modal-surface-button.is-active {
|
||||
opacity: 1;
|
||||
border-color: color-mix(in srgb, var(--color-primary) 28%, var(--color-border));
|
||||
background: color-mix(in srgb, var(--color-background-hover) 72%, transparent);
|
||||
}
|
||||
|
||||
.modal-dock-button .material-symbols-outlined {
|
||||
.modal-dock-button .material-symbols-outlined,
|
||||
.modal-surface-button .material-symbols-outlined {
|
||||
font-size: 19px;
|
||||
}
|
||||
|
||||
.modal-surface-image {
|
||||
display: block;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 6px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* Modal Description */
|
||||
.modal-description {
|
||||
padding: 0.8rem 1rem 0 1rem;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ function normalizeModalPath(modalPath = "") {
|
|||
return String(modalPath || "").replace(/^\/+/, "");
|
||||
}
|
||||
|
||||
function sameModalPath(left = "", right = "") {
|
||||
return normalizeModalPath(left) === normalizeModalPath(right);
|
||||
}
|
||||
|
||||
function modalRequiresExplicitClose(modalOrElement) {
|
||||
const element = modalOrElement?.element || modalOrElement;
|
||||
const path = normalizeModalPath(modalOrElement?.path || element?.path || "");
|
||||
|
|
@ -23,7 +27,7 @@ function modalRequiresExplicitClose(modalOrElement) {
|
|||
}
|
||||
|
||||
function findModalIndexByPath(modalPath) {
|
||||
return modalStack.findIndex((modal) => modal.path === modalPath);
|
||||
return modalStack.findIndex((modal) => sameModalPath(modal.path, modalPath));
|
||||
}
|
||||
|
||||
function focusModal(modalPath) {
|
||||
|
|
@ -201,8 +205,102 @@ function getDockMetadata(doc, modalPath) {
|
|||
};
|
||||
}
|
||||
|
||||
function configureModalDockButton(modal, doc) {
|
||||
function getModalSwitchSurfaces(metadata) {
|
||||
if (!metadata) return [];
|
||||
const surfaces = Array.isArray(rightCanvasStore.panelSurfaces)
|
||||
? rightCanvasStore.panelSurfaces
|
||||
: [];
|
||||
const modalSurfaces = surfaces.filter((surface) => (
|
||||
surface?.id
|
||||
&& surface?.modalPath
|
||||
&& !surface.actionOnly
|
||||
));
|
||||
|
||||
if (modalSurfaces.some((surface) => surface.id === metadata.surfaceId)) {
|
||||
return modalSurfaces;
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
id: metadata.surfaceId,
|
||||
title: metadata.title,
|
||||
icon: metadata.icon,
|
||||
modalPath: metadata.modalPath,
|
||||
},
|
||||
...modalSurfaces,
|
||||
];
|
||||
}
|
||||
|
||||
function createModalSurfaceButton(surface, metadata, modal) {
|
||||
const title = surface.title || surface.id;
|
||||
const targetModalPath = surface.modalPath || "";
|
||||
const isActive = surface.id === metadata.surfaceId || sameModalPath(targetModalPath, modal.path);
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "modal-surface-button";
|
||||
button.dataset.canvasSurface = surface.id;
|
||||
button.title = title;
|
||||
button.setAttribute("aria-label", title);
|
||||
button.setAttribute("aria-pressed", isActive.toString());
|
||||
if (isActive) button.classList.add("is-active");
|
||||
|
||||
if (surface.image) {
|
||||
const image = document.createElement("img");
|
||||
image.className = "modal-surface-image";
|
||||
image.src = surface.image;
|
||||
image.alt = "";
|
||||
image.setAttribute("aria-hidden", "true");
|
||||
button.appendChild(image);
|
||||
} else {
|
||||
const icon = document.createElement("span");
|
||||
icon.className = "material-symbols-outlined";
|
||||
icon.setAttribute("aria-hidden", "true");
|
||||
icon.textContent = surface.icon || "web_asset";
|
||||
button.appendChild(icon);
|
||||
}
|
||||
|
||||
button.addEventListener("click", async () => {
|
||||
if (button.disabled || isActive || !targetModalPath) return;
|
||||
button.disabled = true;
|
||||
try {
|
||||
rightCanvasStore.recordSurfaceMode?.(surface.id, "modal");
|
||||
const openPromise = ensureModalOpen(targetModalPath);
|
||||
if (openPromise?.catch) {
|
||||
openPromise.catch((error) => console.error(`Modal surface ${surface.id} failed to open`, error));
|
||||
}
|
||||
await closeModal(modal.path);
|
||||
} finally {
|
||||
if (document.contains(button)) button.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
function configureModalSurfaceSwitcher(modal, doc) {
|
||||
const metadata = getDockMetadata(doc, modal.path);
|
||||
if (!metadata || !modal.header || modal.header.querySelector(".modal-surface-switcher")) {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
const surfaces = getModalSwitchSurfaces(metadata);
|
||||
if (surfaces.length <= 1) return metadata;
|
||||
|
||||
const switcher = document.createElement("div");
|
||||
switcher.className = "modal-surface-switcher";
|
||||
switcher.setAttribute("role", "group");
|
||||
switcher.setAttribute("aria-label", "Modal surfaces");
|
||||
|
||||
for (const surface of surfaces) {
|
||||
switcher.appendChild(createModalSurfaceButton(surface, metadata, modal));
|
||||
}
|
||||
|
||||
modal.close?.insertAdjacentElement("beforebegin", switcher);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
function configureModalDockButton(modal, doc) {
|
||||
const metadata = configureModalSurfaceSwitcher(modal, doc);
|
||||
if (!metadata || !modal.header || modal.header.querySelector(".modal-dock-button")) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue