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,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;
|
||||
}
|
||||
Reference in New Issue
Block a user