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
@@ -434,20 +434,23 @@ export function openArweaveAttachmentManager({
const solBalance = balanceInfo ? `${escapeHtml(String(balanceInfo.solBalance ?? 0))} SOL` : '—';
const uploadPrice = priceInfo ? `${formatTurboCredits(priceInfo.credits, 6)} credits` : '—';
const fileSize = selectedFile ? formatBytes(selectedFile.size) : '—';
const freeHint = selectedFile
? (combinedUploadSize() <= getFreeTurboUploadBytesLimit() ? 'Да, бесплатно через Turbo' : 'Нет, нужен Turbo balance')
: `До ${formatBytes(getFreeTurboUploadBytesLimit())} бесплатно`;
const freeBadge = priceInfo?.isFree
? ' <span style="color:#15803d;">(пока файлы до 100 KB бесплатно)</span>'
const isFreeSize = selectedFile && combinedUploadSize() <= getFreeTurboUploadBytesLimit();
const summary = selectedFile
? `Размер ${escapeHtml(fileSize)} · ${isFreeSize
? '<span class="ar-attachment-ok">бесплатно через Turbo</span>'
: `нужен баланс Turbo: ${escapeHtml(uploadPrice)}`}`
: '';
mainMetaEl.innerHTML = `
<div>Turbo-адрес: ${escapeHtml(shortAddress(turboAddress || '—'))}</div>
<div>Turbo balance: ${escapeHtml(turboCredits)}</div>
<div>SOL на этом ключе: ${solBalance}</div>
<div>Размер файла: ${escapeHtml(fileSize)}</div>
<div>SHA-256: ${escapeHtml(selectedSha256 || '—')}</div>
<div>Цена Turbo: ${escapeHtml(uploadPrice)}${freeBadge}</div>
<div>До ${escapeHtml(formatBytes(getFreeTurboUploadBytesLimit()))} бесплатно: ${escapeHtml(freeHint)}</div>
${summary ? `<div class="ar-attachment-summary">${summary}</div>` : ''}
<details class="ar-attachment-details">
<summary>Подробнее</summary>
<div>Turbo-адрес: ${escapeHtml(shortAddress(turboAddress || '—'))}</div>
<div>Баланс Turbo: ${escapeHtml(turboCredits)}</div>
<div>SOL на этом ключе: ${solBalance}</div>
<div>SHA-256: ${escapeHtml(selectedSha256 || '—')}</div>
<div>Цена Turbo: ${escapeHtml(uploadPrice)}</div>
<div>Бесплатно до ${escapeHtml(formatBytes(getFreeTurboUploadBytesLimit()))}</div>
</details>
`;
previewMetaEl.innerHTML = '';
if (selectedPreviewEnabled && selectedPreviewFile) {
@@ -628,28 +631,40 @@ export function openArweaveAttachmentManager({
root.innerHTML = `
<div class="modal" data-ar-attach-modal="true">
<div class="modal-card stack ar-attachment-manager-card">
<h3 class="modal-title">${escapeHtml(titleText)}</h3>
<p class="meta-muted" style="margin-top:-6px; color:#15803d;">Маленькие файлы и аватары через Turbo пока загружаются бесплатно.</p>
<div class="form-actions-grid">
<button class="${turboMode ? 'secondary-btn' : 'primary-btn'}" type="button" data-action="switch-arweave">Загрузка используя свой Arweave кошелёк</button>
<button class="${turboMode ? 'primary-btn' : 'secondary-btn'}" type="button" data-action="switch-turbo">Загрузить через Turbo</button>
${canShowHistory ? `<button class="secondary-btn" type="button" data-action="history-top">${isAvatarMode ? 'Выбрать из журнала' : 'Использовать журнал загрузок'}</button>` : ''}
${canShowExisting ? `<button class="secondary-btn" type="button" data-action="existing-top">${isAvatarMode ? 'Ввести tx id' : 'Использовать существующий в Arweave файл'}</button>` : ''}
<div class="ar-attachment-manager-head">
<h3 class="modal-title">${escapeHtml(titleText)}</h3>
<button class="icon-btn ar-attachment-manager-close" type="button" data-action="cancel" aria-label="Закрыть">✕</button>
</div>
<div class="tabs tabs--auto" role="radiogroup" aria-label="Способ загрузки">
<button class="tab-btn" role="radio" aria-checked="${turboMode}" type="button" data-action="switch-turbo">Turbo</button>
<button class="tab-btn" role="radio" aria-checked="${!turboMode}" type="button" data-action="switch-arweave">Свой кошелёк Arweave</button>
</div>
${turboMode
? `
<label class="meta-muted" for="turbo-key-source">Подписывать и пополнять через</label>
<select class="input ar-attachment-wallet-select" id="turbo-key-source"></select>
<p class="meta-muted">Turbo использует Solana-ключ пользователя. Маленькие файлы и аватары пока бесплатно. Если файл больше, пополните Turbo со своего SOL-кошелька.</p>
<p class="ar-attachment-note">Маленькие файлы и аватары через Turbo пока загружаются бесплатно. Для больших файлов пополните Turbo со своего SOL-кошелька.</p>
<div class="form-field">
<label for="turbo-key-source">Подписывать и пополнять через</label>
<select class="select ar-attachment-wallet-select" id="turbo-key-source"></select>
</div>
`
: `
<label class="meta-muted" for="ar-attach-wallet">Кошелёк оплаты Arweave</label>
<select class="input ar-attachment-wallet-select" id="ar-attach-wallet"></select>
<button class="ghost-btn" type="button" data-action="add-wallet">Добавить кошелёк</button>
${isAvatarMode ? '<p class="meta-muted">Выберите изображение. Перед загрузкой оно будет сжато до 512×512 и сохранено в истории как аватар.</p>' : (historyOnly ? '' : '<p class="meta-muted">Для надёжности лучше сначала загрузить файл в Arweave, дождаться доступности в журнале, а потом добавить его из истории загрузок.</p>')}
<div class="form-field">
<label for="ar-attach-wallet">Кошелёк оплаты Arweave</label>
<select class="select ar-attachment-wallet-select" id="ar-attach-wallet"></select>
<button class="text-btn ar-attachment-link" type="button" data-action="add-wallet">+ Добавить кошелёк</button>
</div>
${isAvatarMode ? '<p class="ar-attachment-note">Выберите изображение. Перед загрузкой оно будет сжато до 512×512 и сохранено в истории как аватар.</p>' : (historyOnly ? '' : '<p class="ar-attachment-note">Для надёжности лучше сначала загрузить файл в Arweave, дождаться доступности в журнале, а потом добавить его из истории загрузок.</p>')}
`}
${fixedFile ? '' : '<label class="meta-muted" for="ar-attach-file">Файл для загрузки</label>'}
${fixedFile ? '' : `<input class="input" id="ar-attach-file" type="file" ${isAvatarMode ? 'accept="image/*"' : ''} />`}
${canShowHistory || canShowExisting ? `
<div class="ar-attachment-alt-sources">
${canShowHistory ? `<button class="text-btn ar-attachment-link" type="button" data-action="history-top">${isAvatarMode ? 'Выбрать из журнала' : 'Из журнала загрузок'}</button>` : ''}
${canShowExisting ? `<button class="text-btn ar-attachment-link" type="button" data-action="existing-top">${isAvatarMode ? 'Ввести tx id' : 'Уже есть в Arweave (tx id)'}</button>` : ''}
</div>` : ''}
${fixedFile ? '' : `<label class="ar-attachment-file-picker">
<input class="ar-attachment-file-input" id="ar-attach-file" type="file" ${isAvatarMode ? 'accept="image/*"' : ''} />
<span class="ar-attachment-file-btn">Выбрать файл</span>
<span class="ar-attachment-file-name" data-file-name="true">Файл не выбран</span>
</label>`}
<div class="ar-attachment-meta" data-meta-main="true"></div>
<label class="meta-muted" data-preview-option="true" hidden>
<input type="checkbox" data-preview-toggle="true" />
@@ -658,10 +673,9 @@ export function openArweaveAttachmentManager({
<div class="ar-attachment-meta" data-meta-preview="true"></div>
<p class="meta-muted inline-error" data-error="true"></p>
<div class="form-actions-grid">
${turboMode ? '<button class="secondary-btn" type="button" data-action="topup">Пополнить Turbo</button>' : ''}
${turboMode ? '<button class="secondary-btn" type="button" data-action="topup">Пополнить Turbo</button>' : '<button class="secondary-btn" type="button" data-action="cancel">Отмена</button>'}
<button class="primary-btn" type="button" data-action="upload" disabled>${escapeHtml(uploadText)}</button>
</div>
<button class="secondary-btn" type="button" data-action="cancel">Отмена</button>
</div>
</div>
`;
@@ -734,7 +748,7 @@ export function openArweaveAttachmentManager({
}
}
root.querySelector('[data-action="cancel"]')?.addEventListener('click', () => close(resolve, null));
root.querySelectorAll('[data-action="cancel"]').forEach((btn) => btn.addEventListener('click', () => close(resolve, null)));
root.querySelector('[data-action="topup"]')?.addEventListener('click', async () => {
await promptTurboTopUp(mainMetaEl, previewMetaEl, errorEl);
if (turboMode && selectedFile && selectedSha256) {
@@ -745,6 +759,10 @@ export function openArweaveAttachmentManager({
root.querySelector('[data-action="add-wallet"]')?.addEventListener('click', showAddWallet);
}
fileEl?.addEventListener('change', () => {
const nameEl = root.querySelector('[data-file-name="true"]');
if (nameEl) nameEl.textContent = fileEl.files?.[0]?.name || 'Файл не выбран';
});
fileEl?.addEventListener('change', async () => {
selectedFile = fileEl.files?.[0] || null;
selectedSha256 = '';
@@ -1041,7 +1059,7 @@ export function openArweaveAttachmentManager({
<span>${escapeHtml(formatArweaveHistoryTime(item.uploadedAtMs))}</span>
${hasAttachmentPreview(item) ? '<span class="ar-attachment-placement-flag">С превью</span>' : ''}
<span class="ar-attachment-status ar-attachment-status--pending" data-status="${index}">Проверяем...</span>
${item.pendingPlacement ? '<span class="ar-attachment-placement-flag">Не добавлен в SHiNE</span>' : ''}
${item.pendingPlacement ? '<span class="ar-attachment-placement-flag">Ещё не прикреплён</span>' : ''}
</div>
<div class="mono-cell ar-attachment-history-txid">${escapeHtml(item.ar)}</div>
<button class="${isSelected ? 'secondary-btn' : 'ghost-btn'}" type="button" data-pick="${index}" ${isSelected ? 'disabled' : ''}>${isSelected ? 'Уже добавлен ✓' : 'Выбрать'}</button>