Jason Tudisco 7077191c0c NIP-78 encrypted transport, instant UI, re-sync recovery
Nostr transport:
- Switch from kind 1 to NIP-78 kind 30078 (application-specific data)
- Relays store events persistently — no inventory protocol needed
- AES-256-GCM encryption via Web Crypto API (room key = shared secret)
- PBKDF2 key derivation (100k iterations) from room key
- Chunking for large events (images >48KB) with per-chunk encryption
- Auto re-publish missing local events on room join
- Manual "Re-sync" button for recovery of failed publishes

Performance (all variants):
- Emit 'change' immediately after local store write (IDB/NeDB/SQLite)
- Folder and Nostr writes run fire-and-forget in background
- Fast event hashing: fingerprint metadata only, skip full payload SHA-256
- Saving spinner in paste UI while write completes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 01:11:29 -06:00

228 lines
5.2 KiB
CSS

:root {
--bg: #1a1a1e;
--bg-card: #24242a;
--bg-input: #2c2c34;
--border: #38383f;
--text: #e4e4e8;
--text-muted: #8888a0;
--accent: #6c8cff;
--accent-dim: #4a6ad0;
--success: #4caf80;
--error: #e05555;
--radius: 6px;
--mono: "SF Mono", "Cascadia Code", "Consolas", monospace;
--sans: -apple-system, "Segoe UI", system-ui, sans-serif;
}
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
color: var(--text);
font-family: var(--sans);
line-height: 1.5;
max-width: 720px;
margin: 0 auto;
padding: 2rem 1rem;
}
header { margin-bottom: 1.5rem; }
header h1 {
font-size: 1.4rem;
font-weight: 600;
color: var(--accent);
font-family: var(--mono);
letter-spacing: -0.02em;
}
.variant {
font-size: 0.75rem;
font-weight: 400;
color: var(--text-muted);
background: var(--bg-input);
padding: 0.15rem 0.5rem;
border-radius: 3px;
vertical-align: middle;
}
header .sub {
color: var(--text-muted);
font-size: 0.82rem;
margin-top: 0.2rem;
}
.folder-bar {
display: flex;
align-items: center;
gap: 0.75rem;
margin-top: 0.6rem;
}
.folder-btn {
padding: 0.45rem 0.9rem;
background: var(--accent);
color: #fff;
border: none;
border-radius: var(--radius);
font-size: 0.82rem;
font-family: var(--sans);
cursor: pointer;
transition: background 0.15s;
}
.folder-btn:hover { background: var(--accent-dim); }
.input-area { margin-bottom: 1.5rem; }
.input-row {
display: flex;
gap: 0.5rem;
align-items: center;
}
#paste-input {
flex: 1;
padding: 0.7rem 1rem;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text);
font-size: 0.95rem;
font-family: var(--sans);
outline: none;
transition: border-color 0.15s;
}
#paste-input:focus { border-color: var(--accent); }
#paste-input::placeholder { color: var(--text-muted); }
.clip-btn {
flex-shrink: 0;
width: 38px;
height: 38px;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text-muted);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: border-color 0.15s, color 0.15s;
}
.clip-btn:hover { border-color: var(--accent); color: var(--accent); }
.clip-btn svg { width: 18px; height: 18px; }
.status {
font-size: 0.78rem;
min-height: 1.2em;
color: var(--text-muted);
}
.status.ok { color: var(--success); }
.status.err { color: var(--error); }
.items { display: flex; flex-direction: column; gap: 0.5rem; }
.item {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 0.65rem 0.85rem;
display: flex;
gap: 0.7rem;
align-items: flex-start;
}
.item-thumb {
flex-shrink: 0;
width: 48px;
height: 48px;
border-radius: 4px;
object-fit: cover;
background: var(--bg);
}
.item-icon {
flex-shrink: 0;
width: 48px;
height: 48px;
border-radius: 4px;
background: var(--bg);
display: flex;
align-items: center;
justify-content: center;
}
.item-icon svg { width: 26px; height: 26px; }
.item-icon.pdf { color: #e05555; }
.item-icon.file { color: var(--text-muted); }
.item-filename {
font-size: 0.78rem;
margin-top: 0.2rem;
color: var(--accent);
}
.item-body { flex: 1; min-width: 0; }
.item-meta {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
font-size: 0.73rem;
color: var(--text-muted);
margin-bottom: 0.25rem;
}
.item-meta .hash {
font-family: var(--mono);
color: var(--accent-dim);
}
.item-content {
font-size: 0.88rem;
white-space: pre-wrap;
word-break: break-word;
max-height: 6em;
overflow: hidden;
}
.item-image { margin-top: 0.4rem; }
.item-image img {
max-width: 100%;
max-height: 200px;
border-radius: 4px;
}
.item-tags {
display: flex;
flex-wrap: wrap;
gap: 0.3rem;
margin-top: 0.3rem;
}
.tag {
font-size: 0.7rem;
font-family: var(--mono);
background: #2a2f45;
color: var(--accent);
padding: 0.1rem 0.45rem;
border-radius: 3px;
}
.empty {
text-align: center;
color: var(--text-muted);
padding: 3rem 1rem;
font-size: 0.9rem;
}
/* ── Saving spinner ────────────────────────────────────── */
.saving-spinner {
position: fixed;
top: 12px;
right: 16px;
display: flex;
align-items: center;
gap: 6px;
background: var(--bg-card);
border: 1px solid var(--accent-dim);
color: var(--accent);
font-size: 0.75rem;
font-family: var(--mono);
padding: 5px 12px;
border-radius: var(--radius);
z-index: 1000;
animation: fade-in 0.15s ease;
}
.saving-spinner.hidden {
display: none;
}
.spinner-dot {
width: 8px;
height: 8px;
border-radius: 50%;
border: 2px solid var(--accent-dim);
border-top-color: var(--accent);
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes fade-in {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}