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:
q
2026-09-26 10:05:26 +03:00
co-authored by Claude Opus 5.5
parent 6e5b57fd7c
commit f8900e531a
94 changed files with 3337 additions and 2432 deletions
+152
View File
@@ -0,0 +1,152 @@
// Скрытый редактор палитры: открывается долгим нажатием на «День» или «Ночь» в настройках.
import {
PALETTE_PRESETS,
PALETTE_ROLES,
exportPalette,
getPaletteSettings,
importPalette,
resolvePalette,
resolveThemeMode,
setPaletteSettings,
} from '../services/theme-service.js';
export function openPaletteEditor({ theme = resolveThemeMode(), onClose } = {}) {
let editTheme = theme === 'light' ? 'light' : 'dark';
const opener = document.activeElement;
const modal = document.createElement('div');
modal.className = 'modal palette-editor';
modal.innerHTML = `
<div class="modal-card palette-editor__card" role="dialog" aria-modal="true" aria-labelledby="palette-editor-title">
<div class="palette-editor__head">
<h2 class="modal-title" id="palette-editor-title">Цвета оформления</h2>
<button class="icon-btn palette-editor__close" type="button" aria-label="Закрыть">✕</button>
</div>
<div class="palette-editor__section">
<span class="palette-editor__label">Основа</span>
<div class="tabs tabs--auto palette-editor__presets" role="radiogroup" aria-label="Готовая палитра"></div>
</div>
<div class="palette-editor__section">
<span class="palette-editor__label">Настраиваемая тема</span>
<div class="tabs tabs--auto" role="radiogroup" aria-label="Тема для настройки">
<button type="button" class="tab-btn" role="radio" data-edit-theme="light">День</button>
<button type="button" class="tab-btn" role="radio" data-edit-theme="dark">Ночь</button>
</div>
<p class="palette-editor__hint">Правки видны сразу. Меняется только выбранная тема.</p>
</div>
<div class="palette-editor__roles"></div>
<details class="palette-editor__share">
<summary>Поделиться палитрой</summary>
<p class="palette-editor__hint">Скопируйте текст и отправьте команде или вставьте чужую палитру и нажмите «Применить».</p>
<textarea class="input palette-editor__json" rows="8" spellcheck="false"></textarea>
<p class="palette-editor__error" role="alert" hidden></p>
<div class="palette-editor__actions">
<button type="button" class="secondary-btn" data-action="copy">Скопировать</button>
<button type="button" class="secondary-btn" data-action="import">Применить</button>
</div>
</details>
<div class="palette-editor__actions">
<button type="button" class="secondary-btn" data-action="reset-theme">Сбросить эту тему</button>
<button type="button" class="primary-btn" data-action="done">Готово</button>
</div>
</div>
`;
const card = modal.querySelector('.palette-editor__card');
const presetsEl = modal.querySelector('.palette-editor__presets');
const rolesEl = modal.querySelector('.palette-editor__roles');
const jsonEl = modal.querySelector('.palette-editor__json');
const errorEl = modal.querySelector('.palette-editor__error');
for (const [id, preset] of Object.entries(PALETTE_PRESETS)) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'tab-btn';
btn.setAttribute('role', 'radio');
btn.dataset.preset = id;
btn.textContent = preset.label;
presetsEl.append(btn);
}
const update = (mutate) => {
const settings = getPaletteSettings();
const next = { preset: settings.preset, custom: { dark: { ...settings.custom.dark }, light: { ...settings.custom.light } } };
mutate(next);
setPaletteSettings(next);
render();
};
function render() {
const settings = getPaletteSettings();
const colors = resolvePalette(editTheme, settings);
const custom = settings.custom[editTheme];
modal.querySelectorAll('[data-preset]').forEach((btn) => {
btn.setAttribute('aria-checked', String(btn.dataset.preset === settings.preset));
});
modal.querySelectorAll('[data-edit-theme]').forEach((btn) => {
btn.setAttribute('aria-checked', String(btn.dataset.editTheme === editTheme));
});
rolesEl.replaceChildren(...PALETTE_ROLES.map((role) => {
const row = document.createElement('label');
row.className = 'palette-editor__role';
const changed = Boolean(custom[role.id]);
row.innerHTML = `
<input type="color" value="${colors[role.id]}" aria-label="${role.label}">
<span class="palette-editor__role-name">${role.label}${changed ? ' <span class="palette-editor__changed">изменён</span>' : ''}</span>
<code class="palette-editor__role-value">${colors[role.id]}</code>
`;
const input = row.querySelector('input');
input.addEventListener('input', () => {
document.documentElement.style.setProperty(`--${role.id}`, input.value);
row.querySelector('code').textContent = input.value;
});
input.addEventListener('change', () => update((next) => { next.custom[editTheme][role.id] = input.value; }));
return row;
}));
jsonEl.value = exportPalette();
errorEl.hidden = true;
}
const close = () => {
document.removeEventListener('keydown', onKeydown);
modal.remove();
opener?.focus?.();
onClose?.();
};
const onKeydown = (event) => { if (event.key === 'Escape') close(); };
modal.addEventListener('click', async (event) => {
if (event.target === modal) { close(); return; }
const presetBtn = event.target.closest('[data-preset]');
if (presetBtn) { update((next) => { next.preset = presetBtn.dataset.preset; }); return; }
// data-edit-theme, а не data-theme: data-theme стоит на <html>, и closest() находил бы его при любом клике.
const themeBtn = event.target.closest('[data-edit-theme]');
if (themeBtn) { editTheme = themeBtn.dataset.editTheme; render(); return; }
if (event.target.closest('.palette-editor__close')) { close(); return; }
const action = event.target.closest('[data-action]')?.dataset.action;
if (action === 'done') close();
if (action === 'reset-theme') update((next) => { next.custom[editTheme] = {}; });
if (action === 'copy') {
try {
await navigator.clipboard.writeText(jsonEl.value);
} catch {
jsonEl.select();
}
}
if (action === 'import') {
try {
importPalette(jsonEl.value);
render();
} catch {
errorEl.textContent = 'Не удалось прочитать палитру: проверьте, что текст скопирован целиком.';
errorEl.hidden = false;
}
}
});
document.addEventListener('keydown', onKeydown);
render();
document.body.append(modal);
card.querySelector('.palette-editor__close').focus();
return close;
}