+
.f-topbar-dropdown-wrap {
+ z-index: 10;
+}
+/* Мобильный drawer делаем полупрозрачным, чтобы TopBar просвечивал за ним */
+@media (max-width: 900px) {
+ .f-app.f-sb-open .f-sidebar {
+ background: var(--bg-filter);
+ -webkit-backdrop-filter: blur(20px) saturate(180%);
+ backdrop-filter: blur(20px) saturate(180%);
+ }
+}
/* ── Topbar Profile ── */
.f-topbar-profile {
@@ -121,30 +202,23 @@
color: var(--teal); font-weight: 700;
}
-/* ── Topbar Dropdown (Instagram-style) ── */
+/* ── Topbar Dropdown (фильтр типа привязан к кнопке-логотипу) ── */
.f-topbar-dropdown-wrap {
position: relative;
}
-.f-topbar-dropdown-wrap .f-topbar {
- z-index: 10;
+/* В ленте шапка липнет к верху скроллера: контент прокручивается под ней
+ и «замыливается» через backdrop-filter на .f-topbar */
+.f-feed > .f-topbar-dropdown-wrap {
+ position: sticky;
+ top: 0;
+ z-index: 100;
+ margin: -20px -16px 0;
}
-.f-topbar-dropdown-btn {
- display: inline-flex; align-items: center; justify-content: center; gap: 4px;
- background: none; border: none; cursor: pointer;
- color: var(--text-primary);
- font-family: var(--font); font-weight: 900; font-size: 1.25rem;
- letter-spacing: -.4px; white-space: nowrap;
- padding: 0 6px; border-radius: 8px;
- transition: background .15s;
+@media (max-width: 900px) {
+ .f-feed > .f-topbar-dropdown-wrap {
+ margin: -10px -10px 0;
+ }
}
-.f-topbar-dropdown-btn:hover { background: var(--hover-bg); }
-.f-topbar-logo { height: auto; display: block; min-width: 0; max-width: 100%; flex-shrink: 1; margin-top: 10px; }
-
-.f-topbar-chevron {
- width: 18px; height: 18px; flex-shrink: 0;
- transition: transform .25s ease;
-}
-.f-topbar-chevron.open { transform: rotate(180deg); }
.f-topbar-dropdown-overlay {
position: fixed; inset: 0; z-index: 45;
@@ -504,7 +578,7 @@
.f-wrap { display: flex; flex: 1; overflow: hidden; min-height: 0; }
.f-feed {
- flex: 1; overflow-y: scroll; overscroll-behavior: contain; min-height: 0;
+ flex: 1; overflow-y: scroll; overflow-x: hidden; overscroll-behavior: contain; min-height: 0;
padding: 20px 16px; display: flex; flex-direction: column; gap: 16px;
width: 100%; max-width: 660px; margin: 0 auto;
}
@@ -525,6 +599,13 @@
}
@media (max-width: 900px) {
+ /* Мобильная версия: прижимаем логотип-капсулу влево (к кнопке-гамбургеру) */
+ .f-main-feed .f-topbar {
+ justify-content: flex-start;
+ }
+ .f-main-feed .f-logo-btn { margin-left: 64px; }
+ .f-main-feed .f-topbar-spacer { flex: 1; }
+
.f-mapview-back {
/* Поднимаем кнопку выше плавающей нижней навигации */
bottom: calc(84px + env(safe-area-inset-bottom));
@@ -535,10 +616,6 @@
gap: 12px;
}
- .f-topbar {
- padding: 0 12px;
- }
-
.f-post-btn {
padding: 8px 14px;
font-size: 0.78rem;
@@ -563,8 +640,6 @@
.f-city-ms-input { font-size: 0.85rem; }
.f-city-ms-tag { font-size: 0.75rem; padding: 2px 8px; }
- .f-topbar-dropdown-btn { font-size: 1.15rem; padding: 0 4px; }
- .f-topbar-chevron { width: 18px; height: 18px; }
.f-topbar-dropdown-menu {
padding: 6px 10px; gap: 5px;
margin-top: 4px; border-radius: 999px;
diff --git a/src/utils/compressImage.js b/src/utils/compressImage.js
index 5bcfc29..0ac2119 100644
--- a/src/utils/compressImage.js
+++ b/src/utils/compressImage.js
@@ -1,49 +1,41 @@
-/**
- * Сжимает изображение до maxW пикселей по ширине и конвертирует в JPEG.
- * Используется перед отправкой, чтобы не гнать тяжёлые файлы на сервер.
- * Маленькие картинки (< maxW) не увеличиваются.
- *
- * @param {File} file — оригинальный файл
- * @param {number} maxW — максимальная ширина в пикселях (default 1200)
- * @param {number} quality — качество JPEG 0..1 (default 0.82)
- * @returns {Promise}
- */
-export function compressImage(file, maxW = 1200, quality = 0.82) {
+export async function compressImage(file, maxW = 2048, quality = 0.9) {
+ // если файл и так маленький и лёгкий — не трогаем вообще
+ if (file.size < 300 * 1024) return file;
+
+ const bitmap = await createImageBitmap(file, {
+ resizeWidth: undefined, // посчитаем сами
+ });
+
+ const scale = Math.min(1, maxW / bitmap.width);
+ if (scale === 1) return file; // не увеличиваем и не пересжимаем без причины
+
+ // ресайз через createImageBitmap с высоким качеством ресемплинга
+ const targetW = Math.round(bitmap.width * scale);
+ const targetH = Math.round(bitmap.height * scale);
+ const resized = await createImageBitmap(bitmap, {
+ resizeWidth: targetW,
+ resizeHeight: targetH,
+ resizeQuality: "high",
+ });
+
+ const canvas = document.createElement("canvas");
+ canvas.width = targetW;
+ canvas.height = targetH;
+ const ctx = canvas.getContext("2d");
+ ctx.imageSmoothingEnabled = true;
+ ctx.imageSmoothingQuality = "high";
+ ctx.drawImage(resized, 0, 0);
+
return new Promise((resolve, reject) => {
- const img = new Image();
- const url = URL.createObjectURL(file);
-
- img.onload = () => {
- URL.revokeObjectURL(url); // освобождаем память сразу после загрузки
-
- const scale = Math.min(1, maxW / img.width);
- const canvas = document.createElement("canvas");
- canvas.width = Math.round(img.width * scale);
- canvas.height = Math.round(img.height * scale);
- canvas.getContext("2d").drawImage(img, 0, 0, canvas.width, canvas.height);
-
- canvas.toBlob(
- blob => {
- if (!blob) {
- reject(new Error("canvas.toBlob вернул null"));
- return;
- }
- // Имя приводим к .jpg — результат всегда JPEG, независимо от
- // исходного расширения (png/webp/etc), иначе имя файла не
- // соответствует его реальному содержимому.
- const jpegName = file.name.replace(/\.[^.]+$/, "") + ".jpg";
- resolve(new File([blob], jpegName, { type: "image/jpeg" }));
- },
- "image/jpeg",
- quality
- );
- };
-
- img.onerror = () => {
- URL.revokeObjectURL(url);
- reject(new Error("Не удалось загрузить изображение"));
- };
-
- img.src = url;
+ canvas.toBlob(
+ blob => {
+ if (!blob) return reject(new Error("canvas.toBlob вернул null"));
+ // используем WebP, если формат исходно не требовал прозрачности/точности
+ const outName = file.name.replace(/\.[^.]+$/, "") + ".webp";
+ resolve(new File([blob], outName, { type: "image/webp" }));
+ },
+ "image/webp",
+ quality
+ );
});
}
\ No newline at end of file