feat: compact + Negative, equal split when present, auto-grow prompts (#264)
Prompt editors now give positive the full width when no negative is set, with a compact '+ Negative' control; adding a negative splits the row 50/50. Flake and preset prompt fields auto-grow to fit their content (new attachAutoGrow helper, capped with scroll). Variant-choice editors get the narrow button + equal split while keeping their image-matched height.
This commit is contained in:
+27
-10
@@ -3,7 +3,7 @@ import {
|
||||
css, makeButton, makeSmallButton, makeComfyLabel, makeComfyInput,
|
||||
makeComfyDropdown, makePanelDropdown, makeSearchableDropdown,
|
||||
makeComfyNumberInput, makeComfyValueSlider, makeSmallValueSlider,
|
||||
makeTextarea, makeLabel, makeNumberInput,
|
||||
makeTextarea, makeLabel, makeNumberInput, attachAutoGrow,
|
||||
familyFolder, makeHoverRemoveWrapper, CN_MODEL_MAP,
|
||||
} from "./utils.js";
|
||||
import {
|
||||
@@ -801,22 +801,32 @@ if (!activeFields.includes("controlnets") && fieldState.controlnets._.length > 0
|
||||
fieldWrap.appendChild(promptBox);
|
||||
|
||||
const posCol = document.createElement("div");
|
||||
css(posCol, "flex:0 0 70%;display:flex;flex-direction:column;gap:4px;min-width:0;");
|
||||
css(posCol, "display:flex;flex-direction:column;gap:4px;min-width:0;");
|
||||
promptBox.appendChild(posCol);
|
||||
|
||||
const negCol = document.createElement("div");
|
||||
css(negCol, "flex:0 0 30%;display:flex;flex-direction:column;gap:4px;min-width:0;");
|
||||
css(negCol, "display:flex;flex-direction:column;gap:4px;min-width:0;");
|
||||
promptBox.appendChild(negCol);
|
||||
|
||||
const taCss = "background:#1a1a1a;color:#ddd;border:1px solid #333;padding:6px;border-radius:4px;font-size:12px;width:100%;box-sizing:border-box;font-family:inherit;outline:none;";
|
||||
|
||||
function renderPrompts() {
|
||||
posCol.replaceChildren();
|
||||
negCol.replaceChildren();
|
||||
|
||||
const hasNeg = fieldState.prompt?.negative != null;
|
||||
// Positive spans the width when alone; equal split with a
|
||||
// negative present. The "+ Negative" placeholder stays
|
||||
// compact so it never steals the positive's space (#264).
|
||||
posCol.style.flex = hasNeg ? "1 1 0" : "1 1 auto";
|
||||
negCol.style.flex = hasNeg ? "1 1 0" : "0 0 auto";
|
||||
|
||||
if (fieldState.prompt?.positive != null) {
|
||||
const posTA = makeTextarea(fieldState.prompt.positive, "positive prompt", 3);
|
||||
css(posTA, "background:#1a1a1a;color:#ddd;border:1px solid #333;padding:6px;border-radius:4px;font-size:12px;width:100%;box-sizing:border-box;font-family:inherit;resize:vertical;outline:none;");
|
||||
css(posTA, taCss);
|
||||
posTA.addEventListener("change", () => { fieldState.prompt.positive = posTA.value; });
|
||||
posTA.addEventListener("input", () => { fieldState.prompt.positive = posTA.value; });
|
||||
attachAutoGrow(posTA);
|
||||
const posWrap = makeHoverRemoveWrapper(posTA, () => {
|
||||
if (fieldState.prompt.negative == null) {
|
||||
fieldState.prompt = null;
|
||||
@@ -842,11 +852,12 @@ if (!activeFields.includes("controlnets") && fieldState.controlnets._.length > 0
|
||||
posCol.appendChild(posBtn);
|
||||
}
|
||||
|
||||
if (fieldState.prompt?.negative != null) {
|
||||
if (hasNeg) {
|
||||
const negTA = makeTextarea(fieldState.prompt.negative, "negative prompt", 2);
|
||||
css(negTA, "background:#1a1a1a;color:#ddd;border:1px solid #333;padding:6px;border-radius:4px;font-size:12px;width:100%;box-sizing:border-box;font-family:inherit;resize:vertical;outline:none;");
|
||||
css(negTA, taCss);
|
||||
negTA.addEventListener("change", () => { fieldState.prompt.negative = negTA.value; });
|
||||
negTA.addEventListener("input", () => { fieldState.prompt.negative = negTA.value; });
|
||||
attachAutoGrow(negTA);
|
||||
const negWrap = makeHoverRemoveWrapper(negTA, () => {
|
||||
if (fieldState.prompt.positive == null) {
|
||||
fieldState.prompt = null;
|
||||
@@ -861,7 +872,7 @@ if (!activeFields.includes("controlnets") && fieldState.controlnets._.length > 0
|
||||
} else {
|
||||
const negBtn = document.createElement("button");
|
||||
negBtn.textContent = "+ Negative";
|
||||
css(negBtn, "flex:1;min-height:60px;cursor:pointer;border-radius:4px;font-size:13px;background:#2a2a2a;color:#999;border:1px dashed #555;transition:background 0.15s ease;display:flex;align-items:center;justify-content:center;gap:3px;user-select:none;box-sizing:border-box;white-space:nowrap;padding:0 6px;");
|
||||
css(negBtn, "align-self:flex-start;cursor:pointer;border-radius:4px;font-size:13px;background:#2a2a2a;color:#999;border:1px dashed #555;transition:background 0.15s ease;user-select:none;box-sizing:border-box;white-space:nowrap;padding:6px 10px;");
|
||||
negBtn.addEventListener("mouseenter", () => { negBtn.style.background = "#333"; });
|
||||
negBtn.addEventListener("mouseleave", () => { negBtn.style.background = "#2a2a2a"; });
|
||||
negBtn.addEventListener("click", () => {
|
||||
@@ -1266,11 +1277,11 @@ if (!activeFields.includes("controlnets") && fieldState.controlnets._.length > 0
|
||||
leftCol.appendChild(promptsWrap);
|
||||
|
||||
const choicePosCol = document.createElement("div");
|
||||
css(choicePosCol, "flex:0 0 70%;display:flex;flex-direction:column;gap:4px;min-width:0;min-height:0;");
|
||||
css(choicePosCol, "display:flex;flex-direction:column;gap:4px;min-width:0;min-height:0;");
|
||||
promptsWrap.appendChild(choicePosCol);
|
||||
|
||||
const choiceNegCol = document.createElement("div");
|
||||
css(choiceNegCol, "flex:0 0 30%;display:flex;flex-direction:column;gap:4px;min-width:0;min-height:0;");
|
||||
css(choiceNegCol, "display:flex;flex-direction:column;gap:4px;min-width:0;min-height:0;");
|
||||
promptsWrap.appendChild(choiceNegCol);
|
||||
|
||||
const rightCol = document.createElement("div");
|
||||
@@ -1406,6 +1417,12 @@ if (!activeFields.includes("controlnets") && fieldState.controlnets._.length > 0
|
||||
choicePosCol.replaceChildren();
|
||||
choiceNegCol.replaceChildren();
|
||||
|
||||
// Positive spans the width when alone; equal
|
||||
// split with a compact "+ Negative" otherwise (#264).
|
||||
const hasNeg = choice.negative != null;
|
||||
choicePosCol.style.flex = hasNeg ? "1 1 0" : "1 1 auto";
|
||||
choiceNegCol.style.flex = hasNeg ? "1 1 0" : "0 0 auto";
|
||||
|
||||
if (choice.positive != null) {
|
||||
const cPos = makeTextarea(choice.positive || "", "extra positive", 2);
|
||||
// Fill column height (the column matches the image height via align-items:stretch on bodyRow).
|
||||
@@ -1450,7 +1467,7 @@ if (!activeFields.includes("controlnets") && fieldState.controlnets._.length > 0
|
||||
} else {
|
||||
const negBtn = document.createElement("button");
|
||||
negBtn.textContent = "+ Negative";
|
||||
css(negBtn, "flex:1;min-height:50px;cursor:pointer;border-radius:4px;font-size:13px;background:#2a2a2a;color:#999;border:1px dashed #555;transition:background 0.15s ease;display:flex;align-items:center;justify-content:center;gap:3px;user-select:none;box-sizing:border-box;white-space:nowrap;padding:0 6px;");
|
||||
css(negBtn, "align-self:flex-start;cursor:pointer;border-radius:4px;font-size:13px;background:#2a2a2a;color:#999;border:1px dashed #555;transition:background 0.15s ease;user-select:none;box-sizing:border-box;white-space:nowrap;padding:6px 10px;");
|
||||
negBtn.addEventListener("mouseenter", () => { negBtn.style.background = "#333"; });
|
||||
negBtn.addEventListener("mouseleave", () => { negBtn.style.background = "#2a2a2a"; });
|
||||
negBtn.addEventListener("click", () => {
|
||||
|
||||
+14
-7
@@ -2,7 +2,7 @@ import { openOverlay } from "./modal.js";
|
||||
import {
|
||||
css, makeButton, makeComfyLabel, makeComfyInput,
|
||||
makeComfyDropdown, makeSearchableDropdown, makeComfySlider,
|
||||
makeTextarea, makeHoverRemoveWrapper,
|
||||
makeTextarea, makeHoverRemoveWrapper, attachAutoGrow,
|
||||
} from "./utils.js";
|
||||
import { fetchPreset, fetchCheckpoints, fetchVaes, fetchEmbeddings } from "./api.js";
|
||||
import { openFileBrowser } from "./pickers.js";
|
||||
@@ -460,19 +460,20 @@ pathWrap.appendChild(makeComfyLabel("Output path"));
|
||||
css(promptRow, "display:flex;gap:8px;align-items:stretch;");
|
||||
content.appendChild(promptRow);
|
||||
const posCol = document.createElement("div");
|
||||
css(posCol, "flex:0 0 70%;min-width:0;display:flex;flex-direction:column;gap:4px;");
|
||||
css(posCol, "min-width:0;display:flex;flex-direction:column;gap:4px;");
|
||||
promptRow.appendChild(posCol);
|
||||
const negCol = document.createElement("div");
|
||||
css(negCol, "flex:0 0 30%;min-width:0;display:flex;flex-direction:column;gap:4px;");
|
||||
css(negCol, "min-width:0;display:flex;flex-direction:column;gap:4px;");
|
||||
promptRow.appendChild(negCol);
|
||||
|
||||
const posLabel = makeComfyLabel("Positive prompt");
|
||||
posCol.appendChild(posLabel);
|
||||
const posTA = makeTextarea(promptState.positive, "masterpiece, best quality", 3);
|
||||
css(posTA, "background:#1a1a1a;color:#ddd;border:1px solid #333;padding:8px;border-radius:6px;font-size:13px;width:100%;box-sizing:border-box;font-family:inherit;resize:vertical;outline:none;min-height:78px;flex-shrink:0;");
|
||||
css(posTA, "background:#1a1a1a;color:#ddd;border:1px solid #333;padding:8px;border-radius:6px;font-size:13px;width:100%;box-sizing:border-box;font-family:inherit;outline:none;min-height:78px;flex-shrink:0;");
|
||||
posTA.addEventListener("focus", () => posTA.style.borderColor = "#555");
|
||||
posTA.addEventListener("blur", () => posTA.style.borderColor = "#333");
|
||||
posTA.addEventListener("input", () => { promptState.positive = posTA.value; });
|
||||
attachAutoGrow(posTA);
|
||||
// Positive prompt is required, but we still wrap it so the styling is
|
||||
// consistent with the negative side; no remove button needed though, so
|
||||
// hover-X helper isn't applied here — only on negative.
|
||||
@@ -480,13 +481,19 @@ pathWrap.appendChild(makeComfyLabel("Output path"));
|
||||
|
||||
function renderNegative() {
|
||||
negCol.replaceChildren();
|
||||
if (promptState.negative != null) {
|
||||
const hasNeg = promptState.negative != null;
|
||||
// Equal split when a negative is present; positive spans the width
|
||||
// and the "+ Negative" stays compact when it's absent (#264).
|
||||
posCol.style.flex = hasNeg ? "1 1 0" : "1 1 auto";
|
||||
negCol.style.flex = hasNeg ? "1 1 0" : "0 0 auto";
|
||||
if (hasNeg) {
|
||||
negCol.appendChild(makeComfyLabel("Negative prompt"));
|
||||
const negTA = makeTextarea(promptState.negative, "worst quality, low quality", 3);
|
||||
css(negTA, "background:#1a1a1a;color:#ddd;border:1px solid #333;padding:8px;border-radius:6px;font-size:13px;width:100%;box-sizing:border-box;font-family:inherit;resize:vertical;outline:none;min-height:78px;flex-shrink:0;");
|
||||
css(negTA, "background:#1a1a1a;color:#ddd;border:1px solid #333;padding:8px;border-radius:6px;font-size:13px;width:100%;box-sizing:border-box;font-family:inherit;outline:none;min-height:78px;flex-shrink:0;");
|
||||
negTA.addEventListener("focus", () => negTA.style.borderColor = "#555");
|
||||
negTA.addEventListener("blur", () => negTA.style.borderColor = "#333");
|
||||
negTA.addEventListener("input", () => { promptState.negative = negTA.value; });
|
||||
attachAutoGrow(negTA);
|
||||
const negWrap = makeHoverRemoveWrapper(negTA, () => {
|
||||
promptState.negative = null;
|
||||
renderNegative();
|
||||
@@ -498,7 +505,7 @@ pathWrap.appendChild(makeComfyLabel("Output path"));
|
||||
const negBtn = document.createElement("button");
|
||||
negBtn.type = "button";
|
||||
negBtn.textContent = "+ Negative";
|
||||
css(negBtn, "flex:1;min-height:78px;cursor:pointer;border-radius:6px;font-size:13px;background:#2a2a2a;color:#999;border:1px dashed #555;transition:background 0.15s ease;display:flex;align-items:center;justify-content:center;user-select:none;");
|
||||
css(negBtn, "align-self:flex-start;cursor:pointer;border-radius:6px;font-size:13px;background:#2a2a2a;color:#999;border:1px dashed #555;transition:background 0.15s ease;user-select:none;white-space:nowrap;padding:8px 12px;");
|
||||
negBtn.addEventListener("mouseenter", () => { negBtn.style.background = "#333"; });
|
||||
negBtn.addEventListener("mouseleave", () => { negBtn.style.background = "#2a2a2a"; });
|
||||
negBtn.addEventListener("click", () => {
|
||||
|
||||
@@ -302,6 +302,23 @@ export function makeTextarea(value = "", placeholder = "", rows = 3) {
|
||||
return el;
|
||||
}
|
||||
|
||||
// Grow a textarea to fit its content up to maxPx, then scroll (#264). Returns
|
||||
// the resize fn so callers can trigger it after programmatic value changes.
|
||||
export function attachAutoGrow(el, maxPx = 240) {
|
||||
el.style.resize = "none";
|
||||
el.style.overflowY = "hidden";
|
||||
const resize = () => {
|
||||
el.style.height = "auto";
|
||||
const h = Math.min(el.scrollHeight, maxPx);
|
||||
el.style.height = `${h}px`;
|
||||
el.style.overflowY = el.scrollHeight > maxPx ? "auto" : "hidden";
|
||||
};
|
||||
el.addEventListener("input", resize);
|
||||
// scrollHeight needs layout — size on the next frame once in the DOM.
|
||||
requestAnimationFrame(resize);
|
||||
return resize;
|
||||
}
|
||||
|
||||
export function makeLabel(text) {
|
||||
const l = document.createElement("div");
|
||||
l.textContent = text;
|
||||
|
||||
Reference in New Issue
Block a user