Fix admin logo upload previews
|
|
@ -400,6 +400,7 @@ p {
|
||||||
.breadcrumb,
|
.breadcrumb,
|
||||||
.panel-note,
|
.panel-note,
|
||||||
.field-path,
|
.field-path,
|
||||||
|
.media-upload-hint,
|
||||||
.block-meta,
|
.block-meta,
|
||||||
.group-desc,
|
.group-desc,
|
||||||
.status,
|
.status,
|
||||||
|
|
@ -1103,6 +1104,13 @@ textarea {
|
||||||
line-height: 1.42;
|
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 {
|
.service-media-field {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
@ -1213,14 +1221,35 @@ textarea {
|
||||||
height: 2.78rem;
|
height: 2.78rem;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||||
border-radius: var(--launcher-radius-circle);
|
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);
|
color: rgba(255, 255, 255, 0.62);
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
text-transform: none;
|
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 {
|
.service-media-preview video {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
|
@ -1231,6 +1260,28 @@ textarea {
|
||||||
object-fit: cover;
|
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 {
|
.media-upload-error {
|
||||||
color: #ffd2d2;
|
color: #ffd2d2;
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
|
|
|
||||||
104
admin/admin.js
|
|
@ -362,6 +362,12 @@ function mediaKindFromValue(value) {
|
||||||
return null;
|
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) {
|
function uploadBucketForField(block, editableField) {
|
||||||
const path = editableField.path.toLowerCase();
|
const path = editableField.path.toLowerCase();
|
||||||
const currentValue = String(getByPath(block, 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() {
|
function renderEditor() {
|
||||||
const region = activeRegion();
|
const region = activeRegion();
|
||||||
const block = activeBlock();
|
const block = activeBlock();
|
||||||
|
|
@ -801,13 +820,24 @@ function renderEditor() {
|
||||||
el.enabled.disabled = isStatic;
|
el.enabled.disabled = isStatic;
|
||||||
el.enabled.closest(".toggle-row").hidden = isStatic;
|
el.enabled.closest(".toggle-row").hidden = isStatic;
|
||||||
setStructuralControlsDisabled(isStatic);
|
setStructuralControlsDisabled(isStatic);
|
||||||
|
|
||||||
|
try {
|
||||||
renderContentFields(block);
|
renderContentFields(block);
|
||||||
|
} catch (error) {
|
||||||
|
renderEditorError(error);
|
||||||
|
}
|
||||||
|
|
||||||
el.json.value = JSON.stringify(block, null, 2);
|
el.json.value = JSON.stringify(block, null, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAll() {
|
function renderAll() {
|
||||||
|
try {
|
||||||
renderRegions();
|
renderRegions();
|
||||||
renderEditor();
|
renderEditor();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
setStatus(`Ошибка рендера админки: ${error.message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncBlockFromFields() {
|
function syncBlockFromFields() {
|
||||||
|
|
@ -840,8 +870,62 @@ function groupEditableFields(block) {
|
||||||
return groups;
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMediaPreview(preview, value) {
|
function resetMediaPreview(preview, hint) {
|
||||||
|
preview.classList.remove("is-image", "is-video", "is-solid-dark");
|
||||||
preview.innerHTML = "";
|
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 src = adminPreviewUrl(value);
|
||||||
const kind = mediaKindFromValue(value);
|
const kind = mediaKindFromValue(value);
|
||||||
|
|
@ -853,6 +937,7 @@ function renderMediaPreview(preview, value) {
|
||||||
|
|
||||||
if (kind === "video") {
|
if (kind === "video") {
|
||||||
const video = document.createElement("video");
|
const video = document.createElement("video");
|
||||||
|
preview.classList.add("is-video");
|
||||||
video.src = src;
|
video.src = src;
|
||||||
video.autoplay = true;
|
video.autoplay = true;
|
||||||
video.loop = true;
|
video.loop = true;
|
||||||
|
|
@ -864,12 +949,15 @@ function renderMediaPreview(preview, value) {
|
||||||
|
|
||||||
if (kind === "image") {
|
if (kind === "image") {
|
||||||
const image = document.createElement("img");
|
const image = document.createElement("img");
|
||||||
|
preview.classList.add("is-image");
|
||||||
image.src = src;
|
image.src = src;
|
||||||
image.alt = "";
|
image.alt = "";
|
||||||
image.loading = "lazy";
|
image.loading = "lazy";
|
||||||
|
image.addEventListener("load", () => analyzeLogoPreview(image, preview, hint, editableField));
|
||||||
image.addEventListener("error", () => {
|
image.addEventListener("error", () => {
|
||||||
preview.innerHTML = "";
|
resetMediaPreview(preview, hint);
|
||||||
preview.textContent = "FILE";
|
preview.textContent = "FILE";
|
||||||
|
if (hint) hint.textContent = "Превью не загрузилось. Проверьте путь или внешний URL.";
|
||||||
});
|
});
|
||||||
preview.append(image);
|
preview.append(image);
|
||||||
return;
|
return;
|
||||||
|
|
@ -878,13 +966,13 @@ function renderMediaPreview(preview, value) {
|
||||||
preview.textContent = "FILE";
|
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;
|
hiddenInput.value = value;
|
||||||
urlInput.value = value;
|
urlInput.value = value;
|
||||||
fileName.textContent = truncateText(fileNameFromUrl(value), 28);
|
fileName.textContent = truncateText(fileNameFromUrl(value), 28);
|
||||||
fileName.title = fileNameFromUrl(value);
|
fileName.title = fileNameFromUrl(value);
|
||||||
setByPath(block, editableField.path, value);
|
setByPath(block, editableField.path, value);
|
||||||
renderMediaPreview(preview, value);
|
renderMediaPreview(preview, value, hint, editableField);
|
||||||
|
|
||||||
if (activeBlock() === block) {
|
if (activeBlock() === block) {
|
||||||
el.json.value = JSON.stringify(block, null, 2);
|
el.json.value = JSON.stringify(block, null, 2);
|
||||||
|
|
@ -911,6 +999,7 @@ function renderMediaField({ block, editableField, grid }) {
|
||||||
const urlSourceButton = document.createElement("button");
|
const urlSourceButton = document.createElement("button");
|
||||||
const preview = document.createElement("div");
|
const preview = document.createElement("div");
|
||||||
const path = document.createElement("span");
|
const path = document.createElement("span");
|
||||||
|
const hint = document.createElement("span");
|
||||||
const error = document.createElement("span");
|
const error = document.createElement("span");
|
||||||
const inputId = `media-${editableField.path.replace(/[^a-z0-9_-]+/gi, "-")}-${Math.random().toString(36).slice(2)}`;
|
const inputId = `media-${editableField.path.replace(/[^a-z0-9_-]+/gi, "-")}-${Math.random().toString(36).slice(2)}`;
|
||||||
let source = /^(https?:)?\/\//i.test(value) ? "url" : "file";
|
let source = /^(https?:)?\/\//i.test(value) ? "url" : "file";
|
||||||
|
|
@ -971,10 +1060,11 @@ function renderMediaField({ block, editableField, grid }) {
|
||||||
sourceSwitch.append(fileSourceButton, urlSourceButton);
|
sourceSwitch.append(fileSourceButton, urlSourceButton);
|
||||||
|
|
||||||
preview.className = "service-media-preview";
|
preview.className = "service-media-preview";
|
||||||
renderMediaPreview(preview, value);
|
renderMediaPreview(preview, value, hint, editableField);
|
||||||
|
|
||||||
path.className = "field-path";
|
path.className = "field-path";
|
||||||
path.textContent = `${editableField.path} -> assets/uploads/${uploadBucketForField(block, editableField)}`;
|
path.textContent = `${editableField.path} -> assets/uploads/${uploadBucketForField(block, editableField)}`;
|
||||||
|
hint.className = "media-upload-hint";
|
||||||
error.className = "media-upload-error";
|
error.className = "media-upload-error";
|
||||||
|
|
||||||
fileSourceButton.addEventListener("click", () => setSource("file"));
|
fileSourceButton.addEventListener("click", () => setSource("file"));
|
||||||
|
|
@ -991,6 +1081,7 @@ function renderMediaField({ block, editableField, grid }) {
|
||||||
urlInput,
|
urlInput,
|
||||||
fileName,
|
fileName,
|
||||||
preview,
|
preview,
|
||||||
|
hint,
|
||||||
value: urlInput.value,
|
value: urlInput.value,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -1027,6 +1118,7 @@ function renderMediaField({ block, editableField, grid }) {
|
||||||
urlInput,
|
urlInput,
|
||||||
fileName,
|
fileName,
|
||||||
preview,
|
preview,
|
||||||
|
hint,
|
||||||
value: payload.url,
|
value: payload.url,
|
||||||
});
|
});
|
||||||
setSource("file");
|
setSource("file");
|
||||||
|
|
@ -1042,7 +1134,7 @@ function renderMediaField({ block, editableField, grid }) {
|
||||||
|
|
||||||
setSource(source);
|
setSource(source);
|
||||||
control.append(fileControl, urlInput, sourceSwitch, preview);
|
control.append(fileControl, urlInput, sourceSwitch, preview);
|
||||||
container.append(labelRow, hiddenInput, control, path, error);
|
container.append(labelRow, hiddenInput, control, path, hint, error);
|
||||||
grid.append(container);
|
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": ""
|
"target": ""
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"logoSrc": "./assets/webflow/images/69ad964d951f9d17cc5a919e_logo-app.svg"
|
"logoSrc": "./assets/uploads/images/static-elements/dclogo_darck-mquq1l13-transparent.png"
|
||||||
},
|
},
|
||||||
"notifications": [
|
"notifications": [
|
||||||
{
|
{
|
||||||
|
|
|
||||||