Fix admin logo upload previews

This commit is contained in:
dcconstructions 2026-06-26 12:43:01 +03:00
parent 7230e8a319
commit 819fc3c0ba
11 changed files with 156 additions and 13 deletions

View File

@ -400,6 +400,7 @@ p {
.breadcrumb,
.panel-note,
.field-path,
.media-upload-hint,
.block-meta,
.group-desc,
.status,
@ -1103,6 +1104,13 @@ textarea {
line-height: 1.42;
}
.editor-error-note {
border-radius: var(--launcher-radius-control);
background: rgba(255, 116, 116, 0.09);
color: #ffd8d8;
padding: 1rem;
}
.service-media-field {
min-width: 0;
}
@ -1213,14 +1221,35 @@ textarea {
height: 2.78rem;
place-items: center;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: var(--launcher-radius-circle);
background: rgba(255, 255, 255, 0.055);
background:
linear-gradient(45deg, rgba(255, 255, 255, 0.18) 25%, transparent 25% 75%, rgba(255, 255, 255, 0.18) 75%),
linear-gradient(45deg, rgba(255, 255, 255, 0.18) 25%, transparent 25% 75%, rgba(255, 255, 255, 0.18) 75%),
rgba(18, 18, 20, 0.92);
background-position:
0 0,
6px 6px,
0 0;
background-size:
12px 12px,
12px 12px,
auto;
color: rgba(255, 255, 255, 0.62);
font-size: 0.78rem;
text-transform: none;
}
.service-media-preview img,
.service-media-preview img {
position: absolute;
inset: 0.28rem;
display: block;
width: calc(100% - 0.56rem);
height: calc(100% - 0.56rem);
border-radius: inherit;
object-fit: contain;
}
.service-media-preview video {
position: absolute;
inset: 0;
@ -1231,6 +1260,28 @@ textarea {
object-fit: cover;
}
.service-media-preview.is-solid-dark {
border-color: rgba(255, 255, 255, 0.34);
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.08),
0 0 0 1px rgba(255, 255, 255, 0.08);
}
.media-upload-hint {
font-size: 0.72rem;
font-weight: 720;
line-height: 1.32;
text-transform: none;
}
.media-upload-hint:empty {
display: none;
}
.media-upload-hint:not(:empty) {
color: rgba(255, 228, 170, 0.9);
}
.media-upload-error {
color: #ffd2d2;
font-size: 0.72rem;

View File

@ -362,6 +362,12 @@ function mediaKindFromValue(value) {
return null;
}
function shouldWarnAboutSolidLogoBackground(editableField) {
const path = String(editableField?.path || "").toLowerCase();
const label = String(editableField?.label || "").toLowerCase();
return /logo|icon|wordmark|логотип|икон/.test(`${path} ${label}`);
}
function uploadBucketForField(block, editableField) {
const path = editableField.path.toLowerCase();
const currentValue = String(getByPath(block, editableField.path) ?? "").toLowerCase();
@ -773,6 +779,19 @@ function setStructuralControlsDisabled(disabled) {
}
}
function renderEditorError(error) {
console.error(error);
el.empty.classList.add("hidden");
el.form.classList.remove("hidden");
el.contentFields.innerHTML = "";
const note = document.createElement("div");
note.className = "empty-note editor-error-note";
note.textContent = `Редактор не смог отрисовать поля блока: ${error.message}`;
el.contentFields.append(note);
setStatus(`Ошибка редактора: ${error.message}`);
}
function renderEditor() {
const region = activeRegion();
const block = activeBlock();
@ -801,13 +820,24 @@ function renderEditor() {
el.enabled.disabled = isStatic;
el.enabled.closest(".toggle-row").hidden = isStatic;
setStructuralControlsDisabled(isStatic);
renderContentFields(block);
try {
renderContentFields(block);
} catch (error) {
renderEditorError(error);
}
el.json.value = JSON.stringify(block, null, 2);
}
function renderAll() {
renderRegions();
renderEditor();
try {
renderRegions();
renderEditor();
} catch (error) {
console.error(error);
setStatus(`Ошибка рендера админки: ${error.message}`);
}
}
function syncBlockFromFields() {
@ -840,8 +870,62 @@ function groupEditableFields(block) {
return groups;
}
function renderMediaPreview(preview, value) {
function resetMediaPreview(preview, hint) {
preview.classList.remove("is-image", "is-video", "is-solid-dark");
preview.innerHTML = "";
preview.title = "";
if (hint) hint.textContent = "";
}
function analyzeLogoPreview(image, preview, hint, editableField) {
if (!hint || !shouldWarnAboutSolidLogoBackground(editableField)) return;
try {
const sampleSize = 24;
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d", { willReadFrequently: true });
if (!context) return;
canvas.width = sampleSize;
canvas.height = sampleSize;
context.drawImage(image, 0, 0, sampleSize, sampleSize);
const data = context.getImageData(0, 0, sampleSize, sampleSize).data;
let opaque = 0;
let edgeOpaque = 0;
let edgeDark = 0;
for (let y = 0; y < sampleSize; y += 1) {
for (let x = 0; x < sampleSize; x += 1) {
const offset = (y * sampleSize + x) * 4;
const alpha = data[offset + 3];
if (alpha < 240) continue;
opaque += 1;
const isEdge = x <= 1 || y <= 1 || x >= sampleSize - 2 || y >= sampleSize - 2;
const isDark = data[offset] + data[offset + 1] + data[offset + 2] < 66;
if (isEdge) {
edgeOpaque += 1;
if (isDark) edgeDark += 1;
}
}
}
const total = sampleSize * sampleSize;
const edgeRatio = edgeOpaque ? edgeDark / edgeOpaque : 0;
const opaqueRatio = opaque / total;
if (opaqueRatio > 0.92 && edgeRatio > 0.82) {
preview.classList.add("is-solid-dark");
hint.textContent = "Файл с непрозрачным тёмным фоном. Для логотипа на тёмном интерфейсе лучше PNG/SVG с прозрачностью.";
}
} catch {
// External images can be blocked by canvas CORS. Preview still works, just without diagnostics.
}
}
function renderMediaPreview(preview, value, hint = null, editableField = null) {
resetMediaPreview(preview, hint);
const src = adminPreviewUrl(value);
const kind = mediaKindFromValue(value);
@ -853,6 +937,7 @@ function renderMediaPreview(preview, value) {
if (kind === "video") {
const video = document.createElement("video");
preview.classList.add("is-video");
video.src = src;
video.autoplay = true;
video.loop = true;
@ -864,12 +949,15 @@ function renderMediaPreview(preview, value) {
if (kind === "image") {
const image = document.createElement("img");
preview.classList.add("is-image");
image.src = src;
image.alt = "";
image.loading = "lazy";
image.addEventListener("load", () => analyzeLogoPreview(image, preview, hint, editableField));
image.addEventListener("error", () => {
preview.innerHTML = "";
resetMediaPreview(preview, hint);
preview.textContent = "FILE";
if (hint) hint.textContent = "Превью не загрузилось. Проверьте путь или внешний URL.";
});
preview.append(image);
return;
@ -878,13 +966,13 @@ function renderMediaPreview(preview, value) {
preview.textContent = "FILE";
}
function updateMediaFieldValue({ block, editableField, hiddenInput, urlInput, fileName, preview, value }) {
function updateMediaFieldValue({ block, editableField, hiddenInput, urlInput, fileName, preview, hint, value }) {
hiddenInput.value = value;
urlInput.value = value;
fileName.textContent = truncateText(fileNameFromUrl(value), 28);
fileName.title = fileNameFromUrl(value);
setByPath(block, editableField.path, value);
renderMediaPreview(preview, value);
renderMediaPreview(preview, value, hint, editableField);
if (activeBlock() === block) {
el.json.value = JSON.stringify(block, null, 2);
@ -911,6 +999,7 @@ function renderMediaField({ block, editableField, grid }) {
const urlSourceButton = document.createElement("button");
const preview = document.createElement("div");
const path = document.createElement("span");
const hint = document.createElement("span");
const error = document.createElement("span");
const inputId = `media-${editableField.path.replace(/[^a-z0-9_-]+/gi, "-")}-${Math.random().toString(36).slice(2)}`;
let source = /^(https?:)?\/\//i.test(value) ? "url" : "file";
@ -971,10 +1060,11 @@ function renderMediaField({ block, editableField, grid }) {
sourceSwitch.append(fileSourceButton, urlSourceButton);
preview.className = "service-media-preview";
renderMediaPreview(preview, value);
renderMediaPreview(preview, value, hint, editableField);
path.className = "field-path";
path.textContent = `${editableField.path} -> assets/uploads/${uploadBucketForField(block, editableField)}`;
hint.className = "media-upload-hint";
error.className = "media-upload-error";
fileSourceButton.addEventListener("click", () => setSource("file"));
@ -991,6 +1081,7 @@ function renderMediaField({ block, editableField, grid }) {
urlInput,
fileName,
preview,
hint,
value: urlInput.value,
});
});
@ -1027,6 +1118,7 @@ function renderMediaField({ block, editableField, grid }) {
urlInput,
fileName,
preview,
hint,
value: payload.url,
});
setSource("file");
@ -1042,7 +1134,7 @@ function renderMediaField({ block, editableField, grid }) {
setSource(source);
control.append(fileControl, urlInput, sourceSwitch, preview);
container.append(labelRow, hiddenInput, control, path, error);
container.append(labelRow, hiddenInput, control, path, hint, error);
grid.append(container);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -34,7 +34,7 @@
"target": ""
}
],
"logoSrc": "./assets/webflow/images/69ad964d951f9d17cc5a919e_logo-app.svg"
"logoSrc": "./assets/uploads/images/static-elements/dclogo_darck-mquq1l13-transparent.png"
},
"notifications": [
{

File diff suppressed because one or more lines are too long