Fix admin logo upload previews
|
|
@ -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;
|
||||
|
|
|
|||
104
admin/admin.js
|
|
@ -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);
|
||||
|
||||
try {
|
||||
renderContentFields(block);
|
||||
} catch (error) {
|
||||
renderEditorError(error);
|
||||
}
|
||||
|
||||
el.json.value = JSON.stringify(block, null, 2);
|
||||
}
|
||||
|
||||
function renderAll() {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
|
@ -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": [
|
||||
{
|
||||
|
|
|
|||