From 6f6cd6a4fc3db168c41837f36609b08cd3c5bc49 Mon Sep 17 00:00:00 2001 From: DemonGatanjieu <1027066412@qq.com> Date: Thu, 3 Sep 2026 20:34:09 +0800 Subject: [PATCH] style(recipe-presets): eliminate raw bootstrap-style buttons, fix broken card layout, and replace awkward hero image with sleek compact gallery deck --- web/modules/ui_recipe_detail.js | 291 ++++++++++++++++---------------- web/styles.css | 230 +++++++++++++++++++++++++ 2 files changed, 378 insertions(+), 143 deletions(-) diff --git a/web/modules/ui_recipe_detail.js b/web/modules/ui_recipe_detail.js index a757ca5..8b91ed2 100644 --- a/web/modules/ui_recipe_detail.js +++ b/web/modules/ui_recipe_detail.js @@ -2091,22 +2091,6 @@ function renderRecipeParameters(content, owner, recipe, gallery, refreshGallery, const wrapper = document.createElement('div'); wrapper.className = `anomalous-recipe-detail-parameters${animateSelection ? ' is-switching' : ''}`; - const selectionBanner = document.createElement('section'); - selectionBanner.className = 'anomalous-recipe-parameter-selection-banner'; - selectionBanner.setAttribute('aria-live', 'polite'); - const selectionCopy = document.createElement('div'); - selectionCopy.className = 'anomalous-recipe-parameter-selection-copy'; - appendText(selectionCopy, 'small', t('recipeParameterShowing'), 'anomalous-recipe-detail-muted'); - appendText( - selectionCopy, - 'strong', - parameterState?.editor?.name || selectedNotebook?.name || t('recipeParameterCurrentRecipe'), - ); - if (selectedNotebook?.timestamp) appendText(selectionCopy, 'small', dateText(selectedNotebook.timestamp), 'anomalous-recipe-detail-muted'); - appendText(selectionBanner, 'span', t('recipeParameterActive'), 'anomalous-recipe-parameter-selection-badge'); - selectionBanner.prepend(selectionCopy); - wrapper.appendChild(selectionBanner); - const layout = document.createElement('div'); layout.className = 'anomalous-recipe-parameter-notebook-layout'; const sidebar = document.createElement('aside'); @@ -2133,24 +2117,7 @@ function renderRecipeParameters(content, owner, recipe, gallery, refreshGallery, sidebarActions.style.display = 'grid'; sidebarActions.style.gap = '8px'; sidebarActions.style.marginBottom = '12px'; - - const newSnapshot = button(sidebarActions, t('recipeParameterNew'), 'anomalous-btn-primary'); - newSnapshot.onclick = () => { - const draft = cloneJson({ - workflow: baseSource.workflow, - params: baseSource.params || {}, - }); - if (!draft?.workflow) return; - parameterState.editor = { - draft, - name: `${recipe.name || t('recipeUntitled')} ยท ${t('recipeParameterNew')}`, - }; - parameterState.selectedFilename = null; - gallery.status = 'idle'; - gallery.images = []; - gallery.scanned = 0; - selectParameterTab?.(); - }; + const readCurrentHandler = async () => { readCurrent.disabled = true; readCurrent.classList.add('is-busy'); @@ -2192,31 +2159,48 @@ function renderRecipeParameters(content, owner, recipe, gallery, refreshGallery, readCurrent.textContent = originalLabel; } }; - - const readCurrent = button(sidebarActions, t('recipeParameterReadCurrent'), 'anomalous-btn-success'); + + const readCurrent = button(sidebarActions, '๐Ÿ’พ ' + t('recipeParameterReadCurrent'), 'anomalous-preset-btn-primary'); readCurrent.onclick = readCurrentHandler; - + + const newSnapshot = button(sidebarActions, 'โž• ' + t('recipeParameterNew'), 'anomalous-preset-btn-secondary'); + newSnapshot.onclick = () => { + const draft = cloneJson({ + workflow: baseSource.workflow, + params: baseSource.params || {}, + }); + if (!draft?.workflow) return; + parameterState.editor = { + draft, + name: `${recipe.name || t('recipeUntitled')} ยท ${t('recipeParameterNew')}`, + }; + parameterState.selectedFilename = null; + gallery.status = 'idle'; + gallery.images = []; + gallery.scanned = 0; + selectParameterTab?.(); + }; + sidebar.appendChild(sidebarActions); appendText(sidebar, 'small', t('recipeParameterSnapshotsHint'), 'anomalous-recipe-detail-muted'); const snapshotList = document.createElement('div'); snapshotList.className = 'anomalous-recipe-parameter-notebook-list'; + snapshotList.style.marginTop = '8px'; if (parameterState?.status === 'loading') { appendText(snapshotList, 'p', t('recipeParameterLoading'), 'anomalous-recipe-detail-muted'); } else if (parameterState?.status === 'error') { appendText(snapshotList, 'p', t('recipeParameterLoadError'), 'anomalous-recipe-dialog-error'); } else if (parameterState?.notebooks?.length) { for (const notebook of parameterState.notebooks) { + const isSelected = notebook.filename === parameterState.selectedFilename; const row = document.createElement('div'); - row.className = 'anomalous-recipe-parameter-notebook-row'; + row.className = `anomalous-preset-item-card${isSelected ? ' is-active' : ''}`; const notebookName = notebook.name || t('recipeParameterUntitled'); - const item = button(row, notebookName, 'anomalous-recipe-parameter-notebook-item'); - item.classList.toggle('is-active', notebook.filename === parameterState.selectedFilename); - item.setAttribute('aria-pressed', notebook.filename === parameterState.selectedFilename ? 'true' : 'false'); - item.title = `${notebookName} ยท ${dateText(notebook.timestamp)}`; - appendText(item, 'small', dateText(notebook.timestamp), 'anomalous-recipe-detail-muted'); - item.onclick = () => { + + const main = document.createElement('div'); + main.className = 'anomalous-preset-item-main'; + main.onclick = () => { if (parameterState.selectedFilename === notebook.filename) return; - item.classList.add('is-switching'); parameterState.editor = null; parameterState.selectedFilename = notebook.filename; parameterState.switchToken = (parameterState.switchToken || 0) + 1; @@ -2226,15 +2210,29 @@ function renderRecipeParameters(content, owner, recipe, gallery, refreshGallery, selectParameterTab?.(); }; - const rename = button(row, 'โœ๏ธ', 'anomalous-recipe-parameter-notebook-rename'); + const titleEl = appendText(main, 'div', notebookName, 'anomalous-preset-item-title'); + titleEl.title = notebookName; + + const meta = document.createElement('div'); + meta.className = 'anomalous-preset-item-meta'; + appendText(meta, 'span', dateText(notebook.timestamp), 'anomalous-preset-item-date'); + if (isSelected) { + appendText(meta, 'span', t('recipeParameterActive'), 'anomalous-preset-item-badge'); + } + main.appendChild(meta); + row.appendChild(main); + + const actions = document.createElement('div'); + actions.className = 'anomalous-preset-item-actions'; + + const rename = button(actions, 'โœ๏ธ', 'anomalous-preset-item-btn'); rename.title = t('recipeParameterRename'); - rename.setAttribute('aria-label', `${t('recipeParameterRename')}: ${notebookName}`); - rename.onclick = async () => { + rename.onclick = async (e) => { + e.stopPropagation(); const newName = await anomalousPrompt(t('recipeParameterRenamePrompt'), notebookName); if (newName === null || !newName.trim() || newName.trim() === notebookName) return; const trimmedName = newName.trim(); rename.disabled = true; - item.disabled = true; row.classList.add('is-busy'); try { const response = await fetch('/anomalous/rename_parameter', { @@ -2249,19 +2247,17 @@ function renderRecipeParameters(content, owner, recipe, gallery, refreshGallery, } catch (error) { console.error('Could not rename parameter notebook:', error); rename.disabled = false; - item.disabled = false; row.classList.remove('is-busy'); await anomalousAlert(t('recipeParameterRenameError')); } }; - const remove = button(row, 'ร—', 'anomalous-recipe-parameter-notebook-delete'); + const remove = button(actions, '๐Ÿ—‘๏ธ', 'anomalous-preset-item-btn is-delete'); remove.title = t('recipeParameterDelete'); - remove.setAttribute('aria-label', `${t('recipeParameterDelete')}: ${notebookName}`); - remove.onclick = async () => { + remove.onclick = async (e) => { + e.stopPropagation(); if (!await anomalousConfirm(t('recipeParameterDeleteConfirm', { name: notebookName }))) return; remove.disabled = true; - item.disabled = true; row.classList.add('is-deleting'); try { const response = await fetch('/anomalous/delete_parameter', { @@ -2281,11 +2277,12 @@ function renderRecipeParameters(content, owner, recipe, gallery, refreshGallery, } catch (error) { console.error('Could not delete parameter notebook:', error); remove.disabled = false; - item.disabled = false; row.classList.remove('is-deleting'); await anomalousAlert(t('recipeParameterDeleteError')); } }; + + row.appendChild(actions); snapshotList.appendChild(row); } } else { @@ -2301,37 +2298,30 @@ function renderRecipeParameters(content, owner, recipe, gallery, refreshGallery, } const intro = document.createElement('section'); - intro.className = 'anomalous-recipe-detail-section'; - const introHeading = document.createElement('div'); - introHeading.className = 'anomalous-recipe-detail-section-heading'; - appendText(introHeading, 'h4', t('recipeDetailParameters')); - if (parameterState?.selectedFilename) { - const currentNotebook = parameterState.notebooks?.find(nb => nb.filename === parameterState.selectedFilename); - const currentName = currentNotebook?.name || source?.name || t('recipeParameterUntitled'); - const renameHeadingBtn = button(introHeading, 'โœ๏ธ ' + t('recipeParameterRename'), 'anomalous-btn-ghost'); - renameHeadingBtn.onclick = async () => { - const newName = await anomalousPrompt(t('recipeParameterRenamePrompt'), currentName); - if (newName === null || !newName.trim() || newName.trim() === currentName) return; - const trimmedName = newName.trim(); - renameHeadingBtn.disabled = true; - try { - const response = await fetch('/anomalous/rename_parameter', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ filename: parameterState.selectedFilename, name: trimmedName }), - }); - const payload = await response.json(); - if (!response.ok || payload.status !== 'success') throw new Error(payload.message || 'rename failed'); - await parameterState.refresh?.(true); - } catch (error) { - console.error('Could not rename parameter notebook:', error); - renameHeadingBtn.disabled = false; - await anomalousAlert(t('recipeParameterRenameError')); - } - }; - } - const applyButton = button(introHeading, t('recipeParameterApply'), 'anomalous-btn-primary'); - const applyStatus = appendText(introHeading, 'small', '', 'anomalous-recipe-header-status'); + intro.className = 'anomalous-preset-console-header'; + + const consoleInfo = document.createElement('div'); + consoleInfo.className = 'anomalous-preset-console-info'; + + const titleRow = document.createElement('div'); + titleRow.className = 'anomalous-preset-console-title-row'; + + const currentNotebook = parameterState.notebooks?.find(nb => nb.filename === parameterState.selectedFilename); + const currentName = currentNotebook?.name || source?.name || t('recipeParameterCurrentRecipe'); + + appendText(titleRow, 'h3', currentName, 'anomalous-preset-console-title'); + appendText(titleRow, 'span', t('recipeParameterActive'), 'anomalous-preset-item-badge'); + consoleInfo.appendChild(titleRow); + + appendText(consoleInfo, 'p', t('recipeDetailParametersHint'), 'anomalous-recipe-detail-muted'); + + const consoleActions = document.createElement('div'); + consoleActions.className = 'anomalous-preset-console-actions'; + + const applyButton = button(consoleActions, '๐Ÿš€ ' + t('recipeParameterApply'), 'anomalous-preset-btn-primary'); + applyButton.style.padding = '8px 16px'; + applyButton.style.fontSize = '0.88rem'; + const applyStatus = appendText(consoleActions, 'small', '', 'anomalous-recipe-header-status'); applyButton.onclick = async () => { applyButton.disabled = true; applyButton.classList.add('is-busy'); @@ -2356,10 +2346,74 @@ function renderRecipeParameters(content, owner, recipe, gallery, refreshGallery, applyButton.classList.remove('is-busy'); } }; - intro.appendChild(introHeading); - appendText(intro, 'p', t('recipeDetailParametersHint'), 'anomalous-recipe-detail-muted'); - renderPromptSection(intro, owner, recipe, source, selectParameterTab); + if (parameterState?.selectedFilename) { + const renameHeadingBtn = button(consoleActions, 'โœ๏ธ ' + t('recipeParameterRename'), 'anomalous-btn-ghost'); + renameHeadingBtn.onclick = async () => { + const newName = await anomalousPrompt(t('recipeParameterRenamePrompt'), currentName); + if (newName === null || !newName.trim() || newName.trim() === currentName) return; + const trimmedName = newName.trim(); + renameHeadingBtn.disabled = true; + try { + const response = await fetch('/anomalous/rename_parameter', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ filename: parameterState.selectedFilename, name: trimmedName }), + }); + const payload = await response.json(); + if (!response.ok || payload.status !== 'success') throw new Error(payload.message || 'rename failed'); + await parameterState.refresh?.(true); + } catch (error) { + console.error('Could not rename parameter notebook:', error); + renameHeadingBtn.disabled = false; + await anomalousAlert(t('recipeParameterRenameError')); + } + }; + } + consoleInfo.appendChild(consoleActions); + intro.appendChild(consoleInfo); + + // Compact gallery showcase tile on top right + if (gallery.status === 'ready' && gallery.images.length) { + const compactGallery = document.createElement('div'); + compactGallery.className = 'anomalous-preset-compact-gallery'; + compactGallery.title = `${t('recipeParameterGallery')} (${gallery.images.length})`; + const thumb = document.createElement('img'); + thumb.src = outputImageUrl(gallery.images[0]); + compactGallery.appendChild(thumb); + appendText(compactGallery, 'span', `๐Ÿ–ผ๏ธ ${gallery.images.length}`, 'anomalous-preset-compact-gallery-badge'); + compactGallery.onclick = () => { + const dialog = document.createElement('dialog'); + dialog.className = 'anomalous-recipe-gallery-dialog'; + const closeBtn = document.createElement('button'); + closeBtn.className = 'anomalous-dialog-close anomalous-btn-ghost'; + closeBtn.innerHTML = 'โœ•'; + closeBtn.onclick = () => dialog.close(); + dialog.appendChild(closeBtn); + const heading = document.createElement('h3'); + heading.textContent = `${t('recipeParameterGallery')} (${gallery.images.length})`; + heading.className = 'anomalous-recipe-gallery-dialog-title'; + dialog.appendChild(heading); + const grid = document.createElement('div'); + grid.className = 'anomalous-recipe-gallery-grid'; + for (const sourceImage of gallery.images) { + const card = document.createElement('article'); + card.className = 'anomalous-recipe-gallery-card'; + const url = outputImageUrl(sourceImage); + const image = document.createElement('img'); + image.src = url; + image.loading = 'lazy'; + image.onclick = () => owner.showGalleryViewer?.(url); + card.appendChild(image); + grid.appendChild(card); + } + dialog.appendChild(grid); + document.body.appendChild(dialog); + dialog.addEventListener('close', () => dialog.remove()); + dialog.showModal(); + }; + intro.appendChild(compactGallery); + } const params = source?.params || {}; const resDisplay = formatRecipeResolution(params.resolution); @@ -2413,64 +2467,15 @@ function renderRecipeParameters(content, owner, recipe, gallery, refreshGallery, if (summaryGrid.childElementCount) summary.appendChild(summaryGrid); else appendText(summary, 'p', t('recipeDetailNoSavedParameters'), 'anomalous-recipe-detail-muted'); + const promptWrap = document.createElement('div'); + promptWrap.style.marginBottom = '14px'; + renderPromptSection(promptWrap, owner, recipe, source, selectParameterTab); + const nodesSection = document.createElement('section'); nodesSection.className = 'anomalous-recipe-detail-section'; renderRawNodesLazy(nodesSection, source); - wrapper.append(intro, summary, nodesSection); - - // Replace old bottom gallery with Hero Section at the top - if (gallery.status === 'ready' && gallery.images.length) { - const heroSection = document.createElement('section'); - heroSection.className = 'anomalous-recipe-detail-hero'; - - const heroImage = document.createElement('img'); - heroImage.src = outputImageUrl(gallery.images[0]); - heroImage.className = 'anomalous-recipe-detail-hero-image'; - heroImage.onclick = () => owner.showGalleryViewer?.(heroImage.src); - heroSection.appendChild(heroImage); - - const galleryButton = document.createElement('button'); - galleryButton.className = 'anomalous-recipe-detail-hero-gallery-btn'; - galleryButton.innerHTML = `๐Ÿ–ผ๏ธ ${t('recipeParameterGallery')}`; - galleryButton.onclick = () => { - const dialog = document.createElement('dialog'); - dialog.className = 'anomalous-recipe-gallery-dialog'; - - const closeBtn = document.createElement('button'); - closeBtn.className = 'anomalous-dialog-close anomalous-btn-ghost'; - closeBtn.innerHTML = 'โœ•'; - closeBtn.onclick = () => dialog.close(); - dialog.appendChild(closeBtn); - - const heading = document.createElement('h3'); - heading.textContent = t('recipeParameterGallery'); - heading.className = 'anomalous-recipe-gallery-dialog-title'; - dialog.appendChild(heading); - - const grid = document.createElement('div'); - grid.className = 'anomalous-recipe-gallery-grid'; - for (const sourceImage of gallery.images) { - const card = document.createElement('article'); - card.className = 'anomalous-recipe-gallery-card'; - const url = outputImageUrl(sourceImage); - const image = document.createElement('img'); - image.src = url; - image.loading = 'lazy'; - image.onclick = () => owner.showGalleryViewer?.(url); - card.appendChild(image); - grid.appendChild(card); - } - dialog.appendChild(grid); - document.body.appendChild(dialog); - - dialog.addEventListener('close', () => dialog.remove()); - dialog.showModal(); - }; - heroSection.appendChild(galleryButton); - wrapper.insertBefore(heroSection, intro); - } - + wrapper.append(intro, summary, promptWrap, nodesSection); layout.append(sidebar, wrapper); content.appendChild(layout); } diff --git a/web/styles.css b/web/styles.css index f9123e9..e6f4166 100644 --- a/web/styles.css +++ b/web/styles.css @@ -4226,3 +4226,233 @@ dialog.anomalous-gallery-viewer::backdrop { .anomalous-recipe-beta-notice[style*="display: none"] { display: none !important; } + +/* ========================================================================== */ +/* Modern Studio Presets (Parameter Notebook Overhaul) */ +/* ========================================================================== */ + +.anomalous-preset-btn-primary { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 9px 12px; + border-radius: 8px; + background: linear-gradient(135deg, #6366f1, #4f46e5); + border: 1px solid rgba(129, 140, 248, 0.4); + color: #ffffff; + font-size: 0.82rem; + font-weight: 500; + cursor: pointer; + box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3); + transition: all 0.2s ease; +} + +.anomalous-preset-btn-primary:hover { + filter: brightness(1.12); + box-shadow: 0 4px 14px rgba(99, 102, 241, 0.45); + transform: translateY(-1px); +} + +.anomalous-preset-btn-secondary { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 8px 12px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.09); + color: #cbd5e1; + font-size: 0.82rem; + cursor: pointer; + transition: all 0.2s ease; +} + +.anomalous-preset-btn-secondary:hover { + background: rgba(255, 255, 255, 0.09); + border-color: rgba(255, 255, 255, 0.16); + color: #ffffff; +} + +.anomalous-preset-item-card { + position: relative; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 10px 12px; + border-radius: 9px; + background: rgba(255, 255, 255, 0.025); + border: 1px solid rgba(255, 255, 255, 0.07); + cursor: pointer; + transition: all 0.2s ease; + margin-bottom: 6px; +} + +.anomalous-preset-item-card:hover { + background: rgba(255, 255, 255, 0.05); + border-color: rgba(255, 255, 255, 0.14); +} + +.anomalous-preset-item-card.is-active { + background: linear-gradient(135deg, rgba(99, 102, 241, 0.12), rgba(79, 70, 229, 0.06)); + border-color: rgba(129, 140, 248, 0.5); + box-shadow: inset 3px 0 0 #818cf8, 0 4px 12px rgba(0, 0, 0, 0.2); +} + +.anomalous-preset-item-main { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 4px; +} + +.anomalous-preset-item-title { + font-size: 0.88rem; + font-weight: 500; + color: #e2e8f0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.anomalous-preset-item-card.is-active .anomalous-preset-item-title { + color: #ffffff; + font-weight: 600; +} + +.anomalous-preset-item-meta { + display: flex; + align-items: center; + gap: 6px; + font-size: 0.72rem; + color: #94a3b8; +} + +.anomalous-preset-item-badge { + padding: 1px 6px; + border-radius: 4px; + background: rgba(99, 102, 241, 0.2); + color: #a5b4fc; + font-size: 0.68rem; + font-weight: 600; +} + +.anomalous-preset-item-actions { + display: flex; + align-items: center; + gap: 4px; + opacity: 0.4; + transition: opacity 0.15s ease; +} + +.anomalous-preset-item-card:hover .anomalous-preset-item-actions { + opacity: 1; +} + +.anomalous-preset-item-btn { + padding: 5px 7px; + border-radius: 6px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.08); + color: #cbd5e1; + cursor: pointer; + font-size: 0.78rem; + line-height: 1; + transition: all 0.15s; +} + +.anomalous-preset-item-btn:hover { + background: rgba(255, 255, 255, 0.16); + color: #fff; + transform: translateY(-1px); +} + +.anomalous-preset-item-btn.is-delete:hover { + background: rgba(239, 68, 68, 0.2); + border-color: rgba(239, 68, 68, 0.4); + color: #fca5a5; +} + +/* Preset Console Header (Right Main Stage) */ +.anomalous-preset-console-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 16px 18px; + border-radius: 12px; + background: linear-gradient(145deg, rgba(30, 32, 38, 0.7), rgba(18, 19, 23, 0.85)); + border: 1px solid rgba(255, 255, 255, 0.08); + margin-bottom: 14px; + backdrop-filter: blur(12px); +} + +.anomalous-preset-console-info { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 6px; +} + +.anomalous-preset-console-title-row { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +.anomalous-preset-console-title { + font-size: 1.15rem; + font-weight: 600; + color: #f8fafc; + margin: 0; +} + +.anomalous-preset-console-actions { + display: flex; + align-items: center; + gap: 8px; + margin-top: 4px; +} + +.anomalous-preset-compact-gallery { + position: relative; + width: 120px; + height: 80px; + border-radius: 8px; + overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.12); + cursor: pointer; + flex-shrink: 0; + box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35); + transition: transform 0.2s ease, border-color 0.2s ease; +} + +.anomalous-preset-compact-gallery:hover { + transform: scale(1.03); + border-color: rgba(99, 102, 241, 0.5); +} + +.anomalous-preset-compact-gallery img { + width: 100% !important; + height: 100% !important; + max-height: none !important; + object-fit: cover !important; +} + +.anomalous-preset-compact-gallery-badge { + position: absolute; + bottom: 4px; + right: 4px; + padding: 2px 6px; + border-radius: 4px; + background: rgba(0, 0, 0, 0.75); + color: #fff; + font-size: 0.68rem; + backdrop-filter: blur(4px); + white-space: nowrap; +}