SHA256
- 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>
153 lines
6.9 KiB
JavaScript
153 lines
6.9 KiB
JavaScript
// Скрытый редактор палитры: открывается долгим нажатием на «День» или «Ночь» в настройках.
|
||
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;
|
||
}
|