mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-07-09 17:08:29 +00:00
Route mobile canvas rail to modals
Keep the right-canvas rail visible at mobile widths while routing non-action surface affordances into floating modals instead of the side canvas shell. Hide mobile surface-modal header controls except the close button and Editor New action, and clamp floating surface modal geometry to narrow viewports.
This commit is contained in:
parent
82bb0b9292
commit
a27ac51136
6 changed files with 97 additions and 15 deletions
|
|
@ -810,6 +810,9 @@ def test_surface_buttons_keep_modal_and_canvas_entry_points_separate():
|
|||
canvas_html = (PROJECT_ROOT / "webui" / "components" / "canvas" / "right-canvas.html").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
canvas_css = (PROJECT_ROOT / "webui" / "components" / "canvas" / "right-canvas.css").read_text(
|
||||
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")
|
||||
surfaces_js = (PROJECT_ROOT / "webui" / "js" / "surfaces.js").read_text(encoding="utf-8")
|
||||
|
|
@ -826,6 +829,10 @@ def test_surface_buttons_keep_modal_and_canvas_entry_points_separate():
|
|||
canvas_store.index("async openModalSurface"):
|
||||
canvas_store.index("async undockActiveSurface")
|
||||
]
|
||||
should_render_block = canvas_store[
|
||||
canvas_store.index("shouldRender()"):
|
||||
canvas_store.index("};\n\nexport const store")
|
||||
]
|
||||
surface_button_block = surfaces_js[
|
||||
surfaces_js.index("function createModalSurfaceButton"):
|
||||
surfaces_js.index("function configureModalSurfaceSwitcher")
|
||||
|
|
@ -845,6 +852,12 @@ def test_surface_buttons_keep_modal_and_canvas_entry_points_separate():
|
|||
assert "surfaceModes: this.surfaceModes" in canvas_store
|
||||
assert "normalizeSurfaceMode(mode)" in canvas_store
|
||||
assert "migratePersistedSurfaceState" in canvas_store
|
||||
assert "if (this.isMobileMode && !surface.actionOnly)" not in canvas_store
|
||||
assert "return await this.openModalSurface(targetId, payload);" in canvas_store
|
||||
assert "return await this.open(targetId, payload);" in canvas_store
|
||||
assert 'return await this.open(this.activeSurfaceId || this.panelSurfaces[0]?.id || "", { source: "mobile-toggle" });' in canvas_store
|
||||
assert "return true;" in should_render_block
|
||||
assert "isMobileMode" not in should_render_block
|
||||
assert "this.mountedSurfaces = {}" not in close_block
|
||||
assert "surface?.close" not in close_block
|
||||
assert "this.mountedSurfaces = {}" not in undock_block
|
||||
|
|
@ -854,6 +867,10 @@ def test_surface_buttons_keep_modal_and_canvas_entry_points_separate():
|
|||
|
||||
assert '@click="$store.rightCanvas.openLatest(surface.id)"' not in canvas_html
|
||||
assert canvas_html.count('@click="$store.rightCanvas.open(surface.id)"') == 2
|
||||
assert "body.right-canvas-mobile-mode .right-canvas-rail" in canvas_css
|
||||
assert "display: flex !important;" in canvas_css
|
||||
assert "body.right-canvas-mobile-mode .right-canvas-shell" in canvas_css
|
||||
assert "body.right-canvas-mobile-mode .right-canvas,\nbody.right-canvas-mobile-mode .right-canvas-rail" not in canvas_css
|
||||
|
||||
assert "recordMode(metadata.surfaceId, SURFACE_MODE_FLOATING)" in surfaces_js
|
||||
assert "configureModalSurfaceSwitcher" in surfaces_js
|
||||
|
|
@ -886,6 +903,13 @@ def test_surface_buttons_keep_modal_and_canvas_entry_points_separate():
|
|||
assert ".modal-surface-button.is-active" in surfaces_css
|
||||
assert ".modal-surface-image" in surfaces_css
|
||||
assert ".modal.modal-surface-parked" in surfaces_css
|
||||
assert "@media (max-width: 768px)" in surfaces_css
|
||||
assert ".modal-inner.surface-modal .surface-modal-action-group-surfaces" in surfaces_css
|
||||
assert ".modal-inner.surface-modal .surface-modal-action-group-window" in surfaces_css
|
||||
assert ".modal-inner.surface-modal .surface-modal-action-group-new" in surfaces_css
|
||||
assert ".modal-inner.editor-modal .surface-modal-action-group-new" in surfaces_css
|
||||
assert ".surface-modal-new-action:not(.editor-header-actions)" in surfaces_css
|
||||
assert "const safeMinWidth = Math.min(minWidth, maxWidth)" in surfaces_js
|
||||
assert "grid-auto-flow: column" in surfaces_css
|
||||
assert 'id: "browser"' in surfaces_js
|
||||
assert 'id: "desktop"' in surfaces_js
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
- Keep registered surfaces compatible with WebUI extension hooks.
|
||||
- Preserve responsive layout and avoid overlapping the chat/sidebar shells.
|
||||
- Right-canvas rail and tab buttons are explicit canvas entry points; they must open surfaces in the canvas even when the same surface was last used as a floating modal.
|
||||
- Right-canvas rail and tab buttons are explicit canvas entry points above the mobile breakpoint; at mobile widths, keep the rail visible but route non-action surfaces into floating modals instead of the side canvas shell.
|
||||
|
||||
## Work Guidance
|
||||
|
||||
|
|
|
|||
|
|
@ -122,9 +122,6 @@ const model = {
|
|||
if (!surface) {
|
||||
return false;
|
||||
}
|
||||
if (this.isMobileMode && !surface.actionOnly) {
|
||||
return false;
|
||||
}
|
||||
if (typeof surface.canOpen === "function" && surface.canOpen(payload) === false) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -138,6 +135,12 @@ const model = {
|
|||
return true;
|
||||
}
|
||||
|
||||
if (this.isMobileMode) {
|
||||
this.activeSurfaceId = targetId;
|
||||
this.isOpen = false;
|
||||
return await this.openModalSurface(targetId, payload);
|
||||
}
|
||||
|
||||
this.activeSurfaceId = targetId;
|
||||
this.markSurfaceMounted(targetId);
|
||||
this.isOpen = true;
|
||||
|
|
@ -208,6 +211,9 @@ const model = {
|
|||
async openLatest(surfaceId = "", payload = {}) {
|
||||
const targetId = normalizeSurfaceId(surfaceId || this.activeSurfaceId || this.panelSurfaces[0]?.id || "");
|
||||
if (!targetId) return false;
|
||||
if (this.isMobileMode) {
|
||||
return await this.open(targetId, payload);
|
||||
}
|
||||
if (this.latestSurfaceMode(targetId) === SURFACE_MODE_FLOATING) {
|
||||
return await this.openModalSurface(targetId, payload);
|
||||
}
|
||||
|
|
@ -337,7 +343,7 @@ const model = {
|
|||
|
||||
async toggleCanvas() {
|
||||
if (this.isMobileMode) {
|
||||
return false;
|
||||
return await this.open(this.activeSurfaceId || this.panelSurfaces[0]?.id || "", { source: "mobile-toggle" });
|
||||
}
|
||||
if (this.isOpen) {
|
||||
await this.close();
|
||||
|
|
@ -512,7 +518,7 @@ const model = {
|
|||
},
|
||||
|
||||
shouldRender() {
|
||||
return !this.isMobileMode;
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -318,8 +318,39 @@ body.right-canvas-overlay-mode .right-canvas-resize-handle {
|
|||
display: none;
|
||||
}
|
||||
|
||||
body.right-canvas-mobile-mode .right-canvas,
|
||||
body.right-canvas-mobile-mode .right-canvas {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 3400;
|
||||
display: flex !important;
|
||||
width: 0 !important;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
transform: none;
|
||||
border-left: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
body.right-canvas-mobile-mode .right-canvas.is-open {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
body.right-canvas-mobile-mode .right-canvas-rail {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
right: max(0px, env(safe-area-inset-right));
|
||||
display: flex !important;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
body.right-canvas-mobile-mode .right-canvas-shell,
|
||||
body.right-canvas-mobile-mode .right-canvas-resize-handle {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -176,6 +176,23 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.modal-inner.surface-modal .surface-modal-action-group-surfaces,
|
||||
.modal-inner.surface-modal .surface-modal-action-group-window,
|
||||
.modal-inner.surface-modal .surface-modal-action-separator,
|
||||
.modal-inner.surface-modal .surface-modal-action-group-new {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.modal-inner.editor-modal .surface-modal-action-group-new {
|
||||
display: inline-flex !important;
|
||||
}
|
||||
|
||||
.modal-inner.editor-modal .surface-modal-action-group-new .surface-modal-new-action:not(.editor-header-actions) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.surface-image,
|
||||
.modal-surface-image {
|
||||
display: block;
|
||||
|
|
|
|||
|
|
@ -416,6 +416,8 @@ export function setupFloatingSurfaceModalChrome(options = {}) {
|
|||
|
||||
const viewportWidth = () => Math.max(document.documentElement.clientWidth || 0, globalThis.innerWidth || 0);
|
||||
const viewportHeight = () => Math.max(document.documentElement.clientHeight || 0, globalThis.innerHeight || 0);
|
||||
const availableWidth = () => Math.max(1, viewportWidth() - viewportGap * 2);
|
||||
const availableHeight = () => Math.max(1, viewportHeight() - viewportGap * 2);
|
||||
const currentBounds = () => {
|
||||
const bounds = inner.getBoundingClientRect();
|
||||
return {
|
||||
|
|
@ -426,10 +428,12 @@ export function setupFloatingSurfaceModalChrome(options = {}) {
|
|||
};
|
||||
};
|
||||
const normalizedBounds = (bounds = {}) => {
|
||||
const maxWidth = Math.max(minWidth, viewportWidth() - viewportGap * 2);
|
||||
const maxHeight = Math.max(minHeight, viewportHeight() - viewportGap * 2);
|
||||
const width = Math.min(Math.max(minWidth, Number(bounds.width || minWidth)), maxWidth);
|
||||
const height = Math.min(Math.max(minHeight, Number(bounds.height || minHeight)), maxHeight);
|
||||
const maxWidth = availableWidth();
|
||||
const maxHeight = availableHeight();
|
||||
const safeMinWidth = Math.min(minWidth, maxWidth);
|
||||
const safeMinHeight = Math.min(minHeight, maxHeight);
|
||||
const width = Math.min(Math.max(safeMinWidth, Number(bounds.width || safeMinWidth)), maxWidth);
|
||||
const height = Math.min(Math.max(safeMinHeight, Number(bounds.height || safeMinHeight)), maxHeight);
|
||||
return {
|
||||
left: Math.min(
|
||||
Math.max(viewportGap, Number(bounds.left || viewportGap)),
|
||||
|
|
@ -461,16 +465,16 @@ export function setupFloatingSurfaceModalChrome(options = {}) {
|
|||
inner.style.top = `${Math.round(next.top)}px`;
|
||||
inner.style.width = `${Math.round(next.width)}px`;
|
||||
inner.style.height = `${Math.round(next.height)}px`;
|
||||
inner.style.maxWidth = `${Math.max(minWidth, viewportWidth() - viewportGap * 2)}px`;
|
||||
inner.style.maxHeight = `${Math.max(minHeight, viewportHeight() - viewportGap * 2)}px`;
|
||||
inner.style.maxWidth = `${availableWidth()}px`;
|
||||
inner.style.maxHeight = `${availableHeight()}px`;
|
||||
notifyBoundsChange();
|
||||
return next;
|
||||
};
|
||||
const focusBounds = () => ({
|
||||
left: viewportGap,
|
||||
top: viewportGap,
|
||||
width: viewportWidth() - viewportGap * 2,
|
||||
height: viewportHeight() - viewportGap * 2,
|
||||
width: availableWidth(),
|
||||
height: availableHeight(),
|
||||
});
|
||||
const clampGeometry = () => {
|
||||
if (inner.classList.contains("is-focus-mode")) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue