SHA256
UI: новый общий стиль, тёмная/светлая тема, настраиваемая палитра
- styles/main.css: роли цветов (по умолчанию «Индиго»), шкала отступов/скруглений/шрифтов, цвета отношений для обеих тем; жёсткие цвета в стилях заменены на роли. - Палитра: пресеты и личные правки, «Оформление» Авто/День/Ночь, долгое нажатие — редактор цветов с экспортом/импортом. - Каналы: пузыри постов автора, плашки дней, строка «Написать в канал…», «О канале», создание канала с адресом из названия; лента открывается на свежих постах. - Чаты: плоский список, чипы-фильтры, пузыри, плашки дней; нижняя панель скрыта в переписке. - Корневые разделы — единая шапка; профиль и чужой профиль — общая карточка; настройки, кошелёк, сеансы — меню-списки. - Нижняя панель: иконки без подписей, бейджи на иконках. - confirmDialog вместо window.confirm/alert; на телефоне диалоги — шторки снизу. - docs/UI-Design/ISSUES-for-dev.md — найденные ошибки сервера/UI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
// data-profile-list / data-self-profile-action / data-profile-action обрабатывают profile-view.js и user-profile-view.js.
|
||||
import { iconHtml } from './ui-icon.js';
|
||||
|
||||
export function escapeProfileHtml(text) {
|
||||
return String(text || '')
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
const esc = escapeProfileHtml;
|
||||
|
||||
function statusChip({ kind, label, value, active }) {
|
||||
const n = Number(value || 0);
|
||||
return `
|
||||
<button type="button" class="pf-status${active ? ' is-active' : ''}" data-profile-list="${esc(kind)}"
|
||||
aria-label="${esc(label)}: подтверждений ${n}" title="Статус «${esc(label)}». Число — сколько людей его подтвердили. Нажмите, чтобы увидеть кто.">
|
||||
<span class="pf-status-dot" aria-hidden="true"></span>${esc(label)}${n > 0 ? `<b>${n}</b>` : ''}
|
||||
</button>`;
|
||||
}
|
||||
|
||||
function statHtml({ kind, label, valueHtml, ariaLabel }) {
|
||||
return `
|
||||
<button type="button" class="pf-stat" data-profile-list="${esc(kind)}" aria-label="${esc(ariaLabel)}">
|
||||
<span class="pf-stat-value">${valueHtml}</span>
|
||||
<span class="pf-stat-label">${esc(label)}</span>
|
||||
</button>`;
|
||||
}
|
||||
|
||||
function contactRows(card) {
|
||||
return [
|
||||
['Ссылки', card?.web],
|
||||
['Телефон', card?.phone],
|
||||
['Адрес', card?.address],
|
||||
]
|
||||
.filter(([, value]) => String(value || '').trim())
|
||||
.map(([label, value]) => `
|
||||
<div class="pf-row">
|
||||
<span class="pf-row-label">${esc(label)}</span>
|
||||
<span class="pf-row-value">${esc(value)}</span>
|
||||
</div>`)
|
||||
.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} opts
|
||||
* @param {object} opts.card карточка профиля (loadUserProfileCard)
|
||||
* @param {string} opts.login логин на случай, если в карточке его нет
|
||||
* @param {string} opts.tilesHtml плитки действий (свои/чужие)
|
||||
* @param {string} [opts.tilesWrapClass] доп. класс обёртки плиток (для меню «Добавить»)
|
||||
* @param {string} [opts.beforeTilesHtml] разметка внутри обёртки перед плитками
|
||||
*/
|
||||
export function profileCardHtml({ card, login = '', tilesHtml = '', tilesWrapClass = '', beforeTilesHtml = '', isSelf = true }) {
|
||||
const stats = card?.stats || {};
|
||||
const official = card?.accountRole === 'primary';
|
||||
const shining = card?.shineStatus === 'shining';
|
||||
const cardLogin = card?.login || login;
|
||||
const fullName = [card?.firstName, card?.lastName].map((v) => String(v || '').trim()).filter(Boolean).join(' ');
|
||||
const displayName = fullName || cardLogin || 'Профиль';
|
||||
const about = String(card?.about || '').trim();
|
||||
const spiritualPath = String(card?.spiritualPath || '').trim();
|
||||
const contacts = contactRows(card);
|
||||
const aboutRow = (about || isSelf) ? `
|
||||
<div class="pf-row pf-row--block">
|
||||
<span class="pf-row-label">О себе</span>
|
||||
<p class="pf-row-text${about ? '' : ' is-empty'}">${esc(about || 'Не заполнено')}</p>
|
||||
</div>` : '';
|
||||
const pathRow = spiritualPath ? `
|
||||
<div class="pf-row pf-row--block">
|
||||
<span class="pf-row-label">Духовный путь</span>
|
||||
<p class="pf-row-text">${esc(spiritualPath)}</p>
|
||||
</div>` : '';
|
||||
const listInner = aboutRow + contacts + pathRow;
|
||||
const listHtml = listInner.trim() ? `<div class="pf-list">${listInner}</div>` : '';
|
||||
const friends = Number(stats.friendsCount || 0);
|
||||
const statusChips = [
|
||||
(official || Number(stats.primaryReceivedCount || 0) > 0) && statusChip({ kind: 'primary_received', label: 'Официальный', value: stats.primaryReceivedCount, active: official }),
|
||||
(shining || Number(stats.shineReceivedCount || 0) > 0) && statusChip({ kind: 'shine_received', label: 'Сияющий', value: stats.shineReceivedCount, active: shining }),
|
||||
].filter(Boolean);
|
||||
const statusesHtml = statusChips.length ? `<div class="pf-statuses">${statusChips.join('')}</div>` : '';
|
||||
const closeFriends = Number(stats.closeFriendsCount || 0);
|
||||
|
||||
return `
|
||||
<div class="pf-hero${shining ? ' is-shining' : ''}" aria-label="Профиль ${esc(cardLogin)}">
|
||||
<div class="pf-avatar user-profile-avatar-slot"></div>
|
||||
<h2 class="pf-name">${esc(displayName)}</h2>
|
||||
<div class="pf-login">@${esc(cardLogin)}</div>
|
||||
${statusesHtml}
|
||||
</div>
|
||||
|
||||
<div class="pf-stats">
|
||||
${statHtml({ kind: 'friends', label: 'Друзья', valueHtml: closeFriends ? `${friends}<small> · ${closeFriends} близк.</small>` : String(friends), ariaLabel: `Друзья: ${friends}, близкие: ${closeFriends}` })}
|
||||
${statHtml({ kind: 'channels_owned', label: 'Каналы', valueHtml: String(Number(stats.ownedPublicChannelsCount || 0)), ariaLabel: `Каналы: ${Number(stats.ownedPublicChannelsCount || 0)}` })}
|
||||
${statHtml({ kind: 'channels_following', label: 'Подписки', valueHtml: String(Number(stats.followingChannelsCount || 0)), ariaLabel: `Подписки: ${Number(stats.followingChannelsCount || 0)}` })}
|
||||
</div>
|
||||
|
||||
${tilesHtml ? `
|
||||
<div class="pf-tiles-wrap user-profile-actions-wrap ${esc(tilesWrapClass)}">
|
||||
${beforeTilesHtml}
|
||||
<div class="pf-tiles user-profile-actions">${tilesHtml}</div>
|
||||
</div>` : ''}
|
||||
|
||||
${listHtml}`;
|
||||
}
|
||||
|
||||
export function profileTileHtml({ icon, label, attrs = '', iconMarkup = '' }) {
|
||||
return `<button type="button" class="pf-tile user-profile-action-btn" ${attrs}>${iconMarkup || iconHtml(icon)}<span>${esc(label)}</span></button>`;
|
||||
}
|
||||
Reference in New Issue
Block a user