UI: уплотнить журнал загрузок Arweave

This commit is contained in:
AidarKC
2026-07-31 13:50:17 +04:00
parent 98eba54621
commit cd67176b41
9 changed files with 219 additions and 67 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
client.version=1.4.3 client.version=1.4.4
server.version=1.4.0 server.version=1.4.0
+2 -1
View File
@@ -61,7 +61,8 @@ UI поддерживает три сценария:
- выбрать файл из журнала файлов, загруженных в текущей браузерной сессии. - выбрать файл из журнала файлов, загруженных в текущей браузерной сессии.
Журнал хранится только в `sessionStorage` текущего браузера и не является частью блокчейна. Журнал хранится только в `sessionStorage` текущего браузера и не является частью блокчейна.
В настройках клиента есть отдельный экран журнала загрузок Arweave: пользователь может заранее загрузить файл в Arweave без создания сообщения, увидеть плитку с именем, размером, датой, `txId` и статусом доступности через gateway, а затем выбрать этот файл из истории при создании сообщения. В настройках клиента есть отдельный экран `Загрузить файлы в блокчейн`: пользователь может заранее загрузить файл в Arweave без создания сообщения, увидеть плитку с именем, размером, временем загрузки, `txId` и статусом доступности через gateway, а затем выбрать этот файл из истории при создании сообщения.
Если файл загружен заранее, но ещё не был отправлен ни в одном сообщении SHiNE, клиент показывает локальный флаг `Не добавлен в SHiNE`. Флаг снимается после успешной отправки сообщения с этим вложением.
Если история очищена, уже созданные сообщения не меняются: в блокчейне остаются attach-блоки с `txId`. Если история очищена, уже созданные сообщения не меняются: в блокчейне остаются attach-блоки с `txId`.
## Отображение ## Отображение
+5
View File
@@ -1,5 +1,10 @@
# История изменений документации блокчейна # История изменений документации блокчейна
## 2026-07-31 13:48:57 +0400
- Базовый коммит-ориентир: `98eba54`.
- Уточнено UI-поведение журнала загрузок Arweave: экран называется `Загрузить файлы в блокчейн`, использует компактные плитки и локальный флаг `Не добавлен в SHiNE` для файлов, которые загружены заранее, но ещё не отправлены во вложении сообщения.
- Зафиксировано, что флаг хранится только в клиентском sessionStorage и снимается после успешной отправки сообщения с выбранным вложением; формат attach-блока не изменён.
## 2026-07-31 13:12:24 +0400 ## 2026-07-31 13:12:24 +0400
- Базовый коммит-ориентир: `c530627`. - Базовый коммит-ориентир: `c530627`.
- Для UI-вложений добавлен отдельный экран журнала загрузок Arweave в настройках: загрузка файла без создания сообщения, плитки истории с `txId`, размером, датой и статусом доступности через gateway. - Для UI-вложений добавлен отдельный экран журнала загрузок Arweave в настройках: загрузка файла без создания сообщения, плитки истории с `txId`, размером, датой и статусом доступности через gateway.
@@ -13,13 +13,23 @@ const HISTORY_KEY = 'shine-ui-arweave-attachment-history-v1';
const MAX_HISTORY_ITEMS = 100; const MAX_HISTORY_ITEMS = 100;
const RECENT_UPLOAD_MS = 20 * 60 * 1000; const RECENT_UPLOAD_MS = 20 * 60 * 1000;
function normalizeHistoryItem(input = {}) {
const item = normalizeAttachment(input);
const placedInShineAtMs = Number(input.placedInShineAtMs || 0) || 0;
return {
...item,
pendingPlacement: input.pendingPlacement === true && !placedInShineAtMs,
placedInShineAtMs,
};
}
export function readArweaveAttachmentHistory(login) { export function readArweaveAttachmentHistory(login) {
try { try {
const raw = sessionStorage.getItem(HISTORY_KEY); const raw = sessionStorage.getItem(HISTORY_KEY);
const parsed = raw ? JSON.parse(raw) : {}; const parsed = raw ? JSON.parse(raw) : {};
const key = String(login || '').trim().toLowerCase(); const key = String(login || '').trim().toLowerCase();
const rows = Array.isArray(parsed?.[key]) ? parsed[key] : []; const rows = Array.isArray(parsed?.[key]) ? parsed[key] : [];
return rows.map((item) => normalizeAttachment(item)).filter(Boolean); return rows.map((item) => normalizeHistoryItem(item)).filter(Boolean);
} catch { } catch {
return []; return [];
} }
@@ -30,7 +40,9 @@ export function saveArweaveAttachmentHistory(login, rows) {
const key = String(login || '').trim().toLowerCase(); const key = String(login || '').trim().toLowerCase();
const raw = sessionStorage.getItem(HISTORY_KEY); const raw = sessionStorage.getItem(HISTORY_KEY);
const parsed = raw ? JSON.parse(raw) : {}; const parsed = raw ? JSON.parse(raw) : {};
parsed[key] = (Array.isArray(rows) ? rows : []).slice(0, MAX_HISTORY_ITEMS); parsed[key] = (Array.isArray(rows) ? rows : [])
.map((item) => normalizeHistoryItem(item))
.slice(0, MAX_HISTORY_ITEMS);
sessionStorage.setItem(HISTORY_KEY, JSON.stringify(parsed)); sessionStorage.setItem(HISTORY_KEY, JSON.stringify(parsed));
} catch { } catch {
// ignore sessionStorage errors // ignore sessionStorage errors
@@ -41,12 +53,52 @@ export function clearArweaveAttachmentHistory(login) {
saveArweaveAttachmentHistory(login, []); saveArweaveAttachmentHistory(login, []);
} }
export function addArweaveAttachmentToHistory(login, attachment) { export function addArweaveAttachmentToHistory(login, attachment, { pendingPlacement = undefined, markPlaced = false } = {}) {
const item = normalizeAttachment(attachment); const item = normalizeHistoryItem(attachment);
const rows = readArweaveAttachmentHistory(login).filter((row) => row.ar !== item.ar); const rows = readArweaveAttachmentHistory(login);
rows.unshift(item); const existing = rows.find((row) => row.ar === item.ar) || {};
saveArweaveAttachmentHistory(login, rows); const placedInShineAtMs = markPlaced
return item; ? Date.now()
: Number(existing.placedInShineAtMs || item.placedInShineAtMs || 0) || 0;
const nextPendingPlacement = pendingPlacement === undefined
? (existing.pendingPlacement === true && !placedInShineAtMs)
: (!!pendingPlacement && !placedInShineAtMs);
const nextItem = {
...item,
pendingPlacement: nextPendingPlacement,
placedInShineAtMs,
};
const nextRows = rows.filter((row) => row.ar !== item.ar);
nextRows.unshift(nextItem);
saveArweaveAttachmentHistory(login, nextRows);
return nextItem;
}
export function markArweaveAttachmentPlaced(login, attachment) {
const item = normalizeHistoryItem(attachment);
const rows = readArweaveAttachmentHistory(login);
const existing = rows.find((row) => row.ar === item.ar) || {};
const nextItem = {
...item,
uploadedAtMs: item.uploadedAtMs || existing.uploadedAtMs || Date.now(),
pendingPlacement: false,
placedInShineAtMs: Number(existing.placedInShineAtMs || 0) || Date.now(),
};
const nextRows = rows.filter((row) => row.ar !== item.ar);
nextRows.unshift(nextItem);
saveArweaveAttachmentHistory(login, nextRows);
return nextItem;
}
export function formatArweaveHistoryTime(ms) {
const value = Number(ms || 0);
if (!Number.isFinite(value) || value <= 0) return '—';
const diffMs = Date.now() - value;
if (diffMs >= 0 && diffMs < RECENT_UPLOAD_MS) {
const minutes = Math.max(1, Math.floor(diffMs / 60000));
return `${minutes} мин назад`;
}
return new Date(value).toLocaleString('ru-RU');
} }
export function formatArweaveHistoryDateTime(ms) { export function formatArweaveHistoryDateTime(ms) {
@@ -87,6 +139,7 @@ export function openArweaveAttachmentManager({
gateway, gateway,
onSelect, onSelect,
selectedTxIds = [], selectedTxIds = [],
historyOnly = false,
} = {}) { } = {}) {
const cleanLogin = String(login || '').trim(); const cleanLogin = String(login || '').trim();
const cleanStoragePwd = String(storagePwd || '').trim(); const cleanStoragePwd = String(storagePwd || '').trim();
@@ -118,9 +171,12 @@ export function openArweaveAttachmentManager({
resolve(result); resolve(result);
} }
function finish(resolve, attachment) { function finish(resolve, attachment, { pendingPlacement = undefined } = {}) {
const item = addArweaveAttachmentToHistory(cleanLogin, attachment); const item = addArweaveAttachmentToHistory(cleanLogin, attachment, {
if (typeof onSelect === 'function') onSelect(item); pendingPlacement,
markPlaced: false,
});
if (!pendingPlacement && typeof onSelect === 'function') onSelect(item);
close(resolve, item); close(resolve, item);
} }
@@ -151,19 +207,19 @@ export function openArweaveAttachmentManager({
root.innerHTML = ` root.innerHTML = `
<div class="modal" data-ar-attach-modal="true"> <div class="modal" data-ar-attach-modal="true">
<div class="modal-card stack ar-attachment-manager-card"> <div class="modal-card stack ar-attachment-manager-card">
<h3 class="modal-title">Добавить вложение</h3> <h3 class="modal-title">${historyOnly ? 'Загрузить файл в блокчейн' : 'Добавить вложение'}</h3>
<label class="meta-muted" for="ar-attach-wallet">Кошелёк оплаты Arweave</label> <label class="meta-muted" for="ar-attach-wallet">Кошелёк оплаты Arweave</label>
<select class="input" id="ar-attach-wallet"></select> <select class="input" id="ar-attach-wallet"></select>
<button class="ghost-btn" type="button" data-action="add-wallet">Добавить кошелёк</button> <button class="ghost-btn" type="button" data-action="add-wallet">Добавить кошелёк</button>
<p class="meta-muted">Для надёжности лучше сначала загрузить файл в Arweave, дождаться доступности в журнале, а потом добавить его из истории загрузок.</p> ${historyOnly ? '' : '<p class="meta-muted">Для надёжности лучше сначала загрузить файл в Arweave, дождаться доступности в журнале, а потом добавить его из истории загрузок.</p>'}
<label class="meta-muted" for="ar-attach-file">Файл для загрузки</label> <label class="meta-muted" for="ar-attach-file">Файл для загрузки</label>
<input class="input" id="ar-attach-file" type="file" /> <input class="input" id="ar-attach-file" type="file" />
<div class="ar-attachment-meta" data-meta="true"></div> <div class="ar-attachment-meta" data-meta="true"></div>
<p class="meta-muted inline-error" data-error="true"></p> <p class="meta-muted inline-error" data-error="true"></p>
<div class="form-actions-grid"> <div class="form-actions-grid">
<button class="secondary-btn" type="button" data-action="existing">Ввести txId</button> ${historyOnly ? '' : '<button class="secondary-btn" type="button" data-action="existing">Ввести txId</button>'}
<button class="secondary-btn" type="button" data-action="history">История загрузок</button> ${historyOnly ? '' : '<button class="secondary-btn" type="button" data-action="history">История загрузок</button>'}
<button class="primary-btn" type="button" data-action="upload" disabled>Загрузить</button> <button class="primary-btn" type="button" data-action="upload" disabled>${historyOnly ? 'Загрузить в журнал' : 'Загрузить'}</button>
</div> </div>
<button class="secondary-btn" type="button" data-action="cancel">Отмена</button> <button class="secondary-btn" type="button" data-action="cancel">Отмена</button>
</div> </div>
@@ -261,7 +317,7 @@ export function openArweaveAttachmentManager({
sha256: selectedSha256, sha256: selectedSha256,
ar: uploaded.id, ar: uploaded.id,
uploadedAtMs: Date.now(), uploadedAtMs: Date.now(),
}); }, { pendingPlacement: historyOnly });
} catch (error) { } catch (error) {
uploadBtn.disabled = false; uploadBtn.disabled = false;
setText(errorEl, error?.message || 'Не удалось загрузить файл в Arweave.'); setText(errorEl, error?.message || 'Не удалось загрузить файл в Arweave.');
@@ -349,11 +405,13 @@ export function openArweaveAttachmentManager({
const isSelected = selectedTxIdSet.has(item.ar); const isSelected = selectedTxIdSet.has(item.ar);
return ` return `
<article class="ar-attachment-history-tile${isSelected ? ' is-selected' : ''}" data-history-tile="${index}"> <article class="ar-attachment-history-tile${isSelected ? ' is-selected' : ''}" data-history-tile="${index}">
<div class="ar-attachment-history-tile-head"> <strong class="ar-attachment-history-name">${escapeHtml(item.name)}</strong>
<strong>${escapeHtml(item.name)}</strong> <div class="ar-attachment-history-meta-row">
<span>${escapeHtml(formatBytes(item.size))}</span>
<span>${escapeHtml(formatArweaveHistoryTime(item.uploadedAtMs))}</span>
<span class="ar-attachment-status ar-attachment-status--pending" data-status="${index}">Проверяем...</span> <span class="ar-attachment-status ar-attachment-status--pending" data-status="${index}">Проверяем...</span>
${item.pendingPlacement ? '<span class="ar-attachment-placement-flag">Не добавлен в SHiNE</span>' : ''}
</div> </div>
<div class="meta-muted">${escapeHtml(formatBytes(item.size))} · ${escapeHtml(formatArweaveHistoryDateTime(item.uploadedAtMs))}</div>
<div class="mono-cell ar-attachment-history-txid">${escapeHtml(item.ar)}</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> <button class="${isSelected ? 'secondary-btn' : 'ghost-btn'}" type="button" data-pick="${index}" ${isSelected ? 'disabled' : ''}>${isSelected ? 'Уже добавлен ✓' : 'Выбрать'}</button>
</article> </article>
+42 -28
View File
@@ -1,7 +1,6 @@
import { renderHeader } from '../components/header.js';
import { import {
clearArweaveAttachmentHistory, clearArweaveAttachmentHistory,
formatArweaveHistoryDateTime, formatArweaveHistoryTime,
getArweaveAttachmentAvailability, getArweaveAttachmentAvailability,
openArweaveAttachmentManager, openArweaveAttachmentManager,
readArweaveAttachmentHistory, readArweaveAttachmentHistory,
@@ -10,7 +9,7 @@ import { formatBytes } from '../services/attachment-format.js';
import { toUserMessage } from '../services/ui-error-texts.js'; import { toUserMessage } from '../services/ui-error-texts.js';
import { state } from '../state.js'; import { state } from '../state.js';
export const pageMeta = { id: 'arweave-uploads-view', title: 'Загрузки в Arweave' }; export const pageMeta = { id: 'arweave-uploads-view', title: 'Загрузить файлы в блокчейн' };
function setStatusBadge(node, status, label) { function setStatusBadge(node, status, label) {
if (!node) return; if (!node) return;
@@ -22,10 +21,8 @@ function renderTile(item, index) {
const tile = document.createElement('article'); const tile = document.createElement('article');
tile.className = 'ar-attachment-history-tile ar-attachment-history-tile--page'; tile.className = 'ar-attachment-history-tile ar-attachment-history-tile--page';
const head = document.createElement('div');
head.className = 'ar-attachment-history-tile-head';
const name = document.createElement('strong'); const name = document.createElement('strong');
name.className = 'ar-attachment-history-name';
name.textContent = item.name; name.textContent = item.name;
const status = document.createElement('span'); const status = document.createElement('span');
@@ -34,15 +31,24 @@ function renderTile(item, index) {
status.textContent = 'Проверяем...'; status.textContent = 'Проверяем...';
const meta = document.createElement('div'); const meta = document.createElement('div');
meta.className = 'meta-muted'; meta.className = 'ar-attachment-history-meta-row';
meta.textContent = `${formatBytes(item.size)} · ${formatArweaveHistoryDateTime(item.uploadedAtMs)}`; const size = document.createElement('span');
size.textContent = formatBytes(item.size);
const time = document.createElement('span');
time.textContent = formatArweaveHistoryTime(item.uploadedAtMs);
meta.append(size, time, status);
if (item.pendingPlacement) {
const flag = document.createElement('span');
flag.className = 'ar-attachment-placement-flag';
flag.textContent = 'Не добавлен в SHiNE';
meta.append(flag);
}
const txId = document.createElement('div'); const txId = document.createElement('div');
txId.className = 'mono-cell ar-attachment-history-txid'; txId.className = 'mono-cell ar-attachment-history-txid';
txId.textContent = item.ar; txId.textContent = item.ar;
head.append(name, status); tile.append(name, meta, txId);
tile.append(head, meta, txId);
return tile; return tile;
} }
@@ -50,25 +56,16 @@ export function render({ navigate }) {
const screen = document.createElement('section'); const screen = document.createElement('section');
screen.className = 'stack arweave-uploads-screen'; screen.className = 'stack arweave-uploads-screen';
screen.append(
renderHeader({
title: 'Загрузки в Arweave',
leftAction: { label: '←', onClick: () => navigate('settings-view') },
}),
);
const intro = document.createElement('div');
intro.className = 'card stack';
intro.innerHTML = `
<p class="field-label">Журнал файлов этой сессии</p>
<p class="meta-muted">Здесь можно заранее загрузить файл в Arweave. Он попадёт только в журнал, а в сообщение его потом можно добавить из истории загрузок.</p>
`;
const controls = document.createElement('div'); const controls = document.createElement('div');
controls.className = 'card arweave-uploads-toolbar'; controls.className = 'arweave-uploads-toolbar';
controls.innerHTML = ` controls.innerHTML = `
<button class="secondary-btn arweave-uploads-back" type="button" data-action="back" aria-label="Назад">←</button>
<button class="primary-btn" type="button" data-action="upload">Добавить файл</button> <button class="primary-btn" type="button" data-action="upload">Добавить файл</button>
<button class="secondary-btn" type="button" data-action="clear">Очистить историю</button> <button class="secondary-btn arweave-uploads-menu-btn" type="button" data-action="menu" aria-label="Меню">⋯</button>
<div class="arweave-uploads-menu" data-menu="true" hidden>
<button class="text-btn" type="button" data-action="clear">Очистить историю</button>
<button class="text-btn" type="button" data-action="help">Справка</button>
</div>
`; `;
const statusLine = document.createElement('p'); const statusLine = document.createElement('p');
@@ -80,7 +77,7 @@ export function render({ navigate }) {
function renderHistory() { function renderHistory() {
const rows = readArweaveAttachmentHistory(state.session.login).slice().reverse(); const rows = readArweaveAttachmentHistory(state.session.login).slice().reverse();
list.innerHTML = ''; list.innerHTML = '';
statusLine.textContent = rows.length ? `Файлов в журнале: ${rows.length}` : 'Журнал пока пуст.'; statusLine.textContent = '';
if (!rows.length) { if (!rows.length) {
const empty = document.createElement('div'); const empty = document.createElement('div');
@@ -113,6 +110,7 @@ export function render({ navigate }) {
login: state.session.login, login: state.session.login,
storagePwd: state.session.storagePwdInMemory, storagePwd: state.session.storagePwdInMemory,
gateway: state.entrySettings.arweaveServer, gateway: state.entrySettings.arweaveServer,
historyOnly: true,
}); });
renderHistory(); renderHistory();
} catch (error) { } catch (error) {
@@ -120,14 +118,30 @@ export function render({ navigate }) {
} }
}); });
controls.querySelector('[data-action="back"]')?.addEventListener('click', () => navigate('settings-view'));
controls.querySelector('[data-action="menu"]')?.addEventListener('click', () => {
const menu = controls.querySelector('[data-menu="true"]');
if (menu) menu.hidden = !menu.hidden;
});
controls.querySelector('[data-action="clear"]')?.addEventListener('click', () => { controls.querySelector('[data-action="clear"]')?.addEventListener('click', () => {
const menu = controls.querySelector('[data-menu="true"]');
if (menu) menu.hidden = true;
const confirmed = window.confirm('Очистить журнал загруженных файлов этой сессии?'); const confirmed = window.confirm('Очистить журнал загруженных файлов этой сессии?');
if (!confirmed) return; if (!confirmed) return;
clearArweaveAttachmentHistory(state.session.login); clearArweaveAttachmentHistory(state.session.login);
renderHistory(); renderHistory();
}); });
controls.querySelector('[data-action="help"]')?.addEventListener('click', () => {
const menu = controls.querySelector('[data-menu="true"]');
if (menu) menu.hidden = true;
window.alert(
'Здесь вы можете заранее добавить файл в Arweave. Файл загрузится в блокчейн-хранилище и сохранится в журнале только этой браузерной сессии.\n\n'
+ 'Потом при создании сообщения можно открыть историю загрузок и выбрать уже загруженный файл по txId. '
+ 'Если файл только что загружен, gateway может несколько минут его не отдавать, поэтому статус может быть “ещё обновляется”.'
);
});
screen.append(intro, controls, statusLine, list); screen.append(controls, statusLine, list);
renderHistory(); renderHistory();
return screen; return screen;
} }
+2 -1
View File
@@ -12,7 +12,7 @@ import {
} from '../services/channels-ux.js'; } from '../services/channels-ux.js';
import { navigateBack } from '../router.js'; import { navigateBack } from '../router.js';
import { renderUserAvatar } from '../components/avatar-image.js'; import { renderUserAvatar } from '../components/avatar-image.js';
import { openArweaveAttachmentManager } from '../components/arweave-attachment-manager.js'; import { markArweaveAttachmentPlaced, openArweaveAttachmentManager } from '../components/arweave-attachment-manager.js';
import { import {
composeMessageWithAttachments, composeMessageWithAttachments,
createAttachmentCarouselElement, createAttachmentCarouselElement,
@@ -409,6 +409,7 @@ function openReplyModal({ onSubmit, navigate }) {
try { try {
await onSubmit(composeMessageWithAttachments(text, attachments)); await onSubmit(composeMessageWithAttachments(text, attachments));
attachments.forEach((attachment) => markArweaveAttachmentPlaced(state.session.login, attachment));
close(); close();
} catch (error) { } catch (error) {
setBusy(false); setBusy(false);
+3 -1
View File
@@ -18,7 +18,7 @@ import {
} from '../services/channels-ux.js'; } from '../services/channels-ux.js';
import { navigateBack } from '../router.js'; import { navigateBack } from '../router.js';
import { renderUserAvatar } from '../components/avatar-image.js'; import { renderUserAvatar } from '../components/avatar-image.js';
import { openArweaveAttachmentManager } from '../components/arweave-attachment-manager.js'; import { markArweaveAttachmentPlaced, openArweaveAttachmentManager } from '../components/arweave-attachment-manager.js';
import { import {
composeMessageWithAttachments, composeMessageWithAttachments,
createAttachmentCarouselElement, createAttachmentCarouselElement,
@@ -384,6 +384,7 @@ function openReplyModal({ onSubmit, navigate }) {
try { try {
await onSubmit(composeMessageWithAttachments(text, attachments)); await onSubmit(composeMessageWithAttachments(text, attachments));
attachments.forEach((attachment) => markArweaveAttachmentPlaced(state.session.login, attachment));
close(); close();
} catch (error) { } catch (error) {
setBusy(false); setBusy(false);
@@ -543,6 +544,7 @@ function openAddMessageModal({ channelName, onSubmit, navigate }) {
try { try {
await onSubmit(composeMessageWithAttachments(body, attachments)); await onSubmit(composeMessageWithAttachments(body, attachments));
attachments.forEach((attachment) => markArweaveAttachmentPlaced(state.session.login, attachment));
close(); close();
} catch (error) { } catch (error) {
setBusy(false); setBusy(false);
+1 -1
View File
@@ -56,7 +56,7 @@ export function render({ navigate }) {
</button> </button>
<button class="text-btn" type="button" id="settings-arweave-uploads"> <button class="text-btn" type="button" id="settings-arweave-uploads">
<span style="display:block; text-align:left;"> <span style="display:block; text-align:left;">
<strong>Загрузки в Arweave</strong> <strong>Загрузить файлы в блокчейн</strong>
<span class="meta-muted" style="display:block; margin-top:0.2rem;">Заранее загрузить файл и выбрать его потом из истории</span> <span class="meta-muted" style="display:block; margin-top:0.2rem;">Заранее загрузить файл и выбрать его потом из истории</span>
</span> </span>
</button> </button>
+85 -14
View File
@@ -2500,20 +2500,27 @@ textarea.input {
.ar-attachment-history-tiles { .ar-attachment-history-tiles {
display: grid; display: grid;
gap: 0.75rem; gap: 0.5rem;
max-height: 58vh; max-height: 58vh;
overflow: auto; overflow-x: hidden;
overflow-y: auto;
padding-right: 0.1rem; padding-right: 0.1rem;
width: 100%;
} }
.ar-attachment-history-tile { .ar-attachment-history-tile {
background: linear-gradient(135deg, rgba(248, 250, 252, 0.96), rgba(236, 253, 245, 0.96)); background: linear-gradient(135deg, rgba(248, 250, 252, 0.96), rgba(236, 253, 245, 0.96));
border: 1px solid rgba(20, 184, 166, 0.22); border: 1px solid rgba(20, 184, 166, 0.22);
border-radius: 1rem; border-radius: 0.8rem;
box-sizing: border-box;
color: #0f172a; color: #0f172a;
display: grid; display: grid;
gap: 0.45rem; gap: 0.32rem;
padding: 0.9rem; max-width: 100%;
min-width: 0;
overflow: hidden;
padding: 0.62rem 0.7rem;
width: 100%;
} }
.ar-attachment-history-tile.is-selected { .ar-attachment-history-tile.is-selected {
@@ -2532,6 +2539,16 @@ textarea.input {
justify-content: space-between; justify-content: space-between;
} }
.ar-attachment-history-name {
display: block;
font-size: 0.92rem;
line-height: 1.2;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ar-attachment-history-tile-head strong { .ar-attachment-history-tile-head strong {
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
@@ -2539,19 +2556,34 @@ textarea.input {
white-space: nowrap; white-space: nowrap;
} }
.ar-attachment-history-meta-row {
align-items: center;
color: #64748b;
display: flex;
flex-wrap: wrap;
font-size: 0.76rem;
gap: 0.35rem;
min-width: 0;
}
.ar-attachment-history-txid { .ar-attachment-history-txid {
background: rgba(15, 23, 42, 0.06); background: rgba(15, 23, 42, 0.06);
border-radius: 0.65rem; border-radius: 0.5rem;
padding: 0.45rem 0.55rem; font-size: 0.62rem;
line-height: 1.25;
overflow: hidden;
padding: 0.32rem 0.42rem;
text-overflow: ellipsis;
white-space: nowrap;
} }
.ar-attachment-status { .ar-attachment-status {
border-radius: 999px; border-radius: 999px;
flex: 0 0 auto; flex: 0 0 auto;
font-size: 0.74rem; font-size: 0.68rem;
font-weight: 800; font-weight: 800;
line-height: 1; line-height: 1;
padding: 0.35rem 0.55rem; padding: 0.28rem 0.42rem;
} }
.ar-attachment-status--available { .ar-attachment-status--available {
@@ -2569,20 +2601,59 @@ textarea.input {
color: #991b1b; color: #991b1b;
} }
.ar-attachment-placement-flag {
background: rgba(219, 234, 254, 0.96);
border-radius: 999px;
color: #1e3a8a;
flex: 0 0 auto;
font-size: 0.68rem;
font-weight: 800;
line-height: 1;
padding: 0.28rem 0.42rem;
}
.arweave-uploads-screen { .arweave-uploads-screen {
min-height: calc(100vh - 2rem); min-height: calc(100vh - 1rem);
} }
.arweave-uploads-toolbar { .arweave-uploads-toolbar {
align-items: center; align-items: center;
background: rgba(248, 250, 252, 0.96);
border: 1px solid rgba(15, 23, 42, 0.08);
border-radius: 0.9rem;
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.08);
display: flex; display: flex;
flex-wrap: wrap; gap: 0.5rem;
gap: 0.7rem; justify-content: space-between;
padding: 0.45rem;
position: sticky;
top: 0.5rem;
z-index: 15;
}
.arweave-uploads-back,
.arweave-uploads-menu-btn {
min-width: 2.6rem;
padding-inline: 0.7rem;
}
.arweave-uploads-menu {
background: #fff;
border: 1px solid rgba(15, 23, 42, 0.12);
border-radius: 0.85rem;
box-shadow: 0 18px 50px rgba(15, 23, 42, 0.18);
display: grid;
min-width: 13rem;
padding: 0.35rem;
position: absolute;
right: 0.45rem;
top: calc(100% + 0.35rem);
z-index: 20;
} }
.arweave-uploads-list { .arweave-uploads-list {
max-height: calc(100vh - 18rem); max-height: calc(100vh - 7.5rem);
min-height: 18rem; min-height: calc(100vh - 7.5rem);
} }
.mono-cell { .mono-cell {