Parameter Notebook: Implement Dashboard Layout and Hero Gallery
This commit is contained in:
@@ -1,5 +1,20 @@
|
||||
# AI Changelog
|
||||
|
||||
## [Snapshot] 2026-08-06 - Parameter Notebook UI Redesign & Hero Gallery
|
||||
|
||||
**Implemented**
|
||||
|
||||
- **Parameter Notebook Layout**: Completely redesigned the parameter notebook to use a modern Dashboard layout. The parameter summary now uses a spacious CSS Grid format (`.anomalous-recipe-detail-summary-grid`).
|
||||
- **Node Cards & Smart Expansion**: Each node's parameters are wrapped in a styled card (`.anomalous-recipe-detail-parameter-node`). The inner widgets use a responsive grid layout to eliminate vertical cramming. Overly long nodes automatically collapse to a maximum height (`is-collapsed`) with a smooth gradient mask and a bottom expand button.
|
||||
- **Hero Image Gallery (Plan B)**: Replaced the bottom inline gallery section with a prominent Hero Section (`.anomalous-recipe-detail-hero`) at the top. The first generated image acts as a large cover banner.
|
||||
- **Modal Gallery Viewer**: If a recipe has multiple generated images, a sleek glassmorphism button overlay appears on the hero image (e.g., `🖼️ 图库 (5)`). Clicking it opens a native HTML `<dialog>` containing the full gallery grid, preventing the parameter view from bloating vertically while keeping the core browsing experience focused.
|
||||
|
||||
**Validation**
|
||||
|
||||
- Node widget grid correctly scales based on available width.
|
||||
- Smart expansion accurately toggles `max-height` via CSS classes without recalculating DOM dimensions.
|
||||
- Native `<dialog>` modal manages z-index and backdrop natively, avoiding clashes with the main UI shell.
|
||||
|
||||
## [Snapshot] 2026-08-06 - Parameter Notebook UI Tweaks
|
||||
|
||||
**Implemented**
|
||||
|
||||
+109
-46
@@ -1419,7 +1419,12 @@ function renderParameterNotebookEditor(wrapper, owner, recipe, parameterState, s
|
||||
: workflowNode.widgets_values.map((value, index) => ({ name: `${t('recipeDetailWidget')} ${index + 1}`, index, value }));
|
||||
const block = document.createElement('article');
|
||||
block.className = 'anomalous-recipe-detail-parameter-node';
|
||||
appendText(block, 'strong', [node.title, node.type].filter(Boolean).join(' · ') || t('recipeDetailUnknownNode'));
|
||||
const titleText = [node.title, node.type].filter(Boolean).join(' · ') || t('recipeDetailUnknownNode');
|
||||
appendText(block, 'strong', titleText, 'anomalous-recipe-detail-node-title');
|
||||
|
||||
const widgetsContainer = document.createElement('div');
|
||||
widgetsContainer.className = 'anomalous-recipe-detail-node-widgets';
|
||||
|
||||
for (let visibleIndex = 0; visibleIndex < widgets.length && rendered < 1200; visibleIndex += 1) {
|
||||
const widget = widgets[visibleIndex] || {};
|
||||
const index = Number.isInteger(widget.index) ? widget.index : visibleIndex;
|
||||
@@ -1427,7 +1432,7 @@ function renderParameterNotebookEditor(wrapper, owner, recipe, parameterState, s
|
||||
const value = workflowNode.widgets_values[index];
|
||||
const row = document.createElement('label');
|
||||
row.className = 'anomalous-recipe-detail-parameter-row anomalous-recipe-parameter-editor-row';
|
||||
appendText(row, 'span', widget.name || `${t('recipeDetailWidget')} ${index + 1}`, 'anomalous-recipe-detail-label');
|
||||
appendText(row, 'span', widget.name || `${t('recipeDetailWidget')} ${index + 1}`, 'anomalous-recipe-detail-parameter-name');
|
||||
const volatile = isVolatileParameter(node, widget, index);
|
||||
const sensitive = /(?:api.?key|access.?token|auth|password|passwd|secret|credential)/i.test(String(widget.name || ''));
|
||||
if (volatile || sensitive) {
|
||||
@@ -1450,14 +1455,32 @@ function renderParameterNotebookEditor(wrapper, owner, recipe, parameterState, s
|
||||
input.classList.remove('is-invalid');
|
||||
} catch (error) {
|
||||
input.classList.add('is-invalid');
|
||||
console.warn('Parameter input invalid:', error);
|
||||
}
|
||||
};
|
||||
row.appendChild(input);
|
||||
}
|
||||
block.appendChild(row);
|
||||
widgetsContainer.appendChild(row);
|
||||
rendered += 1;
|
||||
}
|
||||
if (block.querySelector('.anomalous-recipe-parameter-editor-row')) nodeList.appendChild(block);
|
||||
if (widgetsContainer.childElementCount) {
|
||||
block.appendChild(widgetsContainer);
|
||||
if (widgetsContainer.childElementCount > 4) {
|
||||
block.classList.add('is-collapsed');
|
||||
const expandOverlay = document.createElement('div');
|
||||
expandOverlay.className = 'anomalous-recipe-detail-node-expand';
|
||||
const expandBtn = document.createElement('button');
|
||||
expandBtn.className = 'anomalous-btn-ghost';
|
||||
expandBtn.innerHTML = '▼';
|
||||
expandBtn.onclick = () => {
|
||||
block.classList.remove('is-collapsed');
|
||||
expandOverlay.remove();
|
||||
};
|
||||
expandOverlay.appendChild(expandBtn);
|
||||
block.appendChild(expandOverlay);
|
||||
}
|
||||
nodeList.appendChild(block);
|
||||
}
|
||||
}
|
||||
if (!nodeList.childElementCount) appendText(nodeList, 'p', t('recipeDetailNoSavedParameters'), 'anomalous-recipe-detail-muted');
|
||||
editor.appendChild(nodeList);
|
||||
@@ -1703,7 +1726,9 @@ function renderRecipeParameters(content, owner, recipe, gallery, refreshGallery,
|
||||
const block = document.createElement('article');
|
||||
block.className = 'anomalous-recipe-detail-parameter-node';
|
||||
const title = [node.title, node.type].filter(Boolean).join(' · ') || t('recipeDetailUnknownNode');
|
||||
appendText(block, 'strong', title);
|
||||
appendText(block, 'strong', title, 'anomalous-recipe-detail-node-title');
|
||||
const widgetsContainer = document.createElement('div');
|
||||
widgetsContainer.className = 'anomalous-recipe-detail-node-widgets';
|
||||
for (let visibleIndex = 0; visibleIndex < widgets.length && renderedWidgets < 1200; visibleIndex += 1) {
|
||||
const widget = widgets[visibleIndex] || {};
|
||||
const index = Number.isInteger(widget.index) ? widget.index : visibleIndex;
|
||||
@@ -1712,59 +1737,97 @@ function renderRecipeParameters(content, owner, recipe, gallery, refreshGallery,
|
||||
: widget.value;
|
||||
const label = widget.name || `${t('recipeDetailWidget')} ${index + 1}`;
|
||||
const volatile = isVolatileParameter(node, widget, index);
|
||||
if (renderParameterField(block, label, volatile ? 0 : value, {
|
||||
if (renderParameterField(widgetsContainer, label, volatile ? 0 : value, {
|
||||
redact: volatile,
|
||||
copy: false,
|
||||
collapse: false,
|
||||
})) renderedWidgets += 1;
|
||||
}
|
||||
if (block.querySelector('.anomalous-recipe-detail-parameter-row')) nodeList.appendChild(block);
|
||||
if (widgetsContainer.childElementCount) {
|
||||
block.appendChild(widgetsContainer);
|
||||
if (widgetsContainer.childElementCount > 4) {
|
||||
block.classList.add('is-collapsed');
|
||||
const expandOverlay = document.createElement('div');
|
||||
expandOverlay.className = 'anomalous-recipe-detail-node-expand';
|
||||
const expandBtn = document.createElement('button');
|
||||
expandBtn.className = 'anomalous-btn-ghost';
|
||||
expandBtn.innerHTML = '▼';
|
||||
expandBtn.onclick = () => {
|
||||
block.classList.remove('is-collapsed');
|
||||
expandOverlay.remove();
|
||||
};
|
||||
expandOverlay.appendChild(expandBtn);
|
||||
block.appendChild(expandOverlay);
|
||||
}
|
||||
nodeList.appendChild(block);
|
||||
}
|
||||
}
|
||||
if (nodeList.childElementCount) nodesSection.appendChild(nodeList);
|
||||
else appendText(nodesSection, 'p', t('recipeDetailNoSavedParameters'), 'anomalous-recipe-detail-muted');
|
||||
|
||||
wrapper.append(intro, summary, nodesSection);
|
||||
|
||||
const gallerySection = document.createElement('section');
|
||||
gallerySection.className = 'anomalous-recipe-detail-section anomalous-recipe-gallery';
|
||||
const heading = document.createElement('div');
|
||||
heading.className = 'anomalous-recipe-detail-section-heading';
|
||||
appendText(heading, 'h5', t('recipeParameterGallery'));
|
||||
const refreshButton = button(heading, t('recipeGalleryRefresh'), 'anomalous-btn-ghost anomalous-recipe-gallery-refresh');
|
||||
refreshButton.onclick = () => { void refreshGallery(true); };
|
||||
gallerySection.appendChild(heading);
|
||||
if (gallery.status === 'loading') {
|
||||
appendText(gallerySection, 'p', t('recipeParameterGalleryLoading'), 'anomalous-recipe-detail-muted');
|
||||
} else if (gallery.status === 'error') {
|
||||
appendText(gallerySection, 'p', t('recipeParameterGalleryError'), 'anomalous-recipe-detail-muted');
|
||||
} else if (gallery.status === 'ready' && gallery.images.length) {
|
||||
appendText(gallerySection, 'small', t('recipeGalleryScanHint').replace('{count}', String(gallery.scanned || 0)), 'anomalous-recipe-detail-muted');
|
||||
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.alt = t('recipeGalleryOpenImage');
|
||||
image.loading = 'lazy';
|
||||
image.onclick = () => { void showGalleryComparison(card, owner, sourceImage); };
|
||||
card.appendChild(image);
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'anomalous-recipe-gallery-card-actions';
|
||||
const viewImage = button(actions, t('recipeGalleryOpenImage'), 'anomalous-btn-ghost');
|
||||
viewImage.onclick = () => owner.showGalleryViewer?.(url);
|
||||
const compare = button(actions, t('recipeGalleryCompare'), 'anomalous-btn-ghost');
|
||||
compare.onclick = () => { void showGalleryComparison(card, owner, sourceImage); };
|
||||
card.appendChild(actions);
|
||||
grid.appendChild(card);
|
||||
|
||||
// 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);
|
||||
|
||||
if (gallery.images.length > 1) {
|
||||
const galleryButton = document.createElement('button');
|
||||
galleryButton.className = 'anomalous-recipe-detail-hero-gallery-btn';
|
||||
galleryButton.innerHTML = `🖼️ ${t('recipeParameterGallery')} (${gallery.images.length})`;
|
||||
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 = () => { void showGalleryComparison(card, owner, sourceImage); };
|
||||
card.appendChild(image);
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'anomalous-recipe-gallery-card-actions';
|
||||
const viewBtn = button(actions, t('recipeGalleryOpenImage'), 'anomalous-btn-ghost');
|
||||
viewBtn.onclick = () => owner.showGalleryViewer?.(url);
|
||||
const compareBtn = button(actions, t('recipeGalleryCompare'), 'anomalous-btn-ghost');
|
||||
compareBtn.onclick = () => { void showGalleryComparison(card, owner, sourceImage); };
|
||||
card.appendChild(actions);
|
||||
grid.appendChild(card);
|
||||
}
|
||||
dialog.appendChild(grid);
|
||||
document.body.appendChild(dialog);
|
||||
|
||||
dialog.addEventListener('close', () => dialog.remove());
|
||||
dialog.showModal();
|
||||
};
|
||||
heroSection.appendChild(galleryButton);
|
||||
}
|
||||
gallerySection.appendChild(grid);
|
||||
} else if (gallery.status === 'ready') {
|
||||
appendText(gallerySection, 'p', t('recipeParameterGalleryEmpty'), 'anomalous-recipe-detail-muted');
|
||||
wrapper.insertBefore(heroSection, intro);
|
||||
}
|
||||
wrapper.appendChild(gallerySection);
|
||||
|
||||
layout.append(sidebar, wrapper);
|
||||
content.appendChild(layout);
|
||||
}
|
||||
|
||||
+166
@@ -2961,3 +2961,169 @@
|
||||
.anomalous-recipe-parameter-notebook-item.is-switching {
|
||||
animation: anomalous-parameter-switch 0.25s cubic-bezier(0.1, 0.9, 0.2, 1) forwards;
|
||||
}
|
||||
|
||||
/* Parameter Summary Grid Styling */
|
||||
.anomalous-recipe-detail-summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.anomalous-recipe-detail-summary-grid .anomalous-recipe-detail-parameter-row.is-wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
/* Node Parameter Card Styling */
|
||||
.anomalous-recipe-detail-parameter-node {
|
||||
position: relative;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 8px;
|
||||
transition: max-height 0.3s ease;
|
||||
}
|
||||
|
||||
.anomalous-recipe-detail-node-title {
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
font-size: 1.05rem;
|
||||
color: #e0e0e0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Grid Layout for Node Widgets */
|
||||
.anomalous-recipe-detail-node-widgets {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Smart Expansion (Collapsible Nodes) */
|
||||
.anomalous-recipe-detail-parameter-node.is-collapsed {
|
||||
max-height: 180px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.anomalous-recipe-detail-node-expand {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 70px;
|
||||
background: linear-gradient(to bottom, transparent 0%, #202020 80%, #202020 100%);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
padding-bottom: 8px;
|
||||
z-index: 10;
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
|
||||
.anomalous-recipe-detail-node-expand button {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 12px;
|
||||
padding: 4px 16px;
|
||||
font-size: 0.8rem;
|
||||
color: #ccc;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.anomalous-recipe-detail-node-expand button:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Hero Section Styling */
|
||||
.anomalous-recipe-detail-hero {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-height: 350px;
|
||||
margin-bottom: 24px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.anomalous-recipe-detail-hero-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 350px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.anomalous-recipe-detail-hero-image:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.anomalous-recipe-detail-hero-gallery-btn {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: #fff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
backdrop-filter: blur(8px);
|
||||
transition: background 0.2s, transform 0.2s;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.anomalous-recipe-detail-hero-gallery-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Modal Gallery Dialog Styling */
|
||||
.anomalous-recipe-gallery-dialog {
|
||||
background: #1e1e1e;
|
||||
color: #eee;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
width: 90vw;
|
||||
max-width: 1200px;
|
||||
max-height: 90vh;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.anomalous-recipe-gallery-dialog::backdrop {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.anomalous-recipe-gallery-dialog-title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.anomalous-dialog-close {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
font-size: 1.2rem;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user