UI: адаптация шапки «Связи», кнопка поиска каналов, локальный тестовый вход

- Связи: верхняя панель не выходит за границы экрана — кнопки назад/Найти/«?»
  не обрезаются на узких устройствах; заголовок сокращается вместо сдвига кнопок.
- Каналы: emoji-иконка поиска заменена на контурную кнопку-лупу в стиле панели
  (подсказка «Найти канал»); окно поиска без изменений.
- Локальный тестовый вход: на localhost/127.0.0.1/::1 — кнопка демо-входа
  (сеанс local-tester, автономные локальные состояния ЛС/каналов/профиля, без
  сервера и пароля); на shineup.me и тестовых доменах кнопка отсутствует.
- Заметки на ручную проверку добавлены в Dev_Docs/Pending_Features.
- VERSION: client 1.2.262.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Pixel
2026-07-15 12:40:16 +03:00
co-authored by Claude Opus 4.8
parent 127c561a41
commit 8c4997ee2e
19 changed files with 1066 additions and 143 deletions
+58 -22
View File
@@ -7,7 +7,6 @@ import {
} from '../services/user-profile-params.js';
import { buildIdentityLines } from '../services/user-connections.js';
import { renderUserAvatar } from '../components/avatar-image.js';
import { makeProfileLinksRoute } from '../services/shine-routes.js';
export const pageMeta = { id: 'profile-view', title: 'Профиль' };
@@ -30,20 +29,49 @@ function escapeHtml(text) {
.replaceAll("'", '&#39;');
}
function renderProfileInfoText(text) {
const [intro, details] = String(text || '').split(/\n\n/, 2);
if (!details) {
return `<p class="profile-info-modal__lead">${escapeHtml(intro)}</p>`;
}
const [sectionTitle, ...items] = details.split('\n').filter(Boolean);
return `
<p class="profile-info-modal__lead">${escapeHtml(intro)}</p>
<section class="profile-info-modal__section" aria-label="${escapeHtml(sectionTitle)}">
<p class="profile-info-modal__section-title">${escapeHtml(sectionTitle)}</p>
<ol class="profile-info-modal__list">
${items.map((item) => `<li>${escapeHtml(item.replace(/^\d+\)\s*/, ''))}</li>`).join('')}
</ol>
</section>
`;
}
function openProfileInfoModal({ title, text }) {
const root = document.getElementById('modal-root');
if (!root) return;
root.innerHTML = `
<div class="modal" id="profile-info-modal">
<div class="modal-card stack">
<h3 class="modal-title">${escapeHtml(title)}</h3>
<p class="meta-muted" style="white-space: pre-wrap; line-height: 1.45;">${escapeHtml(text)}</p>
<button class="secondary-btn" type="button" id="profile-info-close">Закрыть</button>
</div>
<div class="modal profile-info-modal" id="profile-info-modal" role="dialog" aria-modal="true" aria-labelledby="profile-info-title">
<section class="modal-card profile-info-modal__card stack">
<header class="profile-info-modal__header">
<div>
<p class="profile-info-modal__eyebrow">Информация</p>
<h3 class="modal-title profile-info-modal__title" id="profile-info-title">${escapeHtml(title)}</h3>
</div>
<button class="profile-info-modal__close" type="button" id="profile-info-close-icon" aria-label="Закрыть" title="Закрыть">&times;</button>
</header>
<div class="profile-info-modal__content">
${renderProfileInfoText(text)}
</div>
<footer class="profile-info-modal__footer">
<button class="secondary-btn profile-info-modal__confirm" type="button" id="profile-info-close">Готово</button>
</footer>
</section>
</div>
`;
const close = () => { root.innerHTML = ''; };
root.querySelector('#profile-info-close')?.addEventListener('click', close);
root.querySelector('#profile-info-close-icon')?.addEventListener('click', close);
root.querySelector('#profile-info-modal')?.addEventListener('click', (event) => {
if (event.target?.id === 'profile-info-modal') close();
});
@@ -73,22 +101,9 @@ export function render({ navigate }) {
const topActions = document.createElement('div');
topActions.className = 'profile-top-actions';
topActions.innerHTML = `
<button class="ghost-btn profile-top-action-btn" type="button" data-top-action="edit">Редактировать профиль</button>
<button class="ghost-btn profile-top-action-btn" type="button" data-top-action="settings">Настройки</button>
<button class="ghost-btn profile-top-action-btn profile-top-menu-btn" type="button" data-top-action="settings" aria-label="Настройки" title="Настройки">⋮</button>
`;
topActions.querySelector('[data-top-action="edit"]')?.addEventListener('click', () => navigate('profile-edit-view'));
topActions.querySelector('[data-top-action="settings"]')?.addEventListener('click', () => navigate('settings-view'));
screen.append(topActions);
const bottomActions = document.createElement('div');
bottomActions.className = 'profile-bottom-actions';
bottomActions.innerHTML = `
<button class="ghost-btn profile-top-action-btn" type="button" data-bottom-action="wallet">Кошелёк</button>
<button class="ghost-btn profile-top-action-btn profile-links-two-line" type="button" data-bottom-action="links">Показать\nсвязи</button>
`;
bottomActions.querySelector('[data-bottom-action="wallet"]')?.addEventListener('click', () => navigate('wallet-view'));
bottomActions.querySelector('[data-bottom-action="links"]')?.addEventListener('click', () => navigate(makeProfileLinksRoute(login)));
screen.append(bottomActions);
const card = document.createElement('div');
card.className = 'card stack profile-main-card';
@@ -203,6 +218,27 @@ export function render({ navigate }) {
}
async function refreshProfileSnapshot() {
if (state.session.isLocalDemo) {
currentFields = [
{ key: 'first_name', label: 'Имя', value: 'Тестовый' },
{ key: 'last_name', label: 'Фамилия', value: 'Пользователь' },
{ key: 'address', label: 'Адрес', value: 'Локальный режим' },
{ key: 'website', label: 'Веб', value: '127.0.0.1' },
{ key: 'phone', label: 'Телефон', value: profile.phone },
];
currentToggles = [
{ key: 'official', enabled: false },
{ key: 'shine', enabled: false },
];
currentGender = 'unknown';
currentAvatar = { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 };
syncIdentity();
updateAvatarUi();
updateTogglesUi();
renderFields(currentFields);
return;
}
try {
const snapshot = await loadProfileSnapshot(login);
currentFields = Array.isArray(snapshot.fields) ? snapshot.fields : [];
@@ -218,7 +254,7 @@ export function render({ navigate }) {
}
}
card.append(topRow, badgesRow, listWrap);
card.append(topActions, topRow, badgesRow, listWrap);
screen.append(card);
updateAvatarUi();