diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 2ec0d7c..f643b3f 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c9a468..5ca2d4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/api/__init__.py b/api/__init__.py index c8571e0..0278819 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -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) diff --git a/api/model_folder.py b/api/model_folder.py new file mode 100644 index 0000000..db42665 --- /dev/null +++ b/api/model_folder.py @@ -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"}) diff --git a/web/modules/browser_entry.js b/web/modules/browser_entry.js index 8164262..1cf9caa 100644 --- a/web/modules/browser_entry.js +++ b/web/modules/browser_entry.js @@ -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 } ], diff --git a/web/modules/gallery_card_menu.js b/web/modules/gallery_card_menu.js new file mode 100644 index 0000000..1c62eec --- /dev/null +++ b/web/modules/gallery_card_menu.js @@ -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; +} diff --git a/web/modules/locales.js b/web/modules/locales.js index ea177bc..45d2e24 100644 --- a/web/modules/locales.js +++ b/web/modules/locales.js @@ -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', diff --git a/web/modules/model_location.js b/web/modules/model_location.js new file mode 100644 index 0000000..355d731 --- /dev/null +++ b/web/modules/model_location.js @@ -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; +} diff --git a/web/modules/ui_browser_navigation.js b/web/modules/ui_browser_navigation.js index 5b503cb..c044a2e 100644 --- a/web/modules/ui_browser_navigation.js +++ b/web/modules/ui_browser_navigation.js @@ -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() { diff --git a/web/modules/ui_detail.js b/web/modules/ui_detail.js index aee9724..12c3f19 100644 --- a/web/modules/ui_detail.js +++ b/web/modules/ui_detail.js @@ -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) { diff --git a/web/modules/ui_dialog.js b/web/modules/ui_dialog.js index 078c8b3..ab24615 100644 --- a/web/modules/ui_dialog.js +++ b/web/modules/ui_dialog.js @@ -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); diff --git a/web/modules/ui_gallery_card.js b/web/modules/ui_gallery_card.js index 0c29105..8bd1579 100644 --- a/web/modules/ui_gallery_card.js +++ b/web/modules/ui_gallery_card.js @@ -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 = ``; const DELETE_ICON = ''; @@ -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); diff --git a/web/modules/ui_sidebar.js b/web/modules/ui_sidebar.js index ca88150..fa4cd32 100644 --- a/web/modules/ui_sidebar.js +++ b/web/modules/ui_sidebar.js @@ -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); diff --git a/web/styles.css b/web/styles.css index 1c36db0..2b28923 100644 --- a/web/styles.css +++ b/web/styles.css @@ -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"); diff --git a/web/styles/03-gallery-recipes.css b/web/styles/03-gallery-recipes.css index 0b75288..fb37b2a 100644 --- a/web/styles/03-gallery-recipes.css +++ b/web/styles/03-gallery-recipes.css @@ -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;