Esc and the shortcut close the browser; Show in folder; insert a picture as a node

From a Bilibili comment (2026-10-13):
- Esc closes the top layer: a dialog or menu (the plugin's alert and confirm
  now close on Esc, as OK / Cancel), else the Workflows / Combos workspace,
  else the browser. One capture listener decides before the layers act, so a
  dialog closing on the same Esc still counts as above. The shortcut and the
  Extensions entry toggle the browser.
- A model's file card has Show in folder: POST /anomalous/open_model_folder
  (api/model_folder.py) takes type, folder index, subfolder and file name,
  never a path, and opens Explorer / Finder with the file selected; a POST,
  so local_guard keeps it to this computer. The card moves out of
  ui_detail.js into model_location.js.
- A gallery card's right-click menu (gallery_card_menu.js): Insert as a Load
  Image node reads the output file in place ("name.png [output]"), and View
  Parameters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DemonGatanjieu
2026-10-13 01:09:29 +08:00
co-authored by Claude Opus 5.5
parent a23c3ec69d
commit e0129eaa2f
15 changed files with 276 additions and 61 deletions
+4 -1
View File
@@ -83,6 +83,8 @@ covered by the `styles.css` manifest.
and no scan replaces or deletes it.
- `api/model_catalog.py`, `api/model_resolution.py`, `api/model_metadata.py`, and
`api/model_media.py` own model listing, identity recovery, mutation, and covers;
`api/model_folder.py` shows a model file in Explorer / Finder (by type, folder index and
file name, never a path; a POST, so only this computer may ask);
`api/models.py` is a compatibility facade; `api/model_constants.py` holds the
model, media and sidecar extensions they share. `api/model_type_listing.py` lists a
whole models folder with its subfolders for the models page's type chips.
@@ -349,6 +351,7 @@ covered by the `styles.css` manifest.
settings control that opens ComfyUI's keybinding editor.
- `ui_model_sources.js` renders the Models page's Sources view (the last type chip): where each model is downloaded, for the open workflow or every model, with each link editable and saved in place; `model_source_links.js` owns its data and actions (collecting the workflow's models, resolving them here, saving a link to the model's information and the workflow, the canvas note and the clipboard list).
- `ui_gallery_card.js` builds one output image's gallery card (viewer, workbench, cover pick, drag, star, delete);
`gallery_card_menu.js` is its right-click menu (insert as a Load Image node reading the output file, parameters);
`ui_keep_menu.js` is the star's keep menu (also the workbench's Keep): the whole workflow, the combo or the prompts,
each saying where it goes, opening what was kept already or removing it (to the Recycle Bin through the recipe,
notebook or material delete route); `image_keep.js` does the keeping (an image as a combo,
@@ -376,7 +379,7 @@ covered by the `styles.css` manifest.
card action buttons (one-click canvas addition with plus icon, model metadata editor, direct precision scanner without wizard modal popups)
with absolute positioning cascades immune to tooltip target conflicts, vibrant hover affordance,
safe docked sidebar preservation upon node addition, and multi-type node dispatch; `ui_detail.js`
coordinates detail display, `ui_model_editor.js` owns metadata editing, and
coordinates detail display (the file card with Show in folder in `model_location.js`), `ui_model_editor.js` owns metadata editing, and
`ui_model_selector.js` owns advanced selection. `ui_gallery.js` (cards in
`ui_gallery_card.js`) and `ui_gallery_detail.js` own generated-image browsing and workbench lifecycle,
with stage interaction in `ui_image_stage.js` and metadata tabs in
+6 -1
View File
@@ -1,6 +1,11 @@
# 📈 Anomalous Model Browser Changelog
## v1.58.2 Beta (Fix: switching pictures with the keyboard) — unreleased
## v1.58.2 Beta (Small fixes: Esc closes, Show in folder, insert a picture as a node) — unreleased
### 💬 From a Bilibili comment (来自 B 站评论)
- **Esc closes the browser (Esc 关闭浏览器)**: Esc closes what is on top: a dialog or menu first, then the Workflows / Combos workspace, then the browser. The shortcut (Ctrl + Shift + M by default) and the Extensions menu entry now close the browser when it is open. Esc in a text box still belongs to the text box.
- **Show in folder (打开所在文件夹)**: a model's details have **Show in folder** next to **Copy path**: Explorer (or Finder) opens with the file selected. Only on the computer ComfyUI runs on.
- **Insert a picture as a node (图片作为节点插入)**: right-click a gallery picture → **Insert as a Load Image node** puts a Load Image node showing that picture in the middle of the canvas. It reads the picture from the output folder; nothing is copied.
### ⌨️ Arrow keys in picture details (查看参数里方向键切图)
- In a picture's details, **← / →** (and **A / D**) switch pictures again after you ticked a node's checkbox under **All parameters**; the ticked checkbox kept the keyboard and the keys did nothing until you clicked elsewhere. Typing in a text box still keeps the arrows for the text.
+2 -1
View File
@@ -1,6 +1,6 @@
from . import (
activity_log, audio_catalog, folder_types, gallery_routes, materials, media_routes, model_catalog,
model_media, model_metadata, model_resolution, model_type_listing, kept_images, node_material, recipe_packages, recipes, scan_report, scan_summary,
model_folder, model_media, model_metadata, model_resolution, model_type_listing, kept_images, node_material, recipe_packages, recipes, scan_report, scan_summary,
local_guard, translation_routes, version_manager,
)
from .scanner import *
@@ -48,6 +48,7 @@ def setup_routes(app):
app.router.add_get('/anomalous/config', api_get_config)
app.router.add_post('/anomalous/save_config', api_save_config)
app.router.add_post('/anomalous/delete_model', model_metadata.api_delete_model)
app.router.add_post('/anomalous/open_model_folder', model_folder.api_open_model_folder)
app.router.add_get('/anomalous/compatible_models', model_catalog.api_compatible_models)
app.router.add_get('/anomalous/notebooks', api_get_notebooks)
app.router.add_post('/anomalous/save_notebook', api_save_notebook)
+42
View File
@@ -0,0 +1,42 @@
"""Shows a model file in this computer's file manager: Explorer or Finder with the file
selected, the folder on Linux. The request names the model the way the browser lists it
(type, folder index, subfolder, file name), never a path, and local_guard lets only this
computer ask (it is a POST)."""
import os
import subprocess
import sys
from aiohttp import web
from .utils import require_filename, resolve_folder_subdir, resolve_within
def _reveal(path):
if sys.platform == "win32":
subprocess.Popen(f'explorer /select,"{path}"')
elif sys.platform == "darwin":
subprocess.Popen(["open", "-R", path])
else:
subprocess.Popen(["xdg-open", os.path.dirname(path)])
async def api_open_model_folder(request):
try:
data = await request.json()
except ValueError:
data = None
if not isinstance(data, dict):
return web.json_response({"status": "error", "message": "Invalid request body"}, status=400)
try:
filename = require_filename(data.get("filename", ""))
_, folder = resolve_folder_subdir(data.get("type", ""), int(data.get("path_idx", 0)), data.get("subfolder", "/"))
path = resolve_within(folder, filename)
except (ValueError, KeyError, TypeError):
return web.json_response({"status": "error", "message": "Invalid request parameters"}, status=400)
if not os.path.isfile(path):
return web.json_response({"status": "error", "message": "Model file not found"}, status=404)
try:
_reveal(path)
except OSError as error:
return web.json_response({"status": "error", "message": str(error)}, status=500)
return web.json_response({"status": "success"})
+8 -1
View File
@@ -155,6 +155,13 @@ export function createBrowserEntry({ translate, getCurrentLanguage }) {
ensureBrowser()?.show();
}
// The shortcut and the Extensions menu entry: open, or close when it is open.
function toggle() {
const browser = ensureBrowser();
if (browser?.modal?.classList.contains('visible')) browser.close();
else browser?.show();
}
// The second shortcut (its ids still say materials) opens Prompt Studio.
async function openMaterials() {
const browser = ensureBrowser();
@@ -464,7 +471,7 @@ export function createBrowserEntry({ translate, getCurrentLanguage }) {
onClick: open
}],
commands: [
{ id: OPEN_BROWSER_COMMAND_ID, label: t('mainOpenTitle'), function: open },
{ id: OPEN_BROWSER_COMMAND_ID, label: t('mainOpenTitle'), function: toggle },
{ id: OPEN_MATERIALS_COMMAND_ID, label: t('mainOpenMaterialsTitle'), function: openMaterials },
{ id: RESET_TRIGGER_POSITION_COMMAND_ID, label: t('mainResetFloatingTriggerPosition'), function: resetPosition }
],
+84
View File
@@ -0,0 +1,84 @@
/**
* A gallery card's right-click menu: put the picture on the canvas as a Load Image node, or
* open its parameters. The node reads the output file where it is ("name.png [output]",
* ComfyUI's own way to name an output file), so nothing is copied into the input folder.
*/
import { app } from '../../../scripts/app.js';
import { translate as t } from './locales.js';
import { recordCanvasStep } from './canvas_history.js';
import { showWorkbenchToast } from './ui_prompt_toast.js';
let closeOpenMenu = null;
function row(icon, label, run, close) {
const item = document.createElement('button');
item.type = 'button';
item.className = 'anomalous-keep-row';
item.setAttribute('role', 'menuitem');
const mark = document.createElement('span');
mark.className = 'anomalous-keep-row-icon';
mark.textContent = icon;
const text = document.createElement('strong');
text.textContent = label;
item.append(mark, text);
item.onclick = () => {
close();
run();
};
return item;
}
/** A Load Image node in the middle of the visible canvas, showing `image` from the output folder. */
export function insertLoadImageNode(image) {
const node = globalThis.LiteGraph?.createNode('LoadImage');
const widget = node?.widgets?.find(item => item.name === 'image');
if (!node || !widget || !app.graph) {
showWorkbenchToast(t('galleryInsertNodeFailed'));
return;
}
const [x, y, width, height] = app.canvas?.visible_area || [0, 0, 0, 0];
node.pos = [x + width / 2 - node.size[0] / 2, y + height / 2 - node.size[1] / 2];
app.graph.add(node);
const file = image.subfolder ? `${image.subfolder}/${image.filename}` : image.filename;
widget.value = `${file} [output]`;
widget.callback?.(widget.value); // shows the picture in the node
app.canvas?.selectNode?.(node);
app.graph.setDirtyCanvas(true, true);
recordCanvasStep(app);
showWorkbenchToast(t('galleryInsertNodeDone'));
}
/** Opens the menu at the pointer; `openDetails()` opens the picture's parameters. */
export function openGalleryCardMenu(event, image, { openDetails }) {
event.preventDefault();
closeOpenMenu?.();
const menu = document.createElement('div');
menu.className = 'anomalous-keep-menu anomalous-gallery-card-menu';
menu.setAttribute('role', 'menu');
const onPointer = (pointer) => { if (!menu.contains(pointer.target)) close(); };
const onKey = (key) => {
if (key.key !== 'Escape') return;
key.stopPropagation();
close();
};
const close = () => {
menu.remove();
window.removeEventListener('pointerdown', onPointer, true);
window.removeEventListener('keydown', onKey, true);
window.removeEventListener('wheel', close, true);
if (closeOpenMenu === close) closeOpenMenu = null;
};
menu.append(
row('🖼️', t('galleryInsertAsNode'), () => insertLoadImageNode(image), close),
row('🔍', t('materialViewParameters'), openDetails, close),
);
document.body.append(menu);
const { width, height } = menu.getBoundingClientRect();
menu.style.left = `${Math.max(8, Math.min(event.clientX, window.innerWidth - width - 8))}px`;
menu.style.top = `${Math.max(8, Math.min(event.clientY, window.innerHeight - height - 8))}px`;
window.addEventListener('pointerdown', onPointer, true);
window.addEventListener('keydown', onKey, true);
window.addEventListener('wheel', close, true);
closeOpenMenu = close;
}
+10
View File
@@ -599,6 +599,11 @@ export const i18n = {
detailCopyPath: '复制路径',
detailPathCopied: '已复制',
detailPathCopyFailed: '复制失败',
detailOpenFolder: '打开所在文件夹',
detailOpenFolderFailed: '打不开文件夹:{error}',
galleryInsertAsNode: '作为“加载图像”节点插入工作流',
galleryInsertNodeDone: '已在画布中间放了一个“加载图像”节点',
galleryInsertNodeFailed: '没能放入“加载图像”节点(画布还没准备好?)',
detailCustomNamePlaceholder: '自定义名称 (留空则使用原名)',
detailNotesPlaceholder: '📓 专属备注... (支持多行)',
detailPhysicalRename: '同步物理重命名',
@@ -2502,6 +2507,11 @@ export const i18n = {
detailCopyPath: 'Copy path',
detailPathCopied: 'Copied',
detailPathCopyFailed: 'Copy failed',
detailOpenFolder: 'Show in folder',
detailOpenFolderFailed: 'Could not open the folder: {error}',
galleryInsertAsNode: 'Insert as a Load Image node',
galleryInsertNodeDone: 'A Load Image node is in the middle of the canvas',
galleryInsertNodeFailed: 'Could not add a Load Image node (is the canvas ready?)',
detailCustomNamePlaceholder: 'Custom Name (Leave empty to use original)',
detailNotesPlaceholder: '📓 Custom Notes...',
detailPhysicalRename: 'Physical Rename on Disk',
+70
View File
@@ -0,0 +1,70 @@
/**
* The model details' location card: the file name and full path, Copy path, and Show in
* folder (Explorer / Finder opens with the file selected; only on the computer ComfyUI runs on).
*/
import { translate as t } from './locales.js';
import { anomalousAlert } from './ui_dialog.js';
function actionButton(label) {
const button = document.createElement('button');
button.type = 'button';
button.className = 'anomalous-model-location-copy';
button.textContent = label;
return button;
}
async function showInFolder(model, where) {
try {
const response = await fetch('/anomalous/open_model_folder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...where, filename: model.filename }),
});
const data = await response.json().catch(() => ({}));
if (data.status !== 'success') throw new Error(data.message || `HTTP ${response.status}`);
} catch (error) {
await anomalousAlert(t('detailOpenFolderFailed', { error: error.message }));
}
}
/** `where`: the model's { type, path_idx, subfolder } in the browser. */
export function createModelLocationCard(model, where) {
const card = document.createElement('div');
card.className = 'anomalous-model-location';
const appendValue = (labelKey, value) => {
const row = document.createElement('div');
row.className = 'anomalous-model-location-row';
const label = document.createElement('span');
label.className = 'anomalous-model-location-label';
label.textContent = t(labelKey);
const content = document.createElement('code');
content.textContent = value;
content.title = value;
row.append(label, content);
card.appendChild(row);
};
appendValue('detailPhysicalFilename', model.filename);
if (model.file_path) {
appendValue('detailFullPath', model.file_path);
const actions = document.createElement('div');
actions.className = 'anomalous-model-location-actions';
const copyButton = actionButton(t('detailCopyPath'));
copyButton.onclick = async () => {
try {
await navigator.clipboard.writeText(model.file_path);
copyButton.textContent = t('detailPathCopied');
} catch (error) {
copyButton.textContent = t('detailPathCopyFailed');
}
setTimeout(() => { copyButton.textContent = t('detailCopyPath'); }, 1400);
};
const folderButton = actionButton(t('detailOpenFolder'));
folderButton.onclick = () => showInFolder(model, where);
actions.append(copyButton, folderButton);
card.appendChild(actions);
}
return card;
}
+14 -11
View File
@@ -57,21 +57,24 @@ export function closeWorkspace() {
}
/**
* Esc on the recipe / combo workspace does what a click beside it does:
* `closeWorkspace()`. Not while one of this plugin's dialogs or a ComfyUI dialog sits
* above it, and not while a text field has the key (Esc there belongs to the field).
* ComfyUI's own keybindings mark Esc as handled, so `defaultPrevented` says nothing here.
* Esc closes the browser's top layer: the recipe / combo workspace when it is open (what a
* click beside it does), else the browser. Not while one of this plugin's dialogs, overlays
* or menus, or a ComfyUI dialog, sits above it (they close themselves), and not while a text
* field has the key (Esc there belongs to the field). It listens first (capture), so a dialog
* that closes on this same Esc still counts as above. ComfyUI's own keybindings mark Esc as
* handled, so `defaultPrevented` says nothing here.
*/
export function bindWorkspaceEscape(owner) {
export function bindBrowserEscape(owner) {
window.addEventListener('keydown', (e) => {
if (e.key !== 'Escape') return;
if (!owner.modal?.classList.contains('visible') || owner.nbPanel?.style.display !== 'flex') return;
if (e.key !== 'Escape' || !owner.modal?.classList.contains('visible')) return;
if (e.target?.closest?.('input, textarea, select, [contenteditable="true"]')) return;
const layered = [...document.body.children, ...owner.nbPanel.children]
.some(el => [...el.classList].some(c => c.startsWith('anomalous-') && c.endsWith('overlay')) && el.getClientRects().length);
const layered = [...document.body.children, ...(owner.nbPanel?.children || [])]
.some(el => el !== owner.modal && el.getClientRects().length
&& (el.getAttribute('role') === 'menu' || [...el.classList].some(c => c.startsWith('anomalous-') && c.endsWith('overlay'))));
if (layered || document.querySelector('dialog[open], .p-dialog-mask')) return;
owner.closeWorkspace();
});
if (owner.nbPanel?.style.display === 'flex') owner.closeWorkspace();
else owner.close();
}, true);
}
export function hideAllPanels() {
+4 -41
View File
@@ -7,49 +7,10 @@ import { app } from "../../../scripts/app.js";
import { translate } from './locales.js';
import { escapeHtml, setSafeRichHtml } from './safe_dom.js';
import { renderSourceLine } from './model_source.js';
import { createModelLocationCard } from './model_location.js';
const t = (key, params) => translate(key, params);
function createModelLocationCard(model) {
const card = document.createElement('div');
card.className = 'anomalous-model-location';
const appendValue = (labelKey, value) => {
const row = document.createElement('div');
row.className = 'anomalous-model-location-row';
const label = document.createElement('span');
label.className = 'anomalous-model-location-label';
label.textContent = t(labelKey);
const content = document.createElement('code');
content.textContent = value;
content.title = value;
row.append(label, content);
card.appendChild(row);
};
appendValue('detailPhysicalFilename', model.filename);
if (model.file_path) {
appendValue('detailFullPath', model.file_path);
const copyButton = document.createElement('button');
copyButton.type = 'button';
copyButton.className = 'anomalous-model-location-copy';
copyButton.textContent = t('detailCopyPath');
copyButton.onclick = async () => {
try {
await navigator.clipboard.writeText(model.file_path);
copyButton.textContent = t('detailPathCopied');
} catch (error) {
copyButton.textContent = t('detailPathCopyFailed');
}
setTimeout(() => { copyButton.textContent = t('detailCopyPath'); }, 1400);
};
card.appendChild(copyButton);
}
return card;
}
export function showDetail(model) {
if (this.currentDetailObserver) {
this.currentDetailObserver.disconnect();
@@ -389,7 +350,9 @@ export function showDetail(model) {
topRow.appendChild(editMetaBtn);
rightPanel.appendChild(topRow);
rightPanel.appendChild(createModelLocationCard(model));
rightPanel.appendChild(createModelLocationCard(model, {
type: this.currentType, path_idx: this.currentPathIdx, subfolder: this.currentSubfolder,
}));
// 1.5 Custom Notes Section (Google Material Card)
if (m.custom_notes) {
+10
View File
@@ -62,6 +62,11 @@ export function anomalousAlert(message, title = 'Anomalous') {
};
okBtn.onclick = cleanup;
overlay.onkeydown = (e) => {
if (e.key !== 'Escape') return;
e.stopPropagation();
cleanup();
};
footer.appendChild(okBtn);
dialog.append(heading, text, footer);
@@ -144,6 +149,11 @@ export function anomalousConfirm(message, title = 'Anomalous', options = {}) {
cancelBtn.onclick = () => finish(null);
noBtn?.addEventListener('click', () => finish(false));
okBtn.onclick = () => finish(true);
overlay.onkeydown = (e) => {
if (e.key !== 'Escape') return;
e.stopPropagation();
finish(null); // as Cancel
};
footer.append(cancelBtn);
if (noBtn) footer.append(noBtn);
+6 -1
View File
@@ -2,7 +2,8 @@
* One output image's card in the gallery: the picture (opens the viewer, or becomes a
* model's cover while one is being chosen; drags onto the canvas), Parameters (the image
* workbench), the star (ui_keep_menu.js: keep it as a workflow, a combo or its prompts;
* filled once kept as anything) and Delete (to the Recycle Bin, after a confirmation).
* filled once kept as anything), Delete (to the Recycle Bin, after a confirmation) and the
* right-click menu (gallery_card_menu.js: insert as a Load Image node, parameters).
*/
import { translate as t } from './locales.js';
@@ -10,6 +11,7 @@ import { anomalousAlert, anomalousConfirm } from './ui_dialog.js';
import { showImageWorkbench } from './ui_gallery_detail.js';
import { openKeepMenu } from './ui_keep_menu.js';
import { keptKey } from './image_keep.js';
import { openGalleryCardMenu } from './gallery_card_menu.js';
const DETAILS_ICON = `<svg class="anomalous-gallery-details-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><line x1="2" y1="5" x2="8" y2="5"></line><line x1="12" y1="5" x2="14" y2="5"></line><circle cx="10" cy="5" r="2"></circle><line x1="2" y1="11" x2="4" y2="11"></line><line x1="8" y1="11" x2="14" y2="11"></line><circle cx="6" cy="11" r="2"></circle></svg>`;
const DELETE_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>';
@@ -138,6 +140,9 @@ export function createGalleryCard(owner, imgData, { showViewer }) {
if (window.anomalousDragGhostImg) event.dataTransfer.setDragImage(window.anomalousDragGhostImg, 40, 40);
});
img.onclick = () => (owner.gallerySelectModel ? useAsCover(owner, image) : showViewer(url));
card.addEventListener('contextmenu', (event) => {
if (!owner.gallerySelectModel) openGalleryCardMenu(event, image, { openDetails: () => openWorkbench(owner, image, url) });
});
const details = button('anomalous-gallery-details', t('materialViewDetails'), () => {
if (!owner.gallerySelectModel) openWorkbench(owner, image, url);
+2 -2
View File
@@ -13,7 +13,7 @@ import { renderRailTools } from './ui_rail_tools.js';
import { getActiveDomain } from './ui_domain_switcher.js';
import { renderAudioSidebar } from './ui_audio_sidebar.js';
import { createGallerySearchBar } from './ui_gallery.js';
import { bindWorkspaceEscape } from './ui_browser_navigation.js';
import { bindBrowserEscape } from './ui_browser_navigation.js';
import { createShellRail } from './ui_shell_rail.js';
import { installShellNavigation } from './ui_shell_nav.js';
import { bindShellDrag, bindShellResize } from './ui_shell_frame.js';
@@ -216,7 +216,7 @@ export function createDOM() {
this.nbPanel.onclick = (e) => {
if (e.target === this.nbPanel) this.closeWorkspace();
};
bindWorkspaceEscape(this);
bindBrowserEscape(this);
content.append(header, this.grid, this.detailPanel, this.galleryPanel, this.homePanel, this.activityPanel, this.scanPanel, this.settingsPanel, this.doctorPanel,
this.assistantPanel, this.audioStudioPanel, this.scriptPanel, this.audioGalleryPanel);
+1 -1
View File
@@ -1,7 +1,7 @@
@import url("./styles/00-foundation-models.css?v=20261003-settings-1");
@import url("./styles/01-shell-tools.css?v=20261003-settings-1");
@import url("./styles/02-notebooks.css?v=20261009-click-1");
@import url("./styles/03-gallery-recipes.css?v=20261009-click-1");
@import url("./styles/03-gallery-recipes.css?v=20261013-menu-1");
@import url("./styles/04-dialogs-materials.css?v=20261004-studio-1");
@import url("./styles/05-recipe-studio-overrides.css?v=20261002-theme-1");
@import url("./styles/06-material-workbench.css?v=20261004-studio-1");
+13 -1
View File
@@ -198,7 +198,13 @@
display: none;
}
/* The keep menu under a star or the workbench's Keep (ui_keep_menu.js). */
/* The keep menu under a star or the workbench's Keep (ui_keep_menu.js); a gallery card's
right-click menu (gallery_card_menu.js) wears the same look, as wide as its rows. */
.anomalous-gallery-card-menu {
width: max-content;
min-width: 200px;
}
.anomalous-keep-menu {
position: fixed;
z-index: 1000001;
@@ -1213,6 +1219,12 @@ dialog.anomalous-gallery-viewer[open] {
text-overflow: ellipsis;
}
.anomalous-model-location-actions {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.anomalous-model-location-copy {
justify-self: start;
padding: 4px 9px;