Improve editor UI parity and centering

This commit is contained in:
bbc-s
2026-06-10 18:32:37 +02:00
parent 7b40544dd0
commit b18ac237fd
+76 -10
View File
@@ -71,16 +71,18 @@ function injectStyle() {
const s = document.createElement("style");
s.id = "kjideo-style";
s.textContent = `
.kjideo-wrap { display:flex; flex-direction:column; overflow:hidden; position:relative; pointer-events:auto; gap:4px; min-width:0; }
.kjideo-wrap { display:flex; flex-direction:column; overflow:hidden; position:relative; pointer-events:auto; gap:4px; min-width:0; align-items:stretch; }
.kjideo-canvas { cursor:crosshair; display:block; max-width:100%; width:100%; height:auto; flex:0 0 auto; align-self:center; background:#1a1a1a; border-radius:4px; outline:none; object-fit:contain; }
.kjideo-bar { display:flex; align-items:center; gap:6px; flex-wrap:wrap; font:11px sans-serif; color:#aaa; user-select:none; padding:0 2px; flex:0 0 auto; min-width:0; }
.kjideo-bar span { min-width:0; overflow:hidden; text-overflow:ellipsis; }
.kjideo-actions { display:flex; align-items:center; justify-content:flex-end; gap:6px; flex-wrap:wrap; flex:0 1 auto; min-width:148px; margin-left:auto; }
.kjideo-panel { display:flex; flex-direction:column; gap:5px; padding:6px; background:#262626; border-radius:4px; font:11px sans-serif; color:#bbb; flex:0 0 auto; min-width:0; }
.kjideo-row { display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
.kjideo-btn { background:#333; border:1px solid #555; border-radius:4px; color:#bbb; font:11px sans-serif; cursor:pointer; padding:2px 8px; line-height:16px; white-space:nowrap; flex-shrink:0; }
.kjideo-btn:hover { border-color:#46b4e6; color:#fff; }
.kjideo-btn.active { border-color:#46b4e6; color:#46b4e6; background:#2a3a42; }
.kjideo-area { width:100%; box-sizing:border-box; background:#1d1d1d; border:1px solid #444; border-radius:4px; color:#ddd; font:13px monospace; padding:4px 6px; resize:none; min-height:36px; max-height:96px; overflow:auto; }
.kjideo-label { color:#aaa; font:11px sans-serif; line-height:13px; }
.kjideo-area { width:100%; box-sizing:border-box; background:#1d1d1d; border:1px solid #444; border-radius:4px; color:#ddd; font:13px monospace; padding:4px 6px; resize:vertical; min-height:36px; max-height:160px; overflow:auto; }
.kjideo-sw { width:20px; height:20px; border:1px solid #666; border-radius:3px; cursor:pointer; flex-shrink:0; position:relative; transition:transform .18s ease, box-shadow .12s ease, opacity .12s ease; }
.kjideo-sw:hover { transform:scale(1.2); box-shadow:0 0 0 2px #46b4e6; z-index:3; }
.kjideo-sw.dragging { opacity:.4; box-shadow:0 0 0 2px #46b4e6; }
@@ -118,10 +120,12 @@ app.registerExtension({
const stylePaletteWidget = null;
const bgBrightnessWidget = findW("bg_brightness");
if (bgBrightnessWidget && typeof bgBrightnessWidget.value !== "number") bgBrightnessWidget.value = 25;
const globalPromptWidget = findW("global_prompt");
const globalNegativeWidget = findW("global_negative_prompt");
const wWidget = findW("width"), hWidget = findW("height");
// Hide the data widgets while keeping them serializable.
function hideDataWidgets() {
for (const w of [elementsWidget, bgBrightnessWidget]) {
for (const w of [elementsWidget, bgBrightnessWidget, globalPromptWidget, globalNegativeWidget]) {
if (!w) continue;
w.hidden = true;
w.computeSize = () => [0, -4];
@@ -150,6 +154,8 @@ app.registerExtension({
node._lastImported = ""; // last import_json applied to the editor (avoid re-apply)
node._areaH = node._areaH || {};
node._areaObservers = [];
node._globalAreaH = node._globalAreaH || {};
node._globalAreaObservers = [];
// ── DOM ──
const wrap = document.createElement("div");
@@ -202,7 +208,10 @@ app.registerExtension({
bgSlider.style.cssText = "width:64px;flex:0 0 auto;";
stopProp(bgSlider);
bgSlider.addEventListener("input", () => { if (bgBrightnessWidget) bgBrightnessWidget.value = parseInt(bgSlider.value, 10); drawCanvas(); });
bar.appendChild(hint); bar.appendChild(liveLabel); bar.appendChild(grabBtn); bar.appendChild(bgSlider); bar.appendChild(tokenSpan); bar.appendChild(copyBtn); bar.appendChild(importBtn); bar.appendChild(clearBtn);
const actions = document.createElement("div");
actions.className = "kjideo-actions";
actions.append(liveLabel, grabBtn, bgSlider, tokenSpan, copyBtn, importBtn, clearBtn);
bar.append(hint, actions);
updateGrabBtn();
// Persistent global style-palette row
@@ -222,11 +231,14 @@ app.registerExtension({
addWheelPassthrough(wrap);
addMiddleClickPan(canvasEl);
const globalPanel = document.createElement("div");
globalPanel.className = "kjideo-panel";
const panel = document.createElement("div");
panel.className = "kjideo-panel";
// Canvas above panel so the panel grows downward without shifting the canvas.
wrap.appendChild(bar); wrap.appendChild(styleBar); wrap.appendChild(canvasEl); wrap.appendChild(panel);
wrap.appendChild(bar); wrap.appendChild(styleBar); wrap.appendChild(globalPanel); wrap.appendChild(canvasEl); wrap.appendChild(panel);
const TOOLBAR_H = 44;
const MIN_EDITOR_H = 420;
@@ -259,6 +271,7 @@ app.registerExtension({
const widgetH = Math.min(MAX_EDITOR_H, Math.max(MIN_EDITOR_H, node.size?.[1] || DEFAULT_EDITOR_H));
const reservedH = (bar.offsetHeight || TOOLBAR_H)
+ (styleBar.offsetHeight || 0)
+ (globalPanel.offsetHeight || 96)
+ (panel.offsetHeight || 132)
+ 30;
const maxCanvasW = Math.max(120, nodeW - 30);
@@ -271,6 +284,8 @@ app.registerExtension({
}
canvasEl.style.width = `${Math.round(displayW)}px`;
canvasEl.style.height = `${Math.round(displayH)}px`;
canvasEl.style.marginLeft = "auto";
canvasEl.style.marginRight = "auto";
}
function recalcWidgetHeight() {
@@ -434,6 +449,18 @@ app.registerExtension({
_draw();
});
}
function imageSize(img) {
return {
w: img?.naturalWidth || img?.videoWidth || img?.width || 1,
h: img?.naturalHeight || img?.videoHeight || img?.height || 1,
};
}
function drawImageContain(img, x, y, w, h) {
const s = imageSize(img);
const scale = Math.min(w / s.w, h / s.h);
const dw = s.w * scale, dh = s.h * scale;
ctx.drawImage(img, x + (w - dw) / 2, y + (h - dh) / 2, dw, dh);
}
function _draw() {
// Size the backing store to display Ă— DPR and draw in logical px (crisp text/lines).
const W = logW(), H = logH(), d = window.devicePixelRatio || 1;
@@ -444,7 +471,8 @@ app.registerExtension({
let bri = bgBrightnessWidget ? bgBrightnessWidget.value : 25;
if (typeof bri !== "number" || isNaN(bri)) bri = 25; // guard against unset widget value
if (node._bgImg) { // reference image, dimmed by brightness
ctx.drawImage(node._bgImg, 0, 0, W, H);
ctx.fillStyle = "#1a1a1a"; ctx.fillRect(0, 0, W, H);
drawImageContain(node._bgImg, 0, 0, W, H);
const dim = 1 - bri / 100;
if (dim > 0) { ctx.fillStyle = `rgba(0,0,0,${dim})`; ctx.fillRect(0, 0, W, H); }
} else { // blank canvas grey from brightness
@@ -899,6 +927,12 @@ app.registerExtension({
const w = findW(name);
return w ? w.value : "";
}
function setW(name, value) {
const w = findW(name);
if (!w) return;
w.value = value || "";
w.callback?.(w.value);
}
function buildCaption() { return JSON.stringify(buildZImageRegionPayload(), null, 2); }
function updateTokens() {
const txt = (getW("global_prompt") || "") + " " + node._boxes.map((b) => b.prompt || b.desc || "").join(" ");
@@ -917,6 +951,8 @@ app.registerExtension({
}
function loadCaption(cap) {
closeInlineEditor();
if (typeof cap.global_prompt === "string") setW("global_prompt", cap.global_prompt);
if (typeof cap.global_negative_prompt === "string") setW("global_negative_prompt", cap.global_negative_prompt);
const W = wWidget?.value || 1024, H = hWidget?.value || 1024;
node._boxes = (cap.regions || []).map((b, i) => {
const bb = b.bbox && typeof b.bbox === "object" ? b.bbox : null;
@@ -934,7 +970,7 @@ app.registerExtension({
};
});
node._activeIdx = node._boxes.length ? 0 : -1;
syncCanvasToDims(); commit(); fitNode();
syncCanvasToDims(); renderGlobalPanel(); commit(); fitNode();
}
async function doCopy() {
const txt = buildCaption();
@@ -1064,17 +1100,44 @@ app.registerExtension({
}
// Textarea whose user-dragged height persists across panel rebuilds / box switches.
function makeArea(field, value, placeholder, onInput, defaultH) {
function makeArea(field, value, placeholder, onInput, defaultH, heightStore = node._areaH, observers = node._areaObservers) {
const ta = document.createElement("textarea");
ta.className = "kjideo-area";
ta.placeholder = placeholder;
ta.value = value || "";
const h = defaultH;
const h = heightStore[field] || defaultH;
if (h) ta.style.height = h + "px";
stopProp(ta);
ta.addEventListener("input", onInput);
if (window.ResizeObserver) {
const ro = new ResizeObserver(() => {
const px = Math.round(ta.getBoundingClientRect().height);
if (px > 0) heightStore[field] = px;
scheduleFit();
});
ro.observe(ta);
observers.push(ro);
}
return ta;
}
function addAreaLabel(container, text) {
const label = document.createElement("div");
label.className = "kjideo-label";
label.textContent = text;
container.appendChild(label);
}
function renderGlobalPanel() {
for (const ro of node._globalAreaObservers) ro.disconnect();
node._globalAreaObservers = [];
globalPanel.innerHTML = "";
addAreaLabel(globalPanel, "positive prompt");
globalPanel.appendChild(makeArea("global_prompt", getW("global_prompt"), "global positive prompt",
function () { setW("global_prompt", this.value); updateTokens(); drawCanvas(); }, 64, node._globalAreaH, node._globalAreaObservers));
addAreaLabel(globalPanel, "negative prompt");
globalPanel.appendChild(makeArea("global_negative_prompt", getW("global_negative_prompt"), "global negative prompt",
function () { setW("global_negative_prompt", this.value); updateTokens(); }, 48, node._globalAreaH, node._globalAreaObservers));
scheduleFit();
}
function renderPanel() {
for (const ro of node._areaObservers) ro.disconnect();
node._areaObservers = [];
@@ -1092,8 +1155,10 @@ app.registerExtension({
const col = REGION_COLORS[node._activeIdx % REGION_COLORS.length];
hint.innerHTML = `<b style="color:${col}">region ${node._activeIdx + 1}</b> · dbl-click edit · alt-click overlap · del remove`;
addAreaLabel(panel, "region prompt");
panel.appendChild(makeArea("prompt", b.prompt || b.desc || "", "region prompt",
function () { b.prompt = this.value; b.desc = this.value; touch(); }, 92));
addAreaLabel(panel, "region negative prompt");
panel.appendChild(makeArea("negative_prompt", b.negative_prompt || "", "optional region negative prompt",
function () { b.negative_prompt = this.value; touch(); }, 56));
@@ -1130,7 +1195,7 @@ app.registerExtension({
// Update the token estimate when the caption-level text widgets change.
for (const name of ["global_prompt", "global_negative_prompt"]) {
const w = findW(name);
if (w) chainCallback(w, "callback", () => updateTokens());
if (w) chainCallback(w, "callback", () => { updateTokens(); });
}
// ── keep canvas + getMinHeight in sync while the node is resized ──
@@ -1236,6 +1301,7 @@ app.registerExtension({
if (bgBrightnessWidget) bgSlider.value = bgBrightnessWidget.value;
syncCanvasToDims();
rebuildStylePalette();
renderGlobalPanel();
renderPanel();
drawCanvas();
updateTokens();