diff --git a/.agents/logs/ai_changelog.md b/.agents/logs/ai_changelog.md index a60d382..6d53b50 100644 --- a/.agents/logs/ai_changelog.md +++ b/.agents/logs/ai_changelog.md @@ -804,7 +804,7 @@ - Replaced the narrow six-column common-parameter grid with a responsive minimum-width grid. - Kept labels in a horizontal column and values in a flexible column so sampler, scheduler, and seed no longer render one character per line. -- Summary values now use a single-line ellipsis while the existing copy action still copies the complete value; full expansion remains available in the detailed parameter rows. +- Summary values now wrap in a full-width row instead of being silently truncated; the existing copy action still copies the complete value. - Synchronized the layout boundary in `ARCHITECTURE.md`. **Validation** @@ -814,6 +814,23 @@ - `node tests/recipe_parser_roundtrip.mjs`. - `git diff --check`. +## [Snapshot] 2026-08-05 - Hide volatile values and add editable notes + +**Implemented** + +- Kept the Seed label but redacted runtime-changing seed values from parameter summaries, detailed display, copy actions, and the new-note editor; the serialized workflow slot remains untouched for ComfyUI compatibility. +- Removed summary ellipsis for common values and made long model, LoRA, and resolution values span a full row with wrapping and copy support. +- Renamed the Parameters tab to “Parameter Notebook / 参数笔记本”. +- Added “New parameter note”: it clones the selected snapshot or recipe skeleton, lets users edit safe node widget values, hides volatile/sensitive controls, and saves a new immutable snapshot without modifying history. +- Synchronized the parameter notebook boundaries in `ARCHITECTURE.md`. + +**Validation** + +- All 19 JavaScript files passed `node --check`. +- `python_embeded/python.exe tests/test_recipe_roundtrip.py` (15 tests passed). +- `node tests/recipe_parser_roundtrip.mjs`. +- `git diff --check`. + ## [Snapshot] 2026-08-04 - Bind parameter snapshots to recipes **Implemented** diff --git a/.agents/logs/ai_lessons.md b/.agents/logs/ai_lessons.md index cbb9b20..41a213a 100644 --- a/.agents/logs/ai_lessons.md +++ b/.agents/logs/ai_lessons.md @@ -534,3 +534,9 @@ Keep visual identifiers stable during bug fixing. Restore the established 📦 t **The Problem**: `auto-fit` with a 124px minimum created many narrow summary cards. Flex children then wrapped Chinese labels and numeric values vertically, making ordinary sampler/seed data hard to read. **The Practice**: Give summary cards a readable minimum width, use an explicit label/value grid inside each card, and reserve ellipsis for compact summaries. Keep the full value and copy action available in the detailed viewer rather than forcing every summary card to expand. + +## 81. Volatile values need a display policy separate from workflow storage + +**The Problem**: Seeds and similar runtime fields are useful slots to ComfyUI but are not useful identities for a parameter notebook. Showing them as ordinary values also makes users think they define the saved recipe result. + +**The Practice**: Keep the raw slot only for host compatibility, but redact volatile values from display, copy, editing, and parameter matching. Keep the field label so users understand why a parameter is omitted. New editable notes must create a new immutable snapshot rather than mutating the selected history item. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index da71155..a1ea896 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -36,6 +36,8 @@ The "Parameter Notebook" system has been fully integrated into the Workflow Reci When a user saves or updates a recipe, the system automatically creates a new, immutable Parameter Notebook capturing the exact generation values of that moment, and associates it with the recipe (`recipe_filename`). In the Recipe Detail view, the "Parameters" tab is now a two-pane interface: a sidebar listing the history of Parameter Notebooks for that recipe, and a main viewer showing the parameters and matching `sha256-params-v1` gallery for the selected notebook. The global sidebar entry for Parameter Notebooks has been removed, solidifying recipes as the primary structural template and parameter notebooks as their concrete instantiation history. +The Parameters tab keeps runtime-volatile values such as sampler seeds visible by name but redacts their values from summaries, copies, editing, and parameter matching; the raw serialized workflow retains the slot only for ComfyUI compatibility. Common values use readable horizontal label/value cards and preserve full text instead of silently truncating it. “New parameter note” clones the selected recipe/snapshot into an editable draft and saves a new immutable snapshot; it never edits or overwrites historical notes. + ## 1. Project Philosophy (设计理念) * **Zero Frameworks**: No React, Vue, or build tools. Everything is Vanilla JS and CSS for maximum compatibility, minimum overhead, and zero compilation steps. * **Non-Intrusive Integration**: Operates as a floating Gemini-style popover (`#anomalous-container`) mounted via ComfyUI's standard UI extension system. It avoids altering the native ComfyUI canvas except when explicit interaction is required. The floating trigger is mounted before browser panels are initialized and retries initialization on click, so a panel regression or missing optional browser API cannot hide the plugin entry point. diff --git a/web/modules/locales.js b/web/modules/locales.js index 2fa96a1..011af86 100644 --- a/web/modules/locales.js +++ b/web/modules/locales.js @@ -193,7 +193,7 @@ export const i18n = { recipeGalleryRecipeValue: '配方', recipeGalleryImageValue: '图片', recipeDetailModels: '模型与复现', - recipeDetailParameters: '参数', + recipeDetailParameters: '参数笔记本', recipeDetailParametersHint: '这里展示配方保存的完整参数;长文本可展开并复制。参数图库只匹配相同参数,不会因为本地路径不同而失效。', recipeParameterSnapshots: '参数快照', recipeParameterSnapshotsHint: '每次保存配方都会留下一个只读版本。', @@ -202,6 +202,13 @@ export const i18n = { recipeParameterLoadError: '无法读取参数快照。', recipeParameterNoSnapshots: '这个配方还没有参数快照。', recipeParameterUntitled: '未命名参数快照', + recipeParameterNew: '新建参数笔记', + recipeParameterNewHint: '基于当前配方骨架编辑节点参数;种子等运行时变化值会被忽略。保存后会生成新的只读快照,不会覆盖历史记录。', + recipeParameterName: '笔记名称', + recipeParameterSave: '保存参数笔记', + recipeParameterCancel: '取消编辑', + recipeParameterSaveError: '参数笔记保存失败。', + recipeParameterSensitiveHidden: '敏感控件已隐藏', recipeDetailParameterSummary: '常用参数', recipeDetailNodeParameters: '节点参数', recipeDetailUnknownNode: '未命名节点', @@ -246,6 +253,7 @@ export const i18n = { recipeDetailPositivePrompt: '正向提示词', recipeDetailNegativePrompt: '负面提示词', recipeDetailNoPrompts: '此配方没有可安全展示的提示词摘要。', + recipeDetailVolatileIgnored: '运行时变化,已忽略', recipeDetailExpandValue: '展开完整内容', recipeDetailCollapseValue: '收起内容', recipeIdentityVerified: '身份已核验', @@ -501,7 +509,7 @@ export const i18n = { recipeGalleryRecipeValue: 'Recipe', recipeGalleryImageValue: 'Image', recipeDetailModels: 'Models & reproducibility', - recipeDetailParameters: 'Parameters', + recipeDetailParameters: 'Parameter Notebook', recipeDetailParametersHint: 'This view contains the saved parameters. Long values can be expanded and copied. The parameter gallery matches values, not local paths.', recipeParameterSnapshots: 'Parameter snapshots', recipeParameterSnapshotsHint: 'Each recipe save creates one read-only snapshot.', @@ -510,6 +518,13 @@ export const i18n = { recipeParameterLoadError: 'Could not load parameter snapshots.', recipeParameterNoSnapshots: 'This recipe has no parameter snapshots yet.', recipeParameterUntitled: 'Untitled parameter snapshot', + recipeParameterNew: 'New parameter note', + recipeParameterNewHint: 'Edit node parameters from the current recipe skeleton. Runtime-changing values such as seeds are ignored. Saving creates a new read-only snapshot and never overwrites history.', + recipeParameterName: 'Note name', + recipeParameterSave: 'Save parameter note', + recipeParameterCancel: 'Cancel editing', + recipeParameterSaveError: 'Could not save the parameter note.', + recipeParameterSensitiveHidden: 'Sensitive widget hidden', recipeDetailParameterSummary: 'Common parameters', recipeDetailNodeParameters: 'Node parameters', recipeDetailUnknownNode: 'Unnamed node', @@ -554,6 +569,7 @@ export const i18n = { recipeDetailPositivePrompt: 'Positive prompt', recipeDetailNegativePrompt: 'Negative prompt', recipeDetailNoPrompts: 'This recipe has no safe prompt summary to show.', + recipeDetailVolatileIgnored: 'Runtime-changing value ignored', recipeDetailExpandValue: 'Show full value', recipeDetailCollapseValue: 'Collapse value', recipeIdentityVerified: 'Identity verified', diff --git a/web/modules/ui_recipe_detail.js b/web/modules/ui_recipe_detail.js index bd64e00..6df7a3e 100644 --- a/web/modules/ui_recipe_detail.js +++ b/web/modules/ui_recipe_detail.js @@ -9,6 +9,7 @@ import { } from './recipe_identity.js'; import { buildRecipeDiff, diffIsEmpty } from './recipe_diff.js'; import { appendRecipeToCanvas } from './recipe_actions.js'; +import { applyRecipeWidgetChanges } from './recipe_parser.js'; const t = (key) => { let lang = window.anomalous_browser_lang || 'zh'; @@ -207,12 +208,12 @@ function needsExpansion(value) { return text.length > 260 || text.split(/\r?\n/).length > 3; } -function appendValueViewer(parent, value, className = '') { +function appendValueViewer(parent, value, className = '', options = {}) { const text = displayValue(value); const viewer = document.createElement('div'); viewer.className = `anomalous-recipe-detail-value-viewer${className ? ` ${className}` : ''}`; const code = appendText(viewer, 'code', text, 'anomalous-recipe-detail-full-value'); - if (needsExpansion(text)) { + if (options.collapse !== false && needsExpansion(text)) { code.classList.add('is-collapsed'); const toggle = button(viewer, t('recipeDetailExpandValue'), 'anomalous-recipe-detail-value-toggle'); toggle.onclick = () => { @@ -220,7 +221,7 @@ function appendValueViewer(parent, value, className = '') { toggle.textContent = expanded ? t('recipeDetailCollapseValue') : t('recipeDetailExpandValue'); }; } - appendCopyButton(viewer, text); + if (options.copy !== false) appendCopyButton(viewer, text); parent.appendChild(viewer); return viewer; } @@ -1296,19 +1297,175 @@ function parameterNodeOrder(recipe) { return result; } -function renderParameterField(parent, label, value) { +function isVolatileParameter(node, widget, index) { + const widgetName = String(widget?.name || '').toLowerCase(); + if (/(^|[_\s-])(seed|noise_seed|random_seed|variation_seed|last_seed)([_\s-]|$)/i.test(widgetName)) return true; + const nodeType = String(node?.type || '').toLowerCase(); + if (nodeType === 'ksampler') return index === 0; + if (nodeType === 'ksampleradvanced') return index === 1; + return false; +} + +function renderParameterField(parent, label, value, options = {}) { if (value === undefined || value === null || value === '') return false; const row = document.createElement('div'); row.className = 'anomalous-recipe-detail-parameter-row'; + const text = displayValue(value); + if (options.wide || Array.isArray(value) || typeof value === 'object' || text.length > 90) { + row.classList.add('is-wide'); + } appendText(row, 'span', label, 'anomalous-recipe-detail-label'); - appendValueViewer(row, value); + appendValueViewer( + row, + options.redact ? t('recipeDetailVolatileIgnored') : value, + '', + { collapse: options.collapse !== false, copy: options.copy !== false && !options.redact }, + ); parent.appendChild(row); return true; } +function cloneJson(value) { + try { return JSON.parse(JSON.stringify(value)); } catch (error) { return null; } +} + +function editorValueText(value) { + if (typeof value === 'string') return value; + if (value === undefined) return ''; + try { return JSON.stringify(value); } catch (error) { return String(value); } +} + +function parseEditorValue(raw, original) { + if (typeof original === 'number') { + const value = Number(raw); + if (!Number.isFinite(value)) throw new Error('invalid number'); + return value; + } + if (typeof original === 'boolean') { + if (raw !== 'true' && raw !== 'false') throw new Error('invalid boolean'); + return raw === 'true'; + } + if (original !== null && typeof original === 'object') return JSON.parse(raw); + return raw; +} + +function renderParameterNotebookEditor(wrapper, owner, recipe, parameterState, source, selectParameterTab) { + const editorState = parameterState.editor; + editorState.draft.params = editorState.draft.params || {}; + const editor = document.createElement('section'); + editor.className = 'anomalous-recipe-detail-section anomalous-recipe-parameter-editor'; + const heading = document.createElement('div'); + heading.className = 'anomalous-recipe-detail-section-heading'; + appendText(heading, 'h4', t('recipeParameterNew')); + const actions = document.createElement('div'); + const cancel = button(actions, t('recipeParameterCancel'), 'anomalous-btn-ghost'); + cancel.onclick = () => { + parameterState.editor = null; + selectParameterTab?.(); + }; + const save = button(actions, t('recipeParameterSave'), 'anomalous-btn-primary'); + save.onclick = async () => { + const name = nameInput.value.trim() || recipe.name || t('recipeParameterUntitled'); + save.disabled = true; + try { + const response = await fetch('/anomalous/save_parameter', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name, + tags: recipe.tags || [], + notes: recipe.notes || '', + params: editorState.draft.params || {}, + workflow: editorState.draft.workflow, + recipe_filename: owner.recipeDetailFilename, + }), + }); + if (!response.ok) throw new Error('parameter note save failed'); + parameterState.editor = null; + parameterState.status = 'idle'; + parameterState.selectedFilename = null; + await parameterState.refresh?.(true); + selectParameterTab?.(); + } catch (error) { + console.error('Could not save the new parameter notebook:', error); + await anomalousAlert(t('recipeParameterSaveError')); + save.disabled = false; + } + }; + actions.append(cancel, save); + heading.appendChild(actions); + editor.appendChild(heading); + appendText(editor, 'p', t('recipeParameterNewHint'), 'anomalous-recipe-detail-muted'); + + const nameRow = document.createElement('label'); + nameRow.className = 'anomalous-recipe-parameter-editor-name'; + appendText(nameRow, 'span', t('recipeParameterName')); + const nameInput = document.createElement('input'); + nameInput.type = 'text'; + nameInput.maxLength = 200; + nameInput.value = editorState.name || `${recipe.name || t('recipeUntitled')} · ${t('recipeParameterNew')}`; + nameRow.appendChild(nameInput); + editor.appendChild(nameRow); + + const nodeList = document.createElement('div'); + nodeList.className = 'anomalous-recipe-detail-parameter-list anomalous-recipe-parameter-editor-list'; + let rendered = 0; + for (const { summary: node, workflowNode } of parameterNodeOrder(editorState.draft)) { + if (!workflowNode || !Array.isArray(workflowNode.widgets_values)) continue; + const widgets = Array.isArray(node?.widgets) && node.widgets.length + ? node.widgets + : 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')); + for (let visibleIndex = 0; visibleIndex < widgets.length && rendered < 1200; visibleIndex += 1) { + const widget = widgets[visibleIndex] || {}; + const index = Number.isInteger(widget.index) ? widget.index : visibleIndex; + if (index < 0 || index >= workflowNode.widgets_values.length) continue; + 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'); + 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) { + appendValueViewer(row, t(volatile ? 'recipeDetailVolatileIgnored' : 'recipeParameterSensitiveHidden'), '', { copy: false }); + } else { + const input = document.createElement(typeof value === 'string' && (value.length > 100 || /text|prompt/i.test(String(widget.name || ''))) ? 'textarea' : 'input'); + input.className = 'anomalous-recipe-parameter-editor-input'; + input.value = editorValueText(value); + if (input.tagName === 'TEXTAREA') input.rows = Math.min(8, Math.max(3, input.value.split(/\r?\n/).length)); + input.onchange = () => { + try { + const next = parseEditorValue(input.value, value); + applyRecipeWidgetChanges(editorState.draft.params || {}, editorState.draft.workflow, [{ + nodeId: workflowNode.id, + widgetIndex: index, + value: next, + previousValue: value, + }]); + workflowNode.widgets_values[index] = next; + input.classList.remove('is-invalid'); + } catch (error) { + input.classList.add('is-invalid'); + } + }; + row.appendChild(input); + } + block.appendChild(row); + rendered += 1; + } + if (block.querySelector('.anomalous-recipe-parameter-editor-row')) nodeList.appendChild(block); + } + if (!nodeList.childElementCount) appendText(nodeList, 'p', t('recipeDetailNoSavedParameters'), 'anomalous-recipe-detail-muted'); + editor.appendChild(nodeList); + wrapper.appendChild(editor); +} + function renderRecipeParameters(content, owner, recipe, gallery, refreshGallery, parameterState, selectParameterTab) { const selectedNotebook = parameterState?.notebooks?.find((item) => item.filename === parameterState.selectedFilename); - const source = selectedNotebook?.data?.workflow ? selectedNotebook.data : recipe; + const baseSource = selectedNotebook?.data?.workflow ? selectedNotebook.data : recipe; + const source = parameterState?.editor?.draft || baseSource; const wrapper = document.createElement('div'); wrapper.className = 'anomalous-recipe-detail-parameters'; @@ -1319,6 +1476,23 @@ function renderRecipeParameters(content, owner, recipe, gallery, refreshGallery, const sidebarHeading = document.createElement('div'); sidebarHeading.className = 'anomalous-recipe-detail-section-heading'; appendText(sidebarHeading, 'strong', t('recipeParameterSnapshots')); + const newSnapshot = button(sidebarHeading, 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 refreshSnapshots = button(sidebarHeading, t('recipeParameterRefresh'), 'anomalous-btn-ghost'); refreshSnapshots.onclick = () => { void parameterState.refresh?.(true); }; sidebar.appendChild(sidebarHeading); @@ -1337,6 +1511,7 @@ function renderRecipeParameters(content, owner, recipe, gallery, refreshGallery, appendText(item, 'small', dateText(notebook.timestamp), 'anomalous-recipe-detail-muted'); item.onclick = () => { if (parameterState.selectedFilename === notebook.filename) return; + parameterState.editor = null; parameterState.selectedFilename = notebook.filename; gallery.status = 'idle'; gallery.images = []; @@ -1349,6 +1524,13 @@ function renderRecipeParameters(content, owner, recipe, gallery, refreshGallery, } sidebar.appendChild(snapshotList); + if (parameterState?.editor) { + renderParameterNotebookEditor(wrapper, owner, recipe, parameterState, source, selectParameterTab); + layout.append(sidebar, wrapper); + content.appendChild(layout); + return; + } + const intro = document.createElement('section'); intro.className = 'anomalous-recipe-detail-section'; appendText(intro, 'h4', t('recipeDetailParameters')); @@ -1385,17 +1567,17 @@ function renderRecipeParameters(content, owner, recipe, gallery, refreshGallery, const summaryGrid = document.createElement('div'); summaryGrid.className = 'anomalous-recipe-detail-summary-grid'; const scalarFields = [ - ['recipeDetailSampler', params.sampler_name || params.samplers], - ['recipeDetailScheduler', params.scheduler], - ['recipeDetailSteps', params.steps], - ['recipeDetailCFG', params.cfg], - ['recipeDetailDenoise', params.denoise], - ['recipeDetailSeed', params.seed], - ['recipeDetailResolution', params.resolution], - ['recipeDetailBaseModel', params.baseModel || params.baseModels], - ['recipeDetailLoraSummary', params.loras], + ['recipeDetailSampler', params.sampler_name || params.samplers, {}], + ['recipeDetailScheduler', params.scheduler, {}], + ['recipeDetailSteps', params.steps, {}], + ['recipeDetailCFG', params.cfg, {}], + ['recipeDetailDenoise', params.denoise, {}], + ['recipeDetailSeed', params.seed ?? 0, { redact: true, copy: false }], + ['recipeDetailResolution', params.resolution, { wide: true }], + ['recipeDetailBaseModel', params.baseModel || params.baseModels, { wide: true }], + ['recipeDetailLoraSummary', params.loras, { wide: true }], ]; - for (const [labelKey, value] of scalarFields) renderParameterField(summaryGrid, t(labelKey), value); + for (const [labelKey, value, options] of scalarFields) renderParameterField(summaryGrid, t(labelKey), value, { ...options, collapse: false }); if (summaryGrid.childElementCount) summary.appendChild(summaryGrid); else appendText(summary, 'p', t('recipeDetailNoSavedParameters'), 'anomalous-recipe-detail-muted'); @@ -1427,7 +1609,12 @@ function renderRecipeParameters(content, owner, recipe, gallery, refreshGallery, ? workflowNode.widgets_values[index] : widget.value; const label = widget.name || `${t('recipeDetailWidget')} ${index + 1}`; - if (renderParameterField(block, label, value)) renderedWidgets += 1; + const volatile = isVolatileParameter(node, widget, index); + if (renderParameterField(block, label, volatile ? 0 : value, { + redact: volatile, + copy: false, + collapse: false, + })) renderedWidgets += 1; } if (block.querySelector('.anomalous-recipe-detail-parameter-row')) nodeList.appendChild(block); } diff --git a/web/styles.css b/web/styles.css index e4ddf86..63a261e 100644 --- a/web/styles.css +++ b/web/styles.css @@ -1920,6 +1920,10 @@ gap: 12px; } +.anomalous-recipe-detail-summary-grid > .is-wide { + grid-column: 1 / -1; +} + .anomalous-recipe-detail-summary-grid .anomalous-recipe-detail-label { padding-top: 6px; white-space: nowrap; @@ -1930,9 +1934,8 @@ } .anomalous-recipe-detail-summary-grid .anomalous-recipe-detail-full-value { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + overflow-wrap: anywhere; + white-space: pre-wrap; } .anomalous-recipe-detail-section { @@ -2161,6 +2164,49 @@ min-width: 0; } +.anomalous-recipe-parameter-editor { + min-width: 0; +} + +.anomalous-recipe-parameter-editor-name { + display: grid; + grid-template-columns: max-content minmax(0, 1fr); + gap: 10px; + align-items: center; + margin-bottom: 12px; + color: #bbb; +} + +.anomalous-recipe-parameter-editor-name input, +.anomalous-recipe-parameter-editor-input { + min-width: 0; + padding: 8px 10px; + border: 1px solid #4a4a4a; + border-radius: 7px; + color: #eee; + background: #181818; + font: inherit; +} + +.anomalous-recipe-parameter-editor-input { + width: 100%; + resize: vertical; +} + +.anomalous-recipe-parameter-editor-input:focus, +.anomalous-recipe-parameter-editor-name input:focus { + border-color: rgba(110, 231, 183, 0.7); + outline: none; +} + +.anomalous-recipe-parameter-editor-input.is-invalid { + border-color: rgba(255, 119, 119, 0.9); +} + +.anomalous-recipe-parameter-editor-row { + grid-template-columns: minmax(120px, 0.28fr) minmax(0, 1fr); +} + .anomalous-recipe-model-reference, .anomalous-recipe-detail-parameter-node, .anomalous-recipe-version-row { @@ -2479,6 +2525,7 @@ .anomalous-recipe-model-preview { height: 110px; flex-basis: 110px; } .anomalous-recipe-parameter-notebook-layout { grid-template-columns: 1fr; } .anomalous-recipe-parameter-notebook-sidebar { position: static; max-height: none; } + .anomalous-recipe-parameter-editor-name { grid-template-columns: 1fr; gap: 5px; } } @media (max-width: 560px) {