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

This commit is contained in:
AidarKC
2026-07-31 13:13:58 +04:00
parent c530627078
commit 98eba54621
11 changed files with 310 additions and 27 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
client.version=1.4.2 client.version=1.4.3
server.version=1.4.0 server.version=1.4.0
+2
View File
@@ -61,6 +61,8 @@ UI поддерживает три сценария:
- выбрать файл из журнала файлов, загруженных в текущей браузерной сессии. - выбрать файл из журнала файлов, загруженных в текущей браузерной сессии.
Журнал хранится только в `sessionStorage` текущего браузера и не является частью блокчейна. Журнал хранится только в `sessionStorage` текущего браузера и не является частью блокчейна.
В настройках клиента есть отдельный экран журнала загрузок Arweave: пользователь может заранее загрузить файл в Arweave без создания сообщения, увидеть плитку с именем, размером, датой, `txId` и статусом доступности через gateway, а затем выбрать этот файл из истории при создании сообщения.
Если история очищена, уже созданные сообщения не меняются: в блокчейне остаются attach-блоки с `txId`.
## Отображение ## Отображение
+5
View File
@@ -1,5 +1,10 @@
# История изменений документации блокчейна # История изменений документации блокчейна
## 2026-07-31 13:12:24 +0400
- Базовый коммит-ориентир: `c530627`.
- Для UI-вложений добавлен отдельный экран журнала загрузок Arweave в настройках: загрузка файла без создания сообщения, плитки истории с `txId`, размером, датой и статусом доступности через gateway.
- Уточнено, что журнал хранится только в `sessionStorage` текущего браузера и не влияет на уже записанные attach-блоки в блокчейне.
## 2026-07-31 12:47:41 +0400 ## 2026-07-31 12:47:41 +0400
- Базовый коммит-ориентир: `f961947`. - Базовый коммит-ориентир: `f961947`.
- Для `SHiNE:attach v=1` уточнено клиентское отображение вложений: горизонтальная карусель над текстом сообщения, счётчик позиции, просмотр image/video в модальном окне и скачивание файлов. - Для `SHiNE:attach v=1` уточнено клиентское отображение вложений: горизонтальная карусель над текстом сообщения, счётчик позиции, просмотр image/video в модальном окне и скачивание файлов.
+2
View File
@@ -59,6 +59,7 @@ import * as settingsView from './pages/settings-view.js';
import * as accessServersView from './pages/access-servers-view.js'; import * as accessServersView from './pages/access-servers-view.js';
import * as developerSettingsView from './pages/developer-settings-view.js'; import * as developerSettingsView from './pages/developer-settings-view.js';
import * as serverSettingsView from './pages/server-settings-view.js?v=202606161240'; import * as serverSettingsView from './pages/server-settings-view.js?v=202606161240';
import * as arweaveUploadsView from './pages/arweave-uploads-view.js';
import * as remoteAddBlockSessionView from './pages/remote-addblock-session-view.js?v=202606281300'; import * as remoteAddBlockSessionView from './pages/remote-addblock-session-view.js?v=202606281300';
import * as deviceView from './pages/device-view.js?v=202606131435'; import * as deviceView from './pages/device-view.js?v=202606131435';
import * as connectDeviceView from './pages/connect-device-view.js?v=202606142055'; import * as connectDeviceView from './pages/connect-device-view.js?v=202606142055';
@@ -120,6 +121,7 @@ const routes = {
'access-servers-view': accessServersView, 'access-servers-view': accessServersView,
'developer-settings-view': developerSettingsView, 'developer-settings-view': developerSettingsView,
'server-settings-view': serverSettingsView, 'server-settings-view': serverSettingsView,
'arweave-uploads-view': arweaveUploadsView,
'remote-addblock-session-view': remoteAddBlockSessionView, 'remote-addblock-session-view': remoteAddBlockSessionView,
'device-view': deviceView, 'device-view': deviceView,
'connect-device-view': connectDeviceView, 'connect-device-view': connectDeviceView,
@@ -1,4 +1,5 @@
import { import {
buildArweaveDataUrl,
getArweaveUploadPrice, getArweaveUploadPrice,
loadArweaveFileMetadata, loadArweaveFileMetadata,
sha256HexFromArrayBuffer, sha256HexFromArrayBuffer,
@@ -10,8 +11,9 @@ import { escapeHtml, formatBytes, normalizeAttachment } from '../services/attach
const HISTORY_KEY = 'shine-ui-arweave-attachment-history-v1'; 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;
function readHistory(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) : {};
@@ -23,7 +25,7 @@ function readHistory(login) {
} }
} }
function saveHistory(login, rows) { export function saveArweaveAttachmentHistory(login, rows) {
try { try {
const key = String(login || '').trim().toLowerCase(); const key = String(login || '').trim().toLowerCase();
const raw = sessionStorage.getItem(HISTORY_KEY); const raw = sessionStorage.getItem(HISTORY_KEY);
@@ -35,20 +37,40 @@ function saveHistory(login, rows) {
} }
} }
function addToHistory(login, attachment) { export function clearArweaveAttachmentHistory(login) {
saveArweaveAttachmentHistory(login, []);
}
export function addArweaveAttachmentToHistory(login, attachment) {
const item = normalizeAttachment(attachment); const item = normalizeAttachment(attachment);
const rows = readHistory(login).filter((row) => row.ar !== item.ar); const rows = readArweaveAttachmentHistory(login).filter((row) => row.ar !== item.ar);
rows.unshift(item); rows.unshift(item);
saveHistory(login, rows); saveArweaveAttachmentHistory(login, rows);
return item; return item;
} }
function formatDateTime(ms) { export function formatArweaveHistoryDateTime(ms) {
const value = Number(ms || 0); const value = Number(ms || 0);
if (!Number.isFinite(value) || value <= 0) return '—'; if (!Number.isFinite(value) || value <= 0) return '—';
return new Date(value).toLocaleString('ru-RU'); return new Date(value).toLocaleString('ru-RU');
} }
export async function getArweaveAttachmentAvailability({ gateway, attachment } = {}) {
const item = normalizeAttachment(attachment);
const url = buildArweaveDataUrl({ gateway, txId: item.ar });
try {
const response = await fetch(url, { method: 'HEAD', cache: 'no-store' });
if (response.ok) return { status: 'available', label: '✓ Доступен', url };
} catch {
// gateway может временно не отвечать или блокировать HEAD; для UI это всё равно недоступно.
}
const ageMs = Date.now() - Number(item.uploadedAtMs || 0);
if (Number.isFinite(ageMs) && ageMs >= 0 && ageMs < RECENT_UPLOAD_MS) {
return { status: 'pending', label: '… Ещё обновляется', url };
}
return { status: 'unavailable', label: '✕ Недоступен', url };
}
function setText(node, text) { function setText(node, text) {
if (node) node.textContent = String(text || ''); if (node) node.textContent = String(text || '');
} }
@@ -64,6 +86,7 @@ export function openArweaveAttachmentManager({
storagePwd, storagePwd,
gateway, gateway,
onSelect, onSelect,
selectedTxIds = [],
} = {}) { } = {}) {
const cleanLogin = String(login || '').trim(); const cleanLogin = String(login || '').trim();
const cleanStoragePwd = String(storagePwd || '').trim(); const cleanStoragePwd = String(storagePwd || '').trim();
@@ -82,6 +105,7 @@ export function openArweaveAttachmentManager({
let priceInfo = null; let priceInfo = null;
let balanceInfo = null; let balanceInfo = null;
let autoOpenedFileDialog = false; let autoOpenedFileDialog = false;
const selectedTxIdSet = new Set((Array.isArray(selectedTxIds) ? selectedTxIds : []).map((item) => String(item || '').trim()).filter(Boolean));
function selectedWallet() { function selectedWallet() {
return wallets.find((item) => String(item.id) === String(selectedWalletId)) || wallets[0] || null; return wallets.find((item) => String(item.id) === String(selectedWalletId)) || wallets[0] || null;
@@ -95,7 +119,7 @@ export function openArweaveAttachmentManager({
} }
function finish(resolve, attachment) { function finish(resolve, attachment) {
const item = addToHistory(cleanLogin, attachment); const item = addArweaveAttachmentToHistory(cleanLogin, attachment);
if (typeof onSelect === 'function') onSelect(item); if (typeof onSelect === 'function') onSelect(item);
close(resolve, item); close(resolve, item);
} }
@@ -131,13 +155,14 @@ export function openArweaveAttachmentManager({
<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>
<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> <button class="secondary-btn" type="button" data-action="existing">Ввести txId</button>
<button class="secondary-btn" type="button" data-action="history">История</button> <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>Загрузить</button>
</div> </div>
<button class="secondary-btn" type="button" data-action="cancel">Отмена</button> <button class="secondary-btn" type="button" data-action="cancel">Отмена</button>
@@ -313,29 +338,27 @@ export function openArweaveAttachmentManager({
}; };
const showHistory = () => { const showHistory = () => {
const rows = readHistory(cleanLogin); const rows = readArweaveAttachmentHistory(cleanLogin).slice().reverse();
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 ar-attachment-manager-card--wide"> <div class="modal-card stack ar-attachment-manager-card ar-attachment-manager-card--wide">
<h3 class="modal-title">Журнал загруженных файлов</h3> <h3 class="modal-title">Журнал загруженных файлов</h3>
<p class="meta-muted">Журнал хранится только в этой браузерной сессии.</p> <p class="meta-muted">Журнал хранится только в этой браузерной сессии.</p>
<div class="ar-attachment-history-table-wrap"> <div class="ar-attachment-history-tiles" data-history-scroll="true">
<table class="ar-attachment-history-table"> ${rows.length ? rows.map((item, index) => {
<thead> const isSelected = selectedTxIdSet.has(item.ar);
<tr><th>Файл</th><th>txId</th><th>Размер</th><th>Дата</th><th></th></tr> return `
</thead> <article class="ar-attachment-history-tile${isSelected ? ' is-selected' : ''}" data-history-tile="${index}">
<tbody> <div class="ar-attachment-history-tile-head">
${rows.length ? rows.map((item, index) => ` <strong>${escapeHtml(item.name)}</strong>
<tr> <span class="ar-attachment-status ar-attachment-status--pending" data-status="${index}">Проверяем...</span>
<td>${escapeHtml(item.name)}</td> </div>
<td class="mono-cell">${escapeHtml(item.ar)}</td> <div class="meta-muted">${escapeHtml(formatBytes(item.size))} · ${escapeHtml(formatArweaveHistoryDateTime(item.uploadedAtMs))}</div>
<td>${escapeHtml(formatBytes(item.size))}</td> <div class="mono-cell ar-attachment-history-txid">${escapeHtml(item.ar)}</div>
<td>${escapeHtml(formatDateTime(item.uploadedAtMs))}</td> <button class="${isSelected ? 'secondary-btn' : 'ghost-btn'}" type="button" data-pick="${index}" ${isSelected ? 'disabled' : ''}>${isSelected ? 'Уже добавлен ✓' : 'Выбрать'}</button>
<td><button class="ghost-btn" type="button" data-pick="${index}">Выбрать</button></td> </article>
</tr> `;
`).join('') : '<tr><td colspan="5">В этой сессии ещё нет загруженных файлов.</td></tr>'} }).join('') : '<div class="card meta-muted">В этой сессии ещё нет загруженных файлов.</div>'}
</tbody>
</table>
</div> </div>
<div class="form-actions-grid"> <div class="form-actions-grid">
<button class="secondary-btn" type="button" data-action="back">Назад</button> <button class="secondary-btn" type="button" data-action="back">Назад</button>
@@ -344,6 +367,10 @@ export function openArweaveAttachmentManager({
</div> </div>
`; `;
bindBackdrop(); bindBackdrop();
const scrollEl = root.querySelector('[data-history-scroll="true"]');
window.setTimeout(() => {
if (scrollEl) scrollEl.scrollTop = scrollEl.scrollHeight;
}, 0);
root.querySelector('[data-action="back"]')?.addEventListener('click', showUpload); root.querySelector('[data-action="back"]')?.addEventListener('click', showUpload);
root.querySelectorAll('[data-pick]').forEach((button) => { root.querySelectorAll('[data-pick]').forEach((button) => {
button.addEventListener('click', () => { button.addEventListener('click', () => {
@@ -352,6 +379,20 @@ export function openArweaveAttachmentManager({
if (item) finish(resolve, item); if (item) finish(resolve, item);
}); });
}); });
rows.forEach((item, index) => {
const statusEl = root.querySelector(`[data-status="${index}"]`);
void getArweaveAttachmentAvailability({ gateway: cleanGateway, attachment: item })
.then((result) => {
if (!statusEl) return;
statusEl.className = `ar-attachment-status ar-attachment-status--${result.status}`;
statusEl.textContent = result.label;
})
.catch(() => {
if (!statusEl) return;
statusEl.className = 'ar-attachment-status ar-attachment-status--unavailable';
statusEl.textContent = 'Недоступен';
});
});
}; };
const showAddWallet = () => { const showAddWallet = () => {
+133
View File
@@ -0,0 +1,133 @@
import { renderHeader } from '../components/header.js';
import {
clearArweaveAttachmentHistory,
formatArweaveHistoryDateTime,
getArweaveAttachmentAvailability,
openArweaveAttachmentManager,
readArweaveAttachmentHistory,
} from '../components/arweave-attachment-manager.js';
import { formatBytes } from '../services/attachment-format.js';
import { toUserMessage } from '../services/ui-error-texts.js';
import { state } from '../state.js';
export const pageMeta = { id: 'arweave-uploads-view', title: 'Загрузки в Arweave' };
function setStatusBadge(node, status, label) {
if (!node) return;
node.className = `ar-attachment-status ar-attachment-status--${status || 'pending'}`;
node.textContent = label || 'Проверяем...';
}
function renderTile(item, index) {
const tile = document.createElement('article');
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');
name.textContent = item.name;
const status = document.createElement('span');
status.className = 'ar-attachment-status ar-attachment-status--pending';
status.dataset.statusIndex = String(index);
status.textContent = 'Проверяем...';
const meta = document.createElement('div');
meta.className = 'meta-muted';
meta.textContent = `${formatBytes(item.size)} · ${formatArweaveHistoryDateTime(item.uploadedAtMs)}`;
const txId = document.createElement('div');
txId.className = 'mono-cell ar-attachment-history-txid';
txId.textContent = item.ar;
head.append(name, status);
tile.append(head, meta, txId);
return tile;
}
export function render({ navigate }) {
const screen = document.createElement('section');
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');
controls.className = 'card arweave-uploads-toolbar';
controls.innerHTML = `
<button class="primary-btn" type="button" data-action="upload">Добавить файл</button>
<button class="secondary-btn" type="button" data-action="clear">Очистить историю</button>
`;
const statusLine = document.createElement('p');
statusLine.className = 'meta-muted inline-error';
const list = document.createElement('div');
list.className = 'ar-attachment-history-tiles arweave-uploads-list';
function renderHistory() {
const rows = readArweaveAttachmentHistory(state.session.login).slice().reverse();
list.innerHTML = '';
statusLine.textContent = rows.length ? `Файлов в журнале: ${rows.length}` : 'Журнал пока пуст.';
if (!rows.length) {
const empty = document.createElement('div');
empty.className = 'card meta-muted';
empty.textContent = 'Загруженные в этой сессии файлы появятся здесь.';
list.append(empty);
return;
}
rows.forEach((item, index) => {
list.append(renderTile(item, index));
const statusEl = list.querySelector(`[data-status-index="${index}"]`);
void getArweaveAttachmentAvailability({
gateway: state.entrySettings.arweaveServer,
attachment: item,
})
.then((result) => setStatusBadge(statusEl, result.status, result.label))
.catch(() => setStatusBadge(statusEl, 'unavailable', 'Недоступен'));
});
window.setTimeout(() => {
list.scrollTop = list.scrollHeight;
}, 0);
}
controls.querySelector('[data-action="upload"]')?.addEventListener('click', async () => {
statusLine.textContent = '';
try {
await openArweaveAttachmentManager({
login: state.session.login,
storagePwd: state.session.storagePwdInMemory,
gateway: state.entrySettings.arweaveServer,
});
renderHistory();
} catch (error) {
statusLine.textContent = toUserMessage(error, 'Не удалось загрузить файл.');
}
});
controls.querySelector('[data-action="clear"]')?.addEventListener('click', () => {
const confirmed = window.confirm('Очистить журнал загруженных файлов этой сессии?');
if (!confirmed) return;
clearArweaveAttachmentHistory(state.session.login);
renderHistory();
});
screen.append(intro, controls, statusLine, list);
renderHistory();
return screen;
}
+1
View File
@@ -426,6 +426,7 @@ function openReplyModal({ onSubmit, 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,
selectedTxIds: attachments.map((attachment) => attachment.ar),
}); });
if (!item) return; if (!item) return;
attachments.push(item); attachments.push(item);
+2
View File
@@ -401,6 +401,7 @@ function openReplyModal({ onSubmit, 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,
selectedTxIds: attachments.map((attachment) => attachment.ar),
}); });
if (!item) return; if (!item) return;
attachments.push(item); attachments.push(item);
@@ -559,6 +560,7 @@ function openAddMessageModal({ channelName, onSubmit, 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,
selectedTxIds: attachments.map((attachment) => attachment.ar),
}); });
if (!item) return; if (!item) return;
attachments.push(item); attachments.push(item);
+7
View File
@@ -54,6 +54,12 @@ export function render({ navigate }) {
<span class="meta-muted" style="display:block; margin-top:0.2rem;">Solana, SHiNE и Arweave для публичных данных</span> <span class="meta-muted" style="display:block; margin-top:0.2rem;">Solana, SHiNE и Arweave для публичных данных</span>
</span> </span>
</button> </button>
<button class="text-btn" type="button" id="settings-arweave-uploads">
<span style="display:block; text-align:left;">
<strong>Загрузки в Arweave</strong>
<span class="meta-muted" style="display:block; margin-top:0.2rem;">Заранее загрузить файл и выбрать его потом из истории</span>
</span>
</button>
<button class="text-btn" type="button" id="settings-language">Язык / Language</button> <button class="text-btn" type="button" id="settings-language">Язык / Language</button>
<button class="text-btn" type="button" id="settings-signout">Завершить текущий сеанс</button> <button class="text-btn" type="button" id="settings-signout">Завершить текущий сеанс</button>
`; `;
@@ -62,6 +68,7 @@ export function render({ navigate }) {
card.querySelector('#settings-remote-addblock').addEventListener('click', () => navigate('remote-addblock-session-view')); card.querySelector('#settings-remote-addblock').addEventListener('click', () => navigate('remote-addblock-session-view'));
card.querySelector('#settings-access-servers').addEventListener('click', () => navigate('access-servers-view')); card.querySelector('#settings-access-servers').addEventListener('click', () => navigate('access-servers-view'));
card.querySelector('#settings-blockchain-servers').addEventListener('click', () => navigate('server-settings-view')); card.querySelector('#settings-blockchain-servers').addEventListener('click', () => navigate('server-settings-view'));
card.querySelector('#settings-arweave-uploads').addEventListener('click', () => navigate('arweave-uploads-view'));
card.querySelector('#settings-language').addEventListener('click', () => navigate('language-view')); card.querySelector('#settings-language').addEventListener('click', () => navigate('language-view'));
const signOutBtn = card.querySelector('#settings-signout'); const signOutBtn = card.querySelector('#settings-signout');
+3
View File
@@ -31,6 +31,7 @@ const PRETTY_PATHS = new Map([
['settings-view', 'settings'], ['settings-view', 'settings'],
['access-servers-view', 'settings/access-servers'], ['access-servers-view', 'settings/access-servers'],
['server-settings-view', 'settings/servers'], ['server-settings-view', 'settings/servers'],
['arweave-uploads-view', 'settings/arweave-uploads'],
['developer-settings-view', 'settings/developer'], ['developer-settings-view', 'settings/developer'],
['trusted-device-login-settings-view', 'settings/device-login'], ['trusted-device-login-settings-view', 'settings/device-login'],
['language-view', 'settings/language'], ['language-view', 'settings/language'],
@@ -261,6 +262,7 @@ export function getRoute() {
const sub = decodePart(segments[1] || '').toLowerCase(); const sub = decodePart(segments[1] || '').toLowerCase();
if (sub === 'access-servers') return { pageId: 'access-servers-view', params: {} }; if (sub === 'access-servers') return { pageId: 'access-servers-view', params: {} };
if (sub === 'servers') return { pageId: 'server-settings-view', params: {} }; if (sub === 'servers') return { pageId: 'server-settings-view', params: {} };
if (sub === 'arweave-uploads') return { pageId: 'arweave-uploads-view', params: {} };
if (sub === 'developer') return { pageId: 'developer-settings-view', params: {} }; if (sub === 'developer') return { pageId: 'developer-settings-view', params: {} };
if (sub === 'device-login') return { pageId: 'trusted-device-login-settings-view', params: {} }; if (sub === 'device-login') return { pageId: 'trusted-device-login-settings-view', params: {} };
if (sub === 'language') return { pageId: 'language-view', params: {} }; if (sub === 'language') return { pageId: 'language-view', params: {} };
@@ -373,6 +375,7 @@ export function resolveToolbarActive(pageId) {
pageId === 'access-servers-view' || pageId === 'access-servers-view' ||
pageId === 'developer-settings-view' || pageId === 'developer-settings-view' ||
pageId === 'server-settings-view' || pageId === 'server-settings-view' ||
pageId === 'arweave-uploads-view' ||
pageId === 'remote-addblock-session-view' || pageId === 'remote-addblock-session-view' ||
pageId === 'device-view' || pageId === 'device-view' ||
pageId === 'connect-device-view' || pageId === 'connect-device-view' ||
+87
View File
@@ -2498,6 +2498,93 @@ textarea.input {
font-weight: 700; font-weight: 700;
} }
.ar-attachment-history-tiles {
display: grid;
gap: 0.75rem;
max-height: 58vh;
overflow: auto;
padding-right: 0.1rem;
}
.ar-attachment-history-tile {
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-radius: 1rem;
color: #0f172a;
display: grid;
gap: 0.45rem;
padding: 0.9rem;
}
.ar-attachment-history-tile.is-selected {
border-color: rgba(34, 197, 94, 0.55);
box-shadow: inset 0 0 0 1px rgba(34, 197, 94, 0.18);
}
.ar-attachment-history-tile--page {
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.08);
}
.ar-attachment-history-tile-head {
align-items: flex-start;
display: flex;
gap: 0.65rem;
justify-content: space-between;
}
.ar-attachment-history-tile-head strong {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ar-attachment-history-txid {
background: rgba(15, 23, 42, 0.06);
border-radius: 0.65rem;
padding: 0.45rem 0.55rem;
}
.ar-attachment-status {
border-radius: 999px;
flex: 0 0 auto;
font-size: 0.74rem;
font-weight: 800;
line-height: 1;
padding: 0.35rem 0.55rem;
}
.ar-attachment-status--available {
background: rgba(220, 252, 231, 0.96);
color: #166534;
}
.ar-attachment-status--pending {
background: rgba(254, 249, 195, 0.96);
color: #854d0e;
}
.ar-attachment-status--unavailable {
background: rgba(254, 226, 226, 0.96);
color: #991b1b;
}
.arweave-uploads-screen {
min-height: calc(100vh - 2rem);
}
.arweave-uploads-toolbar {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 0.7rem;
}
.arweave-uploads-list {
max-height: calc(100vh - 18rem);
min-height: 18rem;
}
.mono-cell { .mono-cell {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
overflow-wrap: anywhere; overflow-wrap: anywhere;