SHA256
Compare commits
4
Commits
7aa05dc609
...
svyazi
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
23969121e5 | ||
|
|
4da2d7129a | ||
|
|
2be5ad0b7d | ||
|
|
f44ab75c64 |
+1
-1
@@ -1,2 +1,2 @@
|
||||
client.version=1.12.17
|
||||
client.version=1.12.19
|
||||
server.version=1.10.7
|
||||
|
||||
@@ -218,7 +218,6 @@ const SCROLL_TO_BOTTOM_PAGE_IDS = new Set([
|
||||
'chat-view',
|
||||
'channel-view',
|
||||
'channel-thread-view',
|
||||
'notifications-view',
|
||||
]);
|
||||
|
||||
const FILLED_ACTION_BUTTON_PAGE_IDS = new Set([
|
||||
|
||||
@@ -36,6 +36,7 @@ export function renderAvatar({
|
||||
title = '',
|
||||
alt = 'Аватар',
|
||||
glow = false,
|
||||
official = false,
|
||||
} = {}) {
|
||||
const wrap = document.createElement('div');
|
||||
const classes = new Set(['avatar', 'avatar-image', 'avatar-framed']);
|
||||
@@ -52,6 +53,15 @@ export function renderAvatar({
|
||||
fallback.textContent = String(initials || '?').trim().slice(0, 2).toUpperCase() || '?';
|
||||
wrap.append(fallback);
|
||||
|
||||
if (official) {
|
||||
const badge = document.createElement('img');
|
||||
badge.className = 'avatar-official-badge';
|
||||
badge.src = '/assets/shine-official-badge.svg?v=2';
|
||||
badge.alt = '';
|
||||
badge.setAttribute('aria-hidden', 'true');
|
||||
wrap.append(badge);
|
||||
}
|
||||
|
||||
const txId = String(avatar?.ar || '').trim();
|
||||
if (!validateArweaveTxId(txId)) {
|
||||
return wrap;
|
||||
@@ -146,6 +156,7 @@ export function renderUserAvatar({
|
||||
className = '',
|
||||
title = '',
|
||||
glow = false,
|
||||
official = false,
|
||||
} = {}) {
|
||||
return renderAvatar({
|
||||
initials: buildAvatarInitials({ login, firstName, lastName }),
|
||||
@@ -155,5 +166,6 @@ export function renderUserAvatar({
|
||||
title,
|
||||
alt: 'Аватар',
|
||||
glow,
|
||||
official,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1426,7 +1426,7 @@ export function render({ navigate, route, chrome }) {
|
||||
iconNode: createOverflowDots(),
|
||||
title: 'Действия чата',
|
||||
ariaLabel: 'Открыть меню действий чата',
|
||||
className: 'chat-header-icon-btn chat-header-menu-btn',
|
||||
className: 'chat-header-icon-btn chat-header-menu-btn topbar-overflow-action--raised',
|
||||
menu: {
|
||||
minWidth: 230,
|
||||
items: () => [
|
||||
|
||||
@@ -251,7 +251,7 @@ export function render({ navigate, chrome }) {
|
||||
iconNode: createOverflowDots(),
|
||||
title: 'Меню чатов',
|
||||
ariaLabel: 'Меню чатов',
|
||||
className: 'messages-topbar-menu-btn',
|
||||
className: 'messages-topbar-menu-btn topbar-overflow-action--raised',
|
||||
menu: {
|
||||
minWidth: 210,
|
||||
items: [
|
||||
|
||||
@@ -709,14 +709,23 @@ function buildStableHistoryEngineModel(history, historyDepth = HISTORY_MAX_PREVI
|
||||
const centerKey = normKey(snap?.centerLogin);
|
||||
const snapNodes = Array.isArray(snap?.engineModel?.nodes) ? snap.engineModel.nodes : [];
|
||||
const ownCenter = snapNodes.find((node) => normKey(node?.id) === centerKey);
|
||||
if (ownCenter) centerNodeByKey.set(centerKey, { ...ownCenter, isHistoryCenter: true, keepVisible: true, tier: 1 });
|
||||
if (ownCenter) centerNodeByKey.set(centerKey, {
|
||||
...ownCenter,
|
||||
isHistoryCenter: true,
|
||||
clusterOwnerId: centerKey,
|
||||
keepVisible: true,
|
||||
tier: 1,
|
||||
});
|
||||
|
||||
snapNodes.forEach((rawNode) => {
|
||||
const key = normKey(rawNode?.id);
|
||||
if (!key) return;
|
||||
// Исторические центры сохраняют позицию собственного кластера. Все остальные общие узлы
|
||||
// принадлежат самому свежему кластеру, где встретились, и поэтому «переезжают» туда без дубля.
|
||||
if (!centerKeys.has(key) || key === latestCenterKey || key === centerKey) nodeMap.set(key, { ...rawNode, id: key });
|
||||
if (!centerKeys.has(key) || key === latestCenterKey || key === centerKey) {
|
||||
// Общий узел принадлежит последнему кластеру, возле которого он был перерисован.
|
||||
nodeMap.set(key, { ...rawNode, id: key, clusterOwnerId: centerKey });
|
||||
}
|
||||
|
||||
if (key === centerKey) return;
|
||||
let parents = Array.isArray(rawNode?.edgeParents) ? rawNode.edgeParents : [];
|
||||
@@ -755,9 +764,11 @@ function buildStableHistoryEngineModel(history, historyDepth = HISTORY_MAX_PREVI
|
||||
id: key,
|
||||
login: node?.login || node?.id || key,
|
||||
tier: centerKeys.has(key) ? 1 : Math.max(1, Number(node?.tier) || 1),
|
||||
isHistoryCenter: centerKeys.has(key),
|
||||
fixedLayout: true,
|
||||
keepVisible: centerKeys.has(key) || Boolean(node?.keepVisible),
|
||||
alwaysVisible: (Number(node?.tier) || 1) >= 2 ? true : Boolean(node?.alwaysVisible),
|
||||
clusterOwnerId: normKey(node?.clusterOwnerId || key),
|
||||
edgeParents: edgeParentsByChild.get(key) || [],
|
||||
}));
|
||||
|
||||
@@ -1098,11 +1109,15 @@ export function render({ navigate, route, chrome } = {}) {
|
||||
window.setTimeout(() => inputEl.focus(), 0);
|
||||
}
|
||||
|
||||
function persistManualNodePosition(nodeId, point) {
|
||||
const key = normKey(nodeId);
|
||||
const x = Number(point?.x);
|
||||
const y = Number(point?.y);
|
||||
if (!key || !Number.isFinite(x) || !Number.isFinite(y)) return;
|
||||
function persistManualNodePositions(rows) {
|
||||
const positions = new Map();
|
||||
(Array.isArray(rows) ? rows : []).forEach((row) => {
|
||||
const key = normKey(row?.id || row?.login);
|
||||
const x = Number(row?.x);
|
||||
const y = Number(row?.y);
|
||||
if (key && Number.isFinite(x) && Number.isFinite(y)) positions.set(key, { x, y });
|
||||
});
|
||||
if (!positions.size) return;
|
||||
|
||||
// Координаты исторических snapshot'ов уже находятся в общей world-системе. Обновляем все
|
||||
// упоминания пользователя, чтобы следующий setModel/filter/history render не откатил ручной drag.
|
||||
@@ -1110,12 +1125,13 @@ export function render({ navigate, route, chrome } = {}) {
|
||||
const model = cloneEngineModel(snapshot?.engineModel || { focusId: '', nodes: [] });
|
||||
let changed = false;
|
||||
model.nodes = model.nodes.map((node) => {
|
||||
if (normKey(node?.id) !== key) return node;
|
||||
const point = positions.get(normKey(node?.id));
|
||||
if (!point) return node;
|
||||
changed = true;
|
||||
return {
|
||||
...node,
|
||||
layoutX: x,
|
||||
layoutY: y,
|
||||
layoutX: point.x,
|
||||
layoutY: point.y,
|
||||
fixedLayout: true,
|
||||
};
|
||||
});
|
||||
@@ -1149,10 +1165,19 @@ export function render({ navigate, route, chrome } = {}) {
|
||||
}
|
||||
void load(node.login, { pushHistory: true, transitionAngle, transitionX, transitionY });
|
||||
},
|
||||
// тап по центру — полноценный профиль
|
||||
// Короткий тап по текущему центру сразу открывает его меню.
|
||||
onCenterTap: (node) => {
|
||||
const routeTo = profileInfoRoute(node.login);
|
||||
if (routeTo) navigate(routeTo);
|
||||
const rect = node?.el?.getBoundingClientRect?.();
|
||||
openNodeMenu({
|
||||
login: normalizeLogin(node?.login),
|
||||
displayName: String(node?.name || '').trim(),
|
||||
relationType: node?.relationType,
|
||||
point: rect ? { x: rect.left + rect.width / 2, y: rect.top, rect } : undefined,
|
||||
actions: [
|
||||
{ label: 'Профиль', onClick: () => { const r = profileInfoRoute(node?.login); if (r) navigate(r); } },
|
||||
{ label: 'Написать', onClick: () => navigate(`chat/${encodeURIComponent(normalizeLogin(node?.login))}`) },
|
||||
],
|
||||
});
|
||||
},
|
||||
// долгое нажатие — контекстное меню (вне масштабируемого холста)
|
||||
onNodeLongPress: (node, point) => {
|
||||
@@ -1171,7 +1196,12 @@ export function render({ navigate, route, chrome } = {}) {
|
||||
// Drag периферийного аватара — ручная правка текущей карты. Движок уже двигает DOM/рёбра
|
||||
// в реальном времени; здесь только сохраняем итоговую world-позицию в историю/X2 snapshot.
|
||||
onNodeMoveEnd: (node, point) => {
|
||||
persistManualNodePosition(node?.id || node?.login, point);
|
||||
persistManualNodePositions([{ id: node?.id || node?.login, ...point }]);
|
||||
},
|
||||
// Центр и узлы, которые в последний раз были нарисованы возле него,
|
||||
// сохраняются одной пачкой, чтобы общий узел не вернулся в старый кластер при rebuild.
|
||||
onGroupMoveEnd: (movedNodes) => {
|
||||
persistManualNodePositions(movedNodes);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -219,13 +219,14 @@ function hash01(str) {
|
||||
* @param {Object} opts
|
||||
* @param {HTMLElement} opts.stage - контейнер сцены (position: relative/absolute, overflow hidden)
|
||||
* @param {Object} opts.model - нормализованная модель { focusId, nodes[] }
|
||||
* @param {Function} [opts.onCenterTap] - тап по центральному узлу (node) => void
|
||||
* @param {Function} [opts.onCenterTap] - тап по текущему центральному узлу (node) => void
|
||||
* @param {Function} [opts.onNodeTap] - тап по периферийному узлу (node) => void (вызывается ДО центрирования)
|
||||
* @param {Function} [opts.onNodeLongPress] - долгое нажатие (node, screenPoint) => void
|
||||
* @param {Function} [opts.onNodeMoveEnd] - ручное перемещение периферийного узла (node, {x,y}) => void
|
||||
* @param {Function} [opts.onGroupMoveEnd] - перемещение исторического кластера (nodes[]) => void
|
||||
* @returns {{ destroy: Function, recenter: Function, setModel: Function, getFocusNode: Function }}
|
||||
*/
|
||||
export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeLongPress, onNodeMoveEnd, onNodeHover, onDiveChange } = {}) {
|
||||
export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeLongPress, onNodeMoveEnd, onGroupMoveEnd, onNodeHover, onDiveChange } = {}) {
|
||||
// Слои DOM
|
||||
const edgesSvg = document.createElementNS(SVGNS, 'svg');
|
||||
edgesSvg.setAttribute('class', 'fg-edges');
|
||||
@@ -658,6 +659,8 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
node.relationType = src.relationType;
|
||||
node.shining = Boolean(src.shining);
|
||||
node.official = Boolean(src.official);
|
||||
node.isHistoryCenter = Boolean(src.isHistoryCenter);
|
||||
node.clusterOwnerId = String(src.clusterOwnerId || src.id || '');
|
||||
node.keepVisible = Boolean(src.keepVisible);
|
||||
node.alwaysVisible = Boolean(src.alwaysVisible);
|
||||
node.edgeParents = Array.isArray(src.edgeParents) ? src.edgeParents.map((row) => ({ ...row })) : [];
|
||||
@@ -1472,6 +1475,9 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
let nodeDragActive = false;
|
||||
let nodeDragStartX = 0;
|
||||
let nodeDragStartY = 0;
|
||||
let groupDragActive = false;
|
||||
let groupDragNodes = [];
|
||||
let groupDragStarts = new Map();
|
||||
let longTimer = 0;
|
||||
let longFired = false;
|
||||
const activePointers = new Map(); // id → {x, y}: для щипкового зума двумя пальцами
|
||||
@@ -1602,8 +1608,16 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
if (downNodeEl) { downNodeEl.classList.add('is-pressed'); haptic(6); } // тактильный «клик» вдавливания
|
||||
downNode = nodeFromEvent(ev);
|
||||
nodeDragActive = false;
|
||||
groupDragActive = false;
|
||||
groupDragNodes = [];
|
||||
groupDragStarts = new Map();
|
||||
nodeDragStartX = Number(downNode?.x) || 0;
|
||||
nodeDragStartY = Number(downNode?.y) || 0;
|
||||
if (downNode?.isHistoryCenter) {
|
||||
const ownerId = String(downNode.clusterOwnerId || downNode.id);
|
||||
groupDragNodes = nodes.filter((node) => String(node.clusterOwnerId || node.id) === ownerId);
|
||||
groupDragStarts = new Map(groupDragNodes.map((node) => [String(node.id), { x: Number(node.x) || 0, y: Number(node.y) || 0 }]));
|
||||
}
|
||||
// касание пальцем по узлу = «наведение» (превью ветки), как ховер мышью; мышь обслуживают over/out
|
||||
if (downNode && ev.pointerType !== 'mouse' && typeof onNodeHover === 'function') onNodeHover(downNode, true);
|
||||
if (downNode && typeof onNodeLongPress === 'function') {
|
||||
@@ -1655,15 +1669,34 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
moved = true;
|
||||
if (longTimer) { window.clearTimeout(longTimer); longTimer = 0; }
|
||||
if (downNodeEl) downNodeEl.classList.remove('is-pressed'); // это drag/pan, а не нажатие
|
||||
// Не центральный аватар перетаскивается сам. Пустой фон или центральный узел продолжают панорамировать карту.
|
||||
nodeDragActive = Boolean(downNode && !downNode.isFocus);
|
||||
if (nodeDragActive && cssBloom) endCssBloom();
|
||||
// Исторический центр тащит свой кластер; обычный узел — только себя; пустой фон — всю карту.
|
||||
groupDragActive = Boolean(downNode?.isHistoryCenter && groupDragNodes.length);
|
||||
nodeDragActive = Boolean(downNode && !groupDragActive);
|
||||
if ((nodeDragActive || groupDragActive) && cssBloom) endCssBloom();
|
||||
// палец «съехал» с узла — снимаем временный ховер-превью (касанием), если он был
|
||||
if (ev.pointerType !== 'mouse' && typeof onNodeHover === 'function') onNodeHover(null, false);
|
||||
camTargetX = null; camTargetY = null; // свайп отменяет доводчик камеры (приоритет жеста)
|
||||
cancelTween(); // жест прерывает анимацию центрирования
|
||||
dragging = true;
|
||||
}
|
||||
if (moved && groupDragActive) {
|
||||
const zx = dx / Math.max(0.001, zoom);
|
||||
const zy = dy / Math.max(0.001, zoom);
|
||||
groupDragNodes.forEach((node) => {
|
||||
const start = groupDragStarts.get(String(node.id));
|
||||
if (!start) return;
|
||||
const nx = start.x + zx;
|
||||
const ny = start.y + zy;
|
||||
node.x = nx; node.y = ny;
|
||||
node.tx = nx; node.ty = ny;
|
||||
node.bfx = nx; node.bfy = ny;
|
||||
node.vx = 0; node.vy = 0;
|
||||
node.fixedLayout = true;
|
||||
});
|
||||
renderNodes();
|
||||
renderEdges();
|
||||
return;
|
||||
}
|
||||
if (moved && nodeDragActive && downNode) {
|
||||
// dx/dy приходят в экранных пикселях, координаты узла живут в world-space — делим на текущий zoom.
|
||||
const nx = nodeDragStartX + dx / Math.max(0.001, zoom);
|
||||
@@ -1708,9 +1741,11 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
const wasMoved = moved;
|
||||
const wasLong = longFired;
|
||||
const movedNode = nodeDragActive ? downNode : null;
|
||||
const movedGroup = groupDragActive ? [...groupDragNodes] : [];
|
||||
pointerId = null;
|
||||
dragging = false;
|
||||
nodeDragActive = false;
|
||||
groupDragActive = false;
|
||||
// касание: убрали палец — снимаем временный ховер-превью (фиксацию ниже делает тап через onNodeTap)
|
||||
if (ev.pointerType !== 'mouse' && typeof onNodeHover === 'function') onNodeHover(null, false);
|
||||
|
||||
@@ -1719,6 +1754,11 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
// Передаём окончательную world-позицию наружу, чтобы ручная правка пережила следующий setModel/history render.
|
||||
if (typeof onNodeMoveEnd === 'function') onNodeMoveEnd(movedNode, { x: movedNode.x, y: movedNode.y });
|
||||
renderEdges();
|
||||
} else if (wasMoved && movedGroup.length) {
|
||||
if (typeof onGroupMoveEnd === 'function') {
|
||||
onGroupMoveEnd(movedGroup.map((node) => ({ id: node.id, login: node.login, x: node.x, y: node.y })));
|
||||
}
|
||||
renderEdges();
|
||||
} else if (wasMoved) {
|
||||
// после pan даём физике чуть устаканиться и уснуть
|
||||
wake();
|
||||
@@ -1866,8 +1906,9 @@ export function createForceGraph({ stage, model, onCenterTap, onNodeTap, onNodeL
|
||||
}
|
||||
|
||||
// финальные координаты разлёта (детерминированная орбита с джиттером — в node.tx/ty)
|
||||
const finalX = node.isFocus ? 0 : node.tx;
|
||||
const finalY = node.isFocus ? 0 : node.ty;
|
||||
// Ручно сдвинутый текущий центр не должен прыгать обратно в (0,0) при setModel.
|
||||
const finalX = node.tx;
|
||||
const finalY = node.ty;
|
||||
const finalScale = node.targetScale; // масштаб уже по уровню (focus / tier-1 / tier-2 0.5 / tier-3 точка)
|
||||
const finalOp = node.targetOpacity; // прозрачность по уровню (tier-2 ~0.4, tier-3 ~0.9, иначе 1)
|
||||
|
||||
|
||||
@@ -320,11 +320,15 @@ function renderItem(item, activeTab, navigate) {
|
||||
export function render({ navigate, chrome } = {}) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack notifications-screen';
|
||||
chrome?.setTopbar(createTopBar({ title: 'Уведомления' }));
|
||||
|
||||
const tabs = document.createElement('div');
|
||||
tabs.className = 'notification-feed-tabs app-top-tabs';
|
||||
const tabDefs = [['replies','Ответы'],['connections','Связи'],['events','События']];
|
||||
chrome?.setTopbar(createTopBar({
|
||||
center: tabs,
|
||||
className: 'notifications-topbar',
|
||||
}));
|
||||
|
||||
const list = document.createElement('div');
|
||||
list.className = 'stack notifications-list';
|
||||
let payloadCache = null;
|
||||
@@ -415,7 +419,7 @@ export function render({ navigate, chrome } = {}) {
|
||||
observer?.disconnect();
|
||||
Object.values(pendingSeenTimers).forEach((timer) => clearTimeout(timer));
|
||||
};
|
||||
screen.append(tabs,list);
|
||||
screen.append(list);
|
||||
void load();
|
||||
return screen;
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ export function render({ navigate, chrome }) {
|
||||
iconNode: createOverflowDots(),
|
||||
title: 'Меню профиля',
|
||||
ariaLabel: 'Меню профиля',
|
||||
className: 'profile-head-menu-btn',
|
||||
className: 'profile-head-menu-btn topbar-overflow-action--raised',
|
||||
menu: {
|
||||
className: 'profile-head-menu',
|
||||
minWidth: 250,
|
||||
@@ -176,6 +176,7 @@ export function render({ navigate, chrome }) {
|
||||
size: 'xl',
|
||||
className: 'user-profile-hero-avatar',
|
||||
glow: shining,
|
||||
official,
|
||||
}));
|
||||
|
||||
status.textContent = '';
|
||||
|
||||
@@ -250,6 +250,7 @@ export function render({ navigate, route, chrome }) {
|
||||
size: 'xl',
|
||||
className: 'user-profile-hero-avatar',
|
||||
glow: shining,
|
||||
official,
|
||||
}));
|
||||
|
||||
addMenu = body.querySelector('.user-profile-add-menu');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const encoder = new TextEncoder();
|
||||
const WEB_CRYPTO_REQUIRED_MESSAGE = 'Регистрация и подпись блоков требуют WebCrypto (crypto.subtle). Откройте приложение через HTTPS или localhost в современном браузере и повторите попытку.';
|
||||
import { argon2idAsync } from 'https://esm.sh/@noble/hashes@1.8.0/argon2.js';
|
||||
import { edwardsToMontgomeryPriv, edwardsToMontgomeryPub, x25519 } from 'https://esm.sh/@noble/curves@1.8.1/ed25519';
|
||||
import { argon2idAsync } from '../vendor/noble/hashes-argon2-1.8.0.bundle.mjs';
|
||||
import { edwardsToMontgomeryPriv, edwardsToMontgomeryPub, x25519 } from '../vendor/noble/curves-ed25519-1.8.1.bundle.mjs';
|
||||
const SHINE_KEY_DERIVATION_PREFIX = 'SHiNE-key';
|
||||
|
||||
function getCryptoApi() {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
edwardsToMontgomeryPriv,
|
||||
edwardsToMontgomeryPub,
|
||||
x25519,
|
||||
} from 'https://esm.sh/@noble/curves@1.5.0/ed25519';
|
||||
} from '../vendor/noble/curves-ed25519-1.5.0.bundle.mjs';
|
||||
|
||||
const PAIRING_ENVELOPE_PREFIX = 'shine-esp-pairing-v1:';
|
||||
const PAIRING_HASH_PREFIX = 'sha256$';
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -269,3 +269,23 @@
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* Единый знак официального пользователя. Левый и нижний края зафиксированы,
|
||||
а сам знак растёт вправо и вверх вместе с аватаром. */
|
||||
.avatar.avatar-image.avatar-framed > .avatar-official-badge {
|
||||
position: absolute;
|
||||
left: -2%;
|
||||
bottom: -1%;
|
||||
width: 32%;
|
||||
height: 32%;
|
||||
min-width: 18px;
|
||||
min-height: 18px;
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
display: block;
|
||||
opacity: 1;
|
||||
border-radius: 0;
|
||||
pointer-events: none;
|
||||
z-index: 4;
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.42));
|
||||
}
|
||||
|
||||
@@ -26,3 +26,12 @@
|
||||
0 0 5px rgba(92, 190, 255, 0.72),
|
||||
0 0 10px rgba(72, 145, 255, 0.34);
|
||||
}
|
||||
|
||||
|
||||
/* Явный вариант для шапок, где overflow-кнопку нужно выровнять по эталонной
|
||||
* позиции экрана «Связи». Поднимаем кнопку вместе с зоной нажатия и якорем
|
||||
* выпадающего меню, не меняя базовый TopBar для остальных экранов. */
|
||||
.topbar-overflow-action--raised {
|
||||
position: relative;
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
@@ -97,6 +97,14 @@
|
||||
text-shadow: 0 0 5px var(--app-topbar-blue-glow), 0 0 12px var(--app-topbar-blue-glow-soft);
|
||||
}
|
||||
|
||||
.topbar-slot .topbar__action {
|
||||
min-width: 40px;
|
||||
padding-block: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.topbar-slot .topbar__action svg,
|
||||
.topbar-slot .topbar__back svg {
|
||||
filter: drop-shadow(0 0 3px var(--app-topbar-blue-glow)) drop-shadow(0 0 7px var(--app-topbar-blue-glow-soft));
|
||||
|
||||
@@ -186,14 +186,32 @@
|
||||
|
||||
|
||||
|
||||
/* Уведомления: переключатели лент повторяют стеклянные чипы экрана «Связи». */
|
||||
.notifications-screen .notification-feed-tabs {
|
||||
/* Уведомления: переключатели лент занимают место заголовка в закреплённом
|
||||
* TopBar и повторяют стеклянные чипы экрана «Связи». */
|
||||
.notifications-topbar {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
|
||||
.notifications-topbar .topbar__left,
|
||||
.notifications-topbar .topbar__right {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.notifications-topbar .topbar__center {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.notifications-topbar .notification-feed-tabs {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: nowrap;
|
||||
gap: 8px;
|
||||
padding: 8px 12px 4px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
@@ -201,25 +219,22 @@
|
||||
}
|
||||
|
||||
|
||||
.notifications-screen .notification-feed-tabs .notification-tab-btn {
|
||||
min-width: 92px;
|
||||
.notifications-topbar .notification-feed-tabs .notification-tab-btn {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
padding-inline: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
/* На обычных экранах отступ создаёт стандартный padding screen-content (14px). */
|
||||
.notifications-screen .notification-feed-tabs.app-top-tabs {
|
||||
/* Высота панели остаётся стандартной: перенос фильтров не увеличивает TopBar. */
|
||||
.notifications-topbar .notification-feed-tabs.app-top-tabs {
|
||||
min-height: var(--app-primary-tab-min-height);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
.notifications-screen .notification-feed-tabs .notification-tab-btn {
|
||||
min-width: 92px;
|
||||
}
|
||||
|
||||
|
||||
/* Уведомления: информационные блоки без рамок. Hover/focus не возвращает
|
||||
* обводку — различение сохраняется фоном и лёгкой реакцией на нажатие. */
|
||||
.notifications-screen .notifications-list > .notification-card,
|
||||
|
||||
@@ -799,8 +799,26 @@
|
||||
|
||||
|
||||
.user-profile-screen .user-profile-hero-avatar.avatar-glow::before {
|
||||
inset: -12%;
|
||||
opacity: 0.5;
|
||||
inset: -7%;
|
||||
border: 2px solid rgba(145, 239, 255, 0.94);
|
||||
background: transparent;
|
||||
box-shadow:
|
||||
0 0 3px rgba(155, 244, 255, 0.96),
|
||||
0 0 9px rgba(112, 222, 255, 0.74),
|
||||
0 0 17px rgba(86, 190, 255, 0.38),
|
||||
inset 0 0 4px rgba(176, 248, 255, 0.72);
|
||||
filter: none;
|
||||
transform: none;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
/* На крупном аватаре профиля официальный знак на 20% компактнее базового.
|
||||
* Левый и нижний края остаются на месте, поэтому сокращение приходится на
|
||||
* верхнюю и правую стороны знака. */
|
||||
.user-profile-screen .user-profile-hero-avatar.avatar-framed > .avatar-official-badge {
|
||||
width: 25.6%;
|
||||
height: 25.6%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -210,10 +210,10 @@
|
||||
position: absolute;
|
||||
left: -2%;
|
||||
bottom: -1%;
|
||||
width: 16%;
|
||||
height: 16%;
|
||||
min-width: 9px;
|
||||
min-height: 9px;
|
||||
width: 32%;
|
||||
height: 32%;
|
||||
min-width: 18px;
|
||||
min-height: 18px;
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
display: block;
|
||||
|
||||
Reference in New Issue
Block a user