mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-05-22 19:47:15 +00:00
Unify surface modal header actions
Add a shared grouped action rail for Browser, Desktop, and Editor floating surface modals so modality switches, canvas docking, focus mode, New, and close controls appear in a consistent order with separators. Fix the Desktop focus button class collision that hid the canvas docking button, and align dock button labels with canvas terminology.
This commit is contained in:
parent
8601f0d10c
commit
b64c43e736
10 changed files with 234 additions and 48 deletions
|
|
@ -7,7 +7,11 @@ import { store as chatInputStore } from "/components/chat/input/input-store.js";
|
|||
import { store as pluginSettingsStore } from "/components/plugins/plugin-settings-store.js";
|
||||
import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
|
||||
import { store as rightCanvasStore } from "/components/canvas/right-canvas-store.js";
|
||||
import { openLatest as openLatestSurface, registerUrlHandler } from "/js/surfaces.js";
|
||||
import {
|
||||
openLatest as openLatestSurface,
|
||||
placeSurfaceModalHeaderAction,
|
||||
registerUrlHandler,
|
||||
} from "/js/surfaces.js";
|
||||
|
||||
const websocket = getNamespacedClient("/ws");
|
||||
websocket.addHandlers(["ws_webui"]);
|
||||
|
|
@ -2634,6 +2638,27 @@ const model = {
|
|||
};
|
||||
clampGeometry();
|
||||
|
||||
const newAction = globalThis.document.createElement("div");
|
||||
newAction.className = "browser-header-actions surface-modal-new-action";
|
||||
newAction.innerHTML = `
|
||||
<button type="button" class="browser-header-new-button surface-modal-new-button" title="New Browser" aria-label="New Browser">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">add</span>
|
||||
<span>New</span>
|
||||
</button>
|
||||
`;
|
||||
const newButton = newAction.querySelector(".browser-header-new-button");
|
||||
const onNewClick = async () => {
|
||||
if (!newButton || newButton.disabled || this.isBusy()) return;
|
||||
newButton.disabled = true;
|
||||
try {
|
||||
await this.openNewBrowser();
|
||||
} finally {
|
||||
if (globalThis.document?.contains?.(newButton)) newButton.disabled = false;
|
||||
}
|
||||
};
|
||||
newButton?.addEventListener("click", onNewClick);
|
||||
placeSurfaceModalHeaderAction(header, newAction, "new");
|
||||
|
||||
const focusButton = globalThis.document.createElement("button");
|
||||
focusButton.type = "button";
|
||||
focusButton.className = "surface-button browser-modal-focus-button";
|
||||
|
|
@ -2658,12 +2683,7 @@ const model = {
|
|||
updateFocusButton(false);
|
||||
};
|
||||
updateFocusButton(false);
|
||||
const closeButton = inner.querySelector(".modal-close");
|
||||
if (closeButton) {
|
||||
closeButton.insertAdjacentElement("beforebegin", focusButton);
|
||||
} else {
|
||||
header.appendChild(focusButton);
|
||||
}
|
||||
placeSurfaceModalHeaderAction(header, focusButton, "window");
|
||||
const onFocusClick = () => setFocusMode(!inner.classList.contains("is-focus-mode"));
|
||||
focusButton.addEventListener("click", onFocusClick);
|
||||
|
||||
|
|
@ -2723,6 +2743,8 @@ const model = {
|
|||
header.addEventListener("pointerdown", onPointerDown);
|
||||
|
||||
this._floatingCleanup = () => {
|
||||
newButton?.removeEventListener("click", onNewClick);
|
||||
newAction.remove();
|
||||
focusButton.removeEventListener("click", onFocusClick);
|
||||
focusButton.remove();
|
||||
header.removeEventListener("pointerdown", onPointerDown);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
class="browser-modal"
|
||||
data-surface-id="browser"
|
||||
data-surface-modal-path="/plugins/_browser/webui/main.html"
|
||||
data-surface-dock-title="Open Browser in surface"
|
||||
data-surface-dock-title="Open Browser in canvas"
|
||||
data-surface-dock-icon="dock_to_right"
|
||||
data-canvas-surface="browser"
|
||||
data-canvas-modal-path="/plugins/_browser/webui/main.html"
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@
|
|||
}
|
||||
|
||||
.modal-inner.office-modal .modal-header {
|
||||
grid-template-columns: minmax(0, 1fr) repeat(5, auto);
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
}
|
||||
|
||||
.office-modal-input-shield {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { createStore } from "/js/AlpineStore.js";
|
|||
import { callJsonApi } from "/js/api.js";
|
||||
import { getNamespacedClient } from "/js/websocket.js";
|
||||
import { store as fileBrowserStore } from "/components/modals/file-browser/file-browser-store.js";
|
||||
import { handleUrlIntent } from "/js/surfaces.js";
|
||||
import { handleUrlIntent, placeSurfaceModalHeaderAction } from "/js/surfaces.js";
|
||||
|
||||
const officeSocket = getNamespacedClient("/ws");
|
||||
officeSocket.addHandlers(["ws_webui"]);
|
||||
|
|
@ -2337,9 +2337,9 @@ const model = {
|
|||
if (!header || header.querySelector(".office-header-actions")) return () => {};
|
||||
|
||||
const root = globalThis.document.createElement("div");
|
||||
root.className = "office-header-actions";
|
||||
root.className = "office-header-actions surface-modal-new-action";
|
||||
root.innerHTML = `
|
||||
<button type="button" class="office-header-new-button" aria-haspopup="menu" aria-expanded="false">
|
||||
<button type="button" class="office-header-new-button surface-modal-new-button" aria-haspopup="menu" aria-expanded="false">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">add</span>
|
||||
<span>New</span>
|
||||
<span class="material-symbols-outlined office-new-chevron" aria-hidden="true">expand_more</span>
|
||||
|
|
@ -2396,14 +2396,7 @@ const model = {
|
|||
globalThis.document.addEventListener("click", onDocumentClick);
|
||||
globalThis.document.addEventListener("keydown", onDocumentKeydown);
|
||||
|
||||
const firstHeaderAction = header.querySelector(
|
||||
".modal-surface-switcher, .modal-dock-button, .office-modal-focus-button, .modal-close",
|
||||
);
|
||||
if (firstHeaderAction) {
|
||||
firstHeaderAction.insertAdjacentElement("beforebegin", root);
|
||||
} else {
|
||||
header.appendChild(root);
|
||||
}
|
||||
placeSurfaceModalHeaderAction(header, root, "new");
|
||||
|
||||
setOpen(false);
|
||||
return () => {
|
||||
|
|
@ -2501,20 +2494,16 @@ const model = {
|
|||
|
||||
const focusButton = globalThis.document.createElement("button");
|
||||
focusButton.type = "button";
|
||||
focusButton.className = "modal-dock-button office-modal-focus-button";
|
||||
focusButton.className = "surface-button office-modal-focus-button";
|
||||
focusButton.innerHTML = '<span class="material-symbols-outlined" aria-hidden="true">fullscreen</span>';
|
||||
const updateFocusButton = (active) => {
|
||||
const label = active ? "Restore size" : "Focus mode";
|
||||
focusButton.setAttribute("aria-label", label);
|
||||
focusButton.setAttribute("title", label);
|
||||
focusButton.querySelector(".material-symbols-outlined").textContent = active ? "fullscreen_exit" : "fullscreen";
|
||||
};
|
||||
updateFocusButton(false);
|
||||
const closeButton = inner.querySelector(".modal-close");
|
||||
if (closeButton) {
|
||||
closeButton.insertAdjacentElement("beforebegin", focusButton);
|
||||
} else {
|
||||
header.appendChild(focusButton);
|
||||
}
|
||||
placeSurfaceModalHeaderAction(header, focusButton, "window");
|
||||
cleanup.push(() => focusButton.remove());
|
||||
|
||||
const setFocusMode = (enabled) => {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
class="surface-modal office-modal modal-no-backdrop"
|
||||
data-surface-id="desktop"
|
||||
data-surface-modal-path="/plugins/_desktop/webui/main.html"
|
||||
data-surface-dock-title="Open Desktop in surface"
|
||||
data-surface-dock-title="Open Desktop in canvas"
|
||||
data-surface-dock-icon="dock_to_right"
|
||||
data-canvas-surface="desktop"
|
||||
data-canvas-modal-path="/plugins/_desktop/webui/main.html"
|
||||
|
|
|
|||
|
|
@ -322,7 +322,7 @@
|
|||
}
|
||||
|
||||
.modal-inner.editor-modal .modal-header {
|
||||
grid-template-columns: minmax(0, 1fr) repeat(4, auto);
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
}
|
||||
|
||||
.editor-tabs {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { createStore } from "/js/AlpineStore.js";
|
|||
import { callJsonApi } from "/js/api.js";
|
||||
import { getNamespacedClient } from "/js/websocket.js";
|
||||
import { store as fileBrowserStore } from "/components/modals/file-browser/file-browser-store.js";
|
||||
import { placeSurfaceModalHeaderAction } from "/js/surfaces.js";
|
||||
import {
|
||||
buildMarkdownPages,
|
||||
isExternalHref,
|
||||
|
|
@ -1591,9 +1592,9 @@ const model = {
|
|||
if (!header || header.querySelector(".editor-header-actions")) return () => {};
|
||||
|
||||
const root = document.createElement("div");
|
||||
root.className = "editor-header-actions";
|
||||
root.className = "editor-header-actions surface-modal-new-action";
|
||||
root.innerHTML = `
|
||||
<button type="button" class="editor-header-new-button" aria-haspopup="menu" aria-expanded="false">
|
||||
<button type="button" class="editor-header-new-button surface-modal-new-button" aria-haspopup="menu" aria-expanded="false">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">add</span>
|
||||
<span>New</span>
|
||||
<span class="material-symbols-outlined editor-new-chevron" aria-hidden="true">expand_more</span>
|
||||
|
|
@ -1642,12 +1643,7 @@ const model = {
|
|||
document.addEventListener("click", onMarkdownClick);
|
||||
document.addEventListener("keydown", onMarkdownKeydown);
|
||||
|
||||
const firstHeaderAction = header.querySelector(".modal-close");
|
||||
if (firstHeaderAction) {
|
||||
firstHeaderAction.insertAdjacentElement("beforebegin", root);
|
||||
} else {
|
||||
header.appendChild(root);
|
||||
}
|
||||
placeSurfaceModalHeaderAction(header, root, "new");
|
||||
|
||||
setOpen(false);
|
||||
return () => {
|
||||
|
|
@ -1666,10 +1662,9 @@ const model = {
|
|||
inner.dataset.editorModalReady = "1";
|
||||
inner.classList.add("editor-modal");
|
||||
const cleanup = [];
|
||||
const closeButton = inner.querySelector(".modal-close");
|
||||
const focusButton = document.createElement("button");
|
||||
focusButton.type = "button";
|
||||
focusButton.className = "modal-dock-button editor-modal-focus-button";
|
||||
focusButton.className = "surface-button editor-modal-focus-button";
|
||||
focusButton.innerHTML = '<span class="material-symbols-outlined" aria-hidden="true">fullscreen</span>';
|
||||
const updateFocusButton = (active) => {
|
||||
const label = active ? "Restore size" : "Focus mode";
|
||||
|
|
@ -1684,11 +1679,7 @@ const model = {
|
|||
updateFocusButton(active);
|
||||
};
|
||||
focusButton.addEventListener("click", onFocusClick);
|
||||
if (closeButton) {
|
||||
closeButton.insertAdjacentElement("beforebegin", focusButton);
|
||||
} else {
|
||||
header.appendChild(focusButton);
|
||||
}
|
||||
placeSurfaceModalHeaderAction(header, focusButton, "window");
|
||||
cleanup.push(() => focusButton.removeEventListener("click", onFocusClick));
|
||||
cleanup.push(() => focusButton.remove());
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
class="surface-modal editor-modal modal-no-backdrop"
|
||||
data-surface-id="editor"
|
||||
data-surface-modal-path="/plugins/_editor/webui/main.html"
|
||||
data-surface-dock-title="Open Editor in surface"
|
||||
data-surface-dock-title="Open Editor in canvas"
|
||||
data-surface-dock-icon="dock_to_right"
|
||||
data-canvas-surface="editor"
|
||||
data-canvas-modal-path="/plugins/_editor/webui/main.html"
|
||||
|
|
|
|||
|
|
@ -29,6 +29,42 @@
|
|||
gap: 5px;
|
||||
}
|
||||
|
||||
.surface-modal-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.surface-modal-action-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.surface-modal-action-group[hidden],
|
||||
.surface-modal-action-separator[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.surface-modal-action-separator {
|
||||
display: block;
|
||||
flex: 0 0 1px;
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
margin: 0 5px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--color-border) 72%, transparent);
|
||||
}
|
||||
|
||||
.surface-modal-new-action {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.surface-dock-button,
|
||||
.surface-button,
|
||||
.modal-dock-button,
|
||||
|
|
@ -68,6 +104,62 @@
|
|||
font-size: 19px;
|
||||
}
|
||||
|
||||
.surface-modal-new-button {
|
||||
appearance: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
height: 34px;
|
||||
min-height: 34px;
|
||||
padding: 0 9px 0 8px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 7px;
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0;
|
||||
line-height: 1;
|
||||
opacity: 0.82;
|
||||
white-space: nowrap;
|
||||
transition: background-color 0.16s ease, border-color 0.16s ease, opacity 0.16s ease;
|
||||
}
|
||||
|
||||
.surface-modal-new-button:hover:not(:disabled),
|
||||
.surface-modal-new-action.is-open .surface-modal-new-button {
|
||||
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);
|
||||
}
|
||||
|
||||
.surface-modal-new-button:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.surface-modal-new-button .material-symbols-outlined {
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.surface-modal-actions,
|
||||
.surface-modal-action-group {
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.surface-modal-action-separator {
|
||||
margin: 0 3px;
|
||||
}
|
||||
|
||||
.surface-modal-new-button span:not(.material-symbols-outlined) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.surface-image,
|
||||
.modal-surface-image {
|
||||
display: block;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ const LEGACY_SURFACE_IDS = new Map([
|
|||
|
||||
const registeredSurfaces = new Map();
|
||||
const urlHandlers = new Set();
|
||||
const SURFACE_MODAL_ACTION_GROUPS = ["surfaces", "window", "new"];
|
||||
|
||||
export const CORE_SURFACES = [
|
||||
{
|
||||
|
|
@ -277,6 +278,97 @@ function getModalSwitchSurfaces(metadata) {
|
|||
.sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
|
||||
}
|
||||
|
||||
function directChildByClass(parent, className) {
|
||||
return Array.from(parent?.children || []).find((child) => child.classList?.contains(className)) || null;
|
||||
}
|
||||
|
||||
function ensureSurfaceModalActionRail(header) {
|
||||
if (!header) return null;
|
||||
let rail = directChildByClass(header, "surface-modal-actions");
|
||||
if (!rail) {
|
||||
rail = document.createElement("div");
|
||||
rail.className = "surface-modal-actions";
|
||||
rail.setAttribute("aria-label", "Surface modal actions");
|
||||
|
||||
const closeButton = directChildByClass(header, "modal-close") || header.querySelector?.(".modal-close");
|
||||
if (closeButton) {
|
||||
closeButton.insertAdjacentElement("beforebegin", rail);
|
||||
} else {
|
||||
header.appendChild(rail);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [index, groupName] of SURFACE_MODAL_ACTION_GROUPS.entries()) {
|
||||
if (!rail.querySelector(`[data-surface-modal-action-group="${groupName}"]`)) {
|
||||
if (index > 0 && !rail.querySelector(`[data-surface-modal-separator-before="${groupName}"]`)) {
|
||||
const separator = document.createElement("span");
|
||||
separator.className = "surface-modal-action-separator";
|
||||
separator.dataset.surfaceModalSeparatorBefore = groupName;
|
||||
separator.setAttribute("aria-hidden", "true");
|
||||
rail.appendChild(separator);
|
||||
}
|
||||
|
||||
const group = document.createElement("div");
|
||||
group.className = `surface-modal-action-group surface-modal-action-group-${groupName}`;
|
||||
group.dataset.surfaceModalActionGroup = groupName;
|
||||
rail.appendChild(group);
|
||||
}
|
||||
}
|
||||
|
||||
refreshSurfaceModalActionRail(header);
|
||||
return rail;
|
||||
}
|
||||
|
||||
function surfaceModalActionGroup(header, groupName) {
|
||||
const rail = ensureSurfaceModalActionRail(header);
|
||||
return rail?.querySelector?.(`[data-surface-modal-action-group="${groupName}"]`) || null;
|
||||
}
|
||||
|
||||
export function refreshSurfaceModalActionRail(header) {
|
||||
const rail = directChildByClass(header, "surface-modal-actions");
|
||||
if (!rail) return;
|
||||
|
||||
const groups = Object.fromEntries(
|
||||
SURFACE_MODAL_ACTION_GROUPS.map((groupName) => [
|
||||
groupName,
|
||||
rail.querySelector(`[data-surface-modal-action-group="${groupName}"]`),
|
||||
]),
|
||||
);
|
||||
const hasActions = Object.fromEntries(
|
||||
Object.entries(groups).map(([groupName, group]) => [
|
||||
groupName,
|
||||
Boolean(group?.children?.length),
|
||||
]),
|
||||
);
|
||||
|
||||
for (const [groupName, group] of Object.entries(groups)) {
|
||||
if (group) group.hidden = !hasActions[groupName];
|
||||
}
|
||||
|
||||
const beforeWindow = rail.querySelector('[data-surface-modal-separator-before="window"]');
|
||||
if (beforeWindow) beforeWindow.hidden = !(hasActions.surfaces && (hasActions.window || hasActions.new));
|
||||
|
||||
const beforeNew = rail.querySelector('[data-surface-modal-separator-before="new"]');
|
||||
if (beforeNew) beforeNew.hidden = !(hasActions.window && hasActions.new);
|
||||
}
|
||||
|
||||
export function placeSurfaceModalHeaderAction(header, element, groupName = "window", options = {}) {
|
||||
if (!header || !element) return;
|
||||
const normalizedGroup = SURFACE_MODAL_ACTION_GROUPS.includes(groupName) ? groupName : "window";
|
||||
const group = surfaceModalActionGroup(header, normalizedGroup);
|
||||
if (!group) return;
|
||||
|
||||
if (options.prepend) {
|
||||
if (element.parentElement !== group || group.firstElementChild !== element) {
|
||||
group.insertBefore(element, group.firstElementChild);
|
||||
}
|
||||
} else if (element.parentElement !== group) {
|
||||
group.appendChild(element);
|
||||
}
|
||||
|
||||
refreshSurfaceModalActionRail(header);
|
||||
}
|
||||
|
||||
function markSurfaceModal(modal, metadata) {
|
||||
const element = modal?.element;
|
||||
const inner = modal?.inner || element?.querySelector?.(".modal-inner");
|
||||
|
|
@ -350,11 +442,11 @@ function configureModalSurfaceSwitcher(modal, metadata) {
|
|||
switcher.appendChild(createModalSurfaceButton(surface, metadata, modal));
|
||||
}
|
||||
|
||||
modal.close?.insertAdjacentElement("beforebegin", switcher);
|
||||
placeSurfaceModalHeaderAction(modal.header, switcher, "surfaces");
|
||||
}
|
||||
|
||||
function configureModalDockButton(modal, metadata) {
|
||||
if (!metadata || !modal?.header || modal.header.querySelector(".surface-dock-button, .modal-dock-button")) {
|
||||
if (!metadata || !modal?.header || modal.header.querySelector(".surface-dock-button")) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -384,7 +476,7 @@ function configureModalDockButton(modal, metadata) {
|
|||
}
|
||||
});
|
||||
|
||||
modal.close?.insertAdjacentElement("beforebegin", button);
|
||||
placeSurfaceModalHeaderAction(modal.header, button, "window", { prepend: true });
|
||||
}
|
||||
|
||||
async function configureSurfaceModal(event) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue