new-card unread dot + pulse fix, relay inbox readable, UI polish pass
Board: new-card unread indicator (public/js/modules/board.js,
public/js/modules/buckets.js, public/js/modules/state.js,
public/css/style.css):
* state.unseenCardIds (Set) tracks card ids that arrived since the
last visit to the "all" view. Diff runs in renderBoard() against a
module-private knownCardIds snapshot.
* Bug fix: the first renderBoard runs with state.cards = [] (before
the API returns). The previous diff treated every card from the
real load as "new" and pulsed the entire board. Now the diff is
skipped when knownCardIds is null or empty — the first non-empty
render is a silent prime.
* When activeBucket === 'all' and unseenCardIds is non-empty, the set
is drained and every matching board card gets a 3-cycle accent glow
(`.card-pulse-new`, 0.8s × 3, pinned for anim-fast/slow). Works
both when navigating TO all and when a poll arrives while already
on all.
* When the user is on a different bucket: unseenCardIds accumulates
across polls and a red dot (`.bar-unread-dot`) appears on the "all"
tab. Position is top:3px right:3px *inside* the tab to survive
the double overflow clip (`#main-header overflow:hidden` +
`#bucket-bar overflow-x:auto`). renderBar() bakes the dot into the
all-tab template on every render so it survives bar re-renders that
aren't followed by a board render.
* decorateAllUnreadDot() in board.js is a fallback called at the end
of every renderBoard for cases where the diff changed unseenCardIds
but renderBar hasn't run yet.
Relay: readable inbox (relay/inc/routes/messages.php,
relay/ui/app.js, relay/ui/layout.php, relay/ui/style.css):
* New session-based GET /api/messages/inbox — returns up to 200
messages for the logged-in user, decrypted server-side with the
inter-tenant key (no extra leakage — the relay already sees
plaintext during the routing hop). Does NOT mark messages as
fetched; the hyprcrd client is still the canonical receiver.
* loadInbox() now fetches from /api/messages/inbox and renders
actual inbox items: sender (with user-circle icon + accent2),
optional title (bold), body (pre-wrap, max-height 120px), relative
timestamp (formatRelTime: "just now", "2 min ago", "today 14:32",
"3 d ago", or YYYY-MM-DD for older). Unfetched messages show a
small "new" accent badge next to the sender name.
* deleteMessage(id, btn) — confirms, calls existing DELETE
/api/messages/{id} (already session-aware), removes the DOM row,
re-loads the list, toasts "message deleted".
* Auto-poll: startInboxPoll / stopInboxPoll refresh the inbox every
15s while the compose view is active. Stops on view switch.
* Header-status badge: mirrors the real message count (not just the
/api/status count) and toggles the has-unread class + dot so the
tap-to-scroll-to-inbox target stays fresh.
Relay UI polish (relay/ui/style.css):
* .header-status is now a clickable button with tray icon, hover
tint, focus-ring, and a red dot (::after pseudo) when has-unread.
jumpToInbox() scrolls smoothly to #inbox-section and flashes it
with a 1.6s accent box-shadow pulse.
* .inbox-item .from is flex-row with icon + badge-new pill.
* .inbox-item .del gets hover-background for better mobile tappability.
* @keyframes flash-highlight for the scroll-to-inbox animation.
chore: bump version to 0.9.9
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
a67bedc6f2
commit
c16659c2ad
9 files changed
+773
-246
No files matched your search
+440
-204
@@ -5,55 +5,114 @@
|
||||
:root {
|
||||
--bg:#1e293b; --surface:#273349; --raised:#2e3d56; --border:#384a64;
|
||||
--dim:#64748b; --muted:#94a3b8; --text:#cbd5e1; --bright:#f1f5f9;
|
||||
--accent:#38bdf8; --accent2:#818cf8; --danger:#fb7185;
|
||||
--accent:#38bdf8; --accent2:#818cf8; --danger:#fb7185; --ok:#4ade80;
|
||||
--pixel:'Silkscreen', cursive;
|
||||
--mono:'JetBrains Mono', monospace;
|
||||
--radius:3px;
|
||||
--radius-lg:6px;
|
||||
--shadow-card:0 1px 2px rgba(0,0,0,0.3), 0 8px 24px -8px rgba(0,0,0,0.4);
|
||||
--shadow-popover:0 8px 24px rgba(0,0,0,0.4);
|
||||
--shadow-button:0 4px 12px -4px rgba(56,189,248,0.4);
|
||||
--focus-ring:0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
|
||||
}
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body {
|
||||
font-family:'JetBrains Mono', monospace;
|
||||
font-family:var(--mono);
|
||||
background:var(--bg); color:var(--text);
|
||||
min-height:100dvh; display:flex; flex-direction:column;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.pixel { font-family:var(--pixel); }
|
||||
.hidden { display:none !important; }
|
||||
.shrink { flex-shrink:0; }
|
||||
.spacer { flex:1; }
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────────
|
||||
Header
|
||||
───────────────────────────────────────────────────────────────── */
|
||||
|
||||
/* ── Header ── */
|
||||
header {
|
||||
padding:12px 20px; border-bottom:1px solid var(--border);
|
||||
display:flex; align-items:center; gap:12px; flex-shrink:0;
|
||||
background:var(--surface);
|
||||
position:relative;
|
||||
}
|
||||
header h1 { font-family:var(--pixel); font-size:14px; color:var(--accent); }
|
||||
header .brand-icon {
|
||||
color:var(--accent); font-size:18px; line-height:1;
|
||||
filter: drop-shadow(0 0 8px color-mix(in srgb, var(--accent) 35%, transparent));
|
||||
}
|
||||
header h1 {
|
||||
font-family:var(--pixel); font-size:14px; color:var(--accent);
|
||||
letter-spacing:0.05em;
|
||||
}
|
||||
.header-status {
|
||||
background:none; border:none; padding:4px 8px; cursor:pointer;
|
||||
font-family:var(--pixel); font-size:9px; color:var(--dim);
|
||||
display:inline-flex; align-items:center; gap:6px;
|
||||
border-radius:var(--radius);
|
||||
transition: color .15s, background .15s;
|
||||
}
|
||||
.header-status .ph { font-size:12px; }
|
||||
.header-status:hover {
|
||||
color: var(--accent);
|
||||
background: color-mix(in srgb, var(--accent) 10%, transparent);
|
||||
}
|
||||
.header-status.has-unread {
|
||||
color: var(--accent);
|
||||
position: relative;
|
||||
}
|
||||
.header-status.has-unread::after {
|
||||
content:''; position:absolute; top:2px; right:2px;
|
||||
width:6px; height:6px; border-radius:50%;
|
||||
background:var(--danger);
|
||||
box-shadow: 0 0 6px color-mix(in srgb, var(--danger) 60%, transparent);
|
||||
}
|
||||
.header-status:focus-visible { outline:none; box-shadow: var(--focus-ring); }
|
||||
header .dot {
|
||||
width:8px; height:8px; border-radius:50%;
|
||||
background:var(--accent); animation:pulse 2s ease-in-out infinite;
|
||||
box-shadow: 0 0 8px color-mix(in srgb, var(--accent) 60%, transparent);
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%,100% { opacity:1; transform:scale(1); }
|
||||
50% { opacity:0.4; transform:scale(0.85); }
|
||||
}
|
||||
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.3} }
|
||||
header .spacer { flex:1; }
|
||||
header .user {
|
||||
font-family:var(--pixel); font-size:10px; color:var(--muted);
|
||||
display:flex; align-items:center; gap:6px;
|
||||
}
|
||||
header .user .badge {
|
||||
background:var(--accent); color:var(--bg);
|
||||
/* Match the .nav-btn box-model exactly so the badge sits flush with
|
||||
the header buttons instead of being a tiny pill in the middle. */
|
||||
padding:5px 10px; font-size:9px;
|
||||
border:1px solid var(--accent); border-radius:3px;
|
||||
border:1px solid var(--accent); border-radius:var(--radius);
|
||||
line-height:1; font-family:var(--pixel);
|
||||
}
|
||||
header .nav-btn {
|
||||
background:none; border:1px solid var(--border); color:var(--muted);
|
||||
cursor:pointer; padding:5px 10px;
|
||||
font-family:var(--pixel); font-size:9px; border-radius:3px;
|
||||
font-family:var(--pixel); font-size:9px; border-radius:var(--radius);
|
||||
transition:all .15s;
|
||||
}
|
||||
header .nav-btn:hover { color:var(--accent); border-color:var(--accent); }
|
||||
header .nav-btn.active { background:var(--accent); color:var(--bg); border-color:var(--accent); }
|
||||
header .nav-btn.danger:hover { color:var(--danger); border-color:var(--danger); }
|
||||
header .nav-btn:hover {
|
||||
color:var(--accent); border-color:var(--accent);
|
||||
background: color-mix(in srgb, var(--accent) 8%, transparent);
|
||||
}
|
||||
header .nav-btn:focus-visible {
|
||||
outline:none; box-shadow: var(--focus-ring);
|
||||
}
|
||||
header .nav-btn.active {
|
||||
background:var(--accent); color:var(--bg); border-color:var(--accent);
|
||||
box-shadow: 0 0 12px -2px color-mix(in srgb, var(--accent) 50%, transparent);
|
||||
}
|
||||
header .nav-btn.danger:hover {
|
||||
color:var(--danger); border-color:var(--danger);
|
||||
background: color-mix(in srgb, var(--danger) 8%, transparent);
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────────
|
||||
Layout shell
|
||||
───────────────────────────────────────────────────────────────── */
|
||||
|
||||
/* ── Layouts ── */
|
||||
main {
|
||||
flex:1; display:flex; flex-direction:column;
|
||||
padding:20px; gap:16px; max-width:920px; width:100%;
|
||||
@@ -61,97 +120,183 @@ main {
|
||||
}
|
||||
@media (max-width: 600px) { main { padding:12px; gap:12px; } }
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────────
|
||||
Form primitives — used by every view (auth, compose, account, admin)
|
||||
───────────────────────────────────────────────────────────────── */
|
||||
|
||||
.field { display:flex; flex-direction:column; gap:5px; width:100%; }
|
||||
.field-label,
|
||||
.field > label {
|
||||
font-family:var(--pixel); font-size:9px; color:var(--dim);
|
||||
text-transform:uppercase; letter-spacing:0.05em;
|
||||
}
|
||||
.input,
|
||||
.field input[type="text"],
|
||||
.field input[type="password"],
|
||||
.field input[type="email"],
|
||||
.field select,
|
||||
.field textarea {
|
||||
background:var(--bg); border:1px solid var(--border);
|
||||
color:var(--bright); padding:9px 12px; font-size:13px;
|
||||
font-family:var(--mono); border-radius:var(--radius);
|
||||
transition:border-color .15s, box-shadow .15s, background .15s;
|
||||
width:100%;
|
||||
}
|
||||
.input:focus,
|
||||
.field input:focus,
|
||||
.field select:focus,
|
||||
.field textarea:focus {
|
||||
border-color:var(--accent); outline:none;
|
||||
box-shadow: var(--focus-ring);
|
||||
background: color-mix(in srgb, var(--bg) 96%, var(--accent) 4%);
|
||||
}
|
||||
.input::placeholder,
|
||||
.field input::placeholder,
|
||||
.field textarea::placeholder { color:var(--dim); }
|
||||
.field textarea { min-height:140px; resize:vertical; line-height:1.55; }
|
||||
.field-hint {
|
||||
font-size:9px; color:var(--dim); line-height:1.4;
|
||||
font-family:var(--mono);
|
||||
}
|
||||
|
||||
/* Inline label-on-checkbox style */
|
||||
label.checkbox {
|
||||
font-family:var(--pixel); font-size:9px; color:var(--dim);
|
||||
text-transform:uppercase;
|
||||
display:flex; align-items:center; gap:6px; cursor:pointer;
|
||||
}
|
||||
label.checkbox input[type="checkbox"] { accent-color:var(--accent); cursor:pointer; }
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────────
|
||||
Buttons
|
||||
───────────────────────────────────────────────────────────────── */
|
||||
|
||||
.btn {
|
||||
padding:10px 18px; border:1px solid transparent; border-radius:var(--radius);
|
||||
font-family:var(--pixel); font-size:11px; cursor:pointer;
|
||||
transition:all .15s;
|
||||
display:inline-flex; align-items:center; gap:6px;
|
||||
justify-content:center;
|
||||
}
|
||||
.btn:active { transform: translateY(1px); }
|
||||
.btn:focus-visible { outline:none; box-shadow: var(--focus-ring); }
|
||||
|
||||
.btn-primary { background:var(--accent); color:var(--bg); border-color:var(--accent); }
|
||||
.btn-primary:hover {
|
||||
filter:brightness(1.12);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-button);
|
||||
}
|
||||
.btn-secondary {
|
||||
background:transparent; border-color:var(--border); color:var(--muted);
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
color:var(--accent); border-color:var(--accent);
|
||||
background: color-mix(in srgb, var(--accent) 8%, transparent);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.btn-danger {
|
||||
background:transparent; border-color:var(--danger); color:var(--danger);
|
||||
}
|
||||
.btn-danger:hover {
|
||||
background:var(--danger); color:var(--bg);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────────
|
||||
Cards (compose, sections)
|
||||
───────────────────────────────────────────────────────────────── */
|
||||
|
||||
.compose-card,
|
||||
.section {
|
||||
background:var(--surface); border:1px solid var(--border);
|
||||
border-radius:var(--radius-lg); padding:18px;
|
||||
display:flex; flex-direction:column; gap:12px;
|
||||
}
|
||||
.compose-card h2,
|
||||
.section h2 {
|
||||
font-family:var(--pixel); font-size:11px; color:var(--accent);
|
||||
text-transform:uppercase; letter-spacing:0.05em;
|
||||
display:flex; align-items:center; gap:8px;
|
||||
}
|
||||
.section h2 .count {
|
||||
background:var(--bg); padding:3px 7px; border-radius:var(--radius);
|
||||
font-size:9px; color:var(--muted);
|
||||
border:1px solid var(--border);
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────────
|
||||
Compose
|
||||
───────────────────────────────────────────────────────────────── */
|
||||
|
||||
#view-compose:not(.hidden) {
|
||||
display: flex; flex-direction: column; gap: 16px;
|
||||
}
|
||||
.compose-card .actions {
|
||||
display:flex; gap:8px; align-items:center;
|
||||
}
|
||||
.compose-card .actions .spacer { flex:1; }
|
||||
|
||||
/* Status text (used in compose, account, create-user) */
|
||||
.status { font-family:var(--pixel); font-size:9px; color:var(--dim); }
|
||||
.status.ok { color:var(--ok); }
|
||||
.status.err { color:var(--danger); }
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────────
|
||||
Auth forms (setup, login, change-password)
|
||||
───────────────────────────────────────────────────────────────── */
|
||||
|
||||
.auth-form {
|
||||
display:flex; flex-direction:column; align-items:center;
|
||||
padding:48px 24px; gap:14px; max-width:380px; margin:0 auto;
|
||||
padding:40px 28px; gap:14px; max-width:400px; margin:48px auto 0;
|
||||
background:var(--surface); border:1px solid var(--border);
|
||||
border-radius:var(--radius-lg);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
.auth-form .auth-icon {
|
||||
font-size:36px; color:var(--accent); margin-bottom:4px;
|
||||
filter: drop-shadow(0 0 16px color-mix(in srgb, var(--accent) 40%, transparent));
|
||||
}
|
||||
.auth-form h2 {
|
||||
font-family:var(--pixel); font-size:14px; color:var(--accent);
|
||||
margin-bottom:4px; letter-spacing:0.05em;
|
||||
}
|
||||
.auth-form p {
|
||||
font-size:11px; color:var(--muted); text-align:center; line-height:1.55;
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
.auth-form h2 { font-family:var(--pixel); font-size:13px; color:var(--accent); margin-bottom:4px; }
|
||||
.auth-form p { font-size:11px; color:var(--muted); text-align:center; line-height:1.5; }
|
||||
.auth-form .field { width:100%; }
|
||||
.auth-form .field label {
|
||||
font-family:var(--pixel); font-size:9px; color:var(--dim);
|
||||
text-transform:uppercase; display:block; margin-bottom:4px;
|
||||
}
|
||||
.auth-form input {
|
||||
width:100%; background:var(--surface); border:1px solid var(--border);
|
||||
color:var(--bright); padding:9px 12px; font-size:13px;
|
||||
font-family:'JetBrains Mono', monospace; border-radius:3px;
|
||||
}
|
||||
.auth-form input:focus { border-color:var(--accent); outline:none; }
|
||||
.auth-form .btn { width:100%; }
|
||||
.auth-form .err {
|
||||
font-family:var(--pixel); font-size:9px; color:var(--danger);
|
||||
min-height:11px; text-align:center;
|
||||
}
|
||||
|
||||
/* ── Buttons ── */
|
||||
.btn {
|
||||
padding:10px 18px; border:none; border-radius:3px;
|
||||
font-family:var(--pixel); font-size:11px; cursor:pointer;
|
||||
transition:all .15s;
|
||||
/* Flash-highlight: a brief accent pulse on a card after the user
|
||||
tapped the header inbox link and we just scrolled it into view. */
|
||||
@keyframes flash-highlight {
|
||||
0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 60%, transparent); }
|
||||
50% { box-shadow: 0 0 0 6px color-mix(in srgb, var(--accent) 30%, transparent),
|
||||
0 0 24px 4px color-mix(in srgb, var(--accent) 50%, transparent); }
|
||||
100% { box-shadow: 0 0 0 0 transparent; }
|
||||
}
|
||||
.flash-highlight {
|
||||
animation: flash-highlight 1.6s ease-out;
|
||||
}
|
||||
.btn:active { opacity:.7; }
|
||||
.btn-primary { background:var(--accent); color:var(--bg); }
|
||||
.btn-primary:hover { filter:brightness(1.1); }
|
||||
.btn-secondary { background:transparent; border:1px solid var(--border); color:var(--muted); }
|
||||
.btn-secondary:hover { color:var(--accent); border-color:var(--accent); }
|
||||
.btn-danger { background:transparent; border:1px solid var(--danger); color:var(--danger); }
|
||||
.btn-danger:hover { background:var(--danger); color:var(--bg); }
|
||||
|
||||
/* ── Compose view ── */
|
||||
#view-compose:not(.hidden) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.compose-card {
|
||||
background:var(--surface); border:1px solid var(--border);
|
||||
border-radius:4px; padding:16px;
|
||||
display:flex; flex-direction:column; gap:10px;
|
||||
}
|
||||
.compose-card h2 {
|
||||
font-family:var(--pixel); font-size:11px; color:var(--accent);
|
||||
text-transform:uppercase;
|
||||
}
|
||||
.compose-card .field { display:flex; flex-direction:column; gap:4px; }
|
||||
.compose-card .field label {
|
||||
font-family:var(--pixel); font-size:9px; color:var(--dim);
|
||||
text-transform:uppercase;
|
||||
}
|
||||
.compose-card input, .compose-card select, .compose-card textarea {
|
||||
background:var(--bg); border:1px solid var(--border); color:var(--bright);
|
||||
padding:8px 10px; font-size:13px;
|
||||
font-family:'JetBrains Mono', monospace; border-radius:3px;
|
||||
}
|
||||
.compose-card input:focus, .compose-card select:focus, .compose-card textarea:focus {
|
||||
border-color:var(--accent); outline:none;
|
||||
}
|
||||
.compose-card textarea { min-height:140px; resize:vertical; line-height:1.55; }
|
||||
.compose-card .actions { display:flex; gap:8px; align-items:center; }
|
||||
.compose-card .actions .spacer { flex:1; }
|
||||
.compose-card .status { font-family:var(--pixel); font-size:9px; color:var(--dim); }
|
||||
.compose-card .status.ok { color:var(--accent); }
|
||||
.compose-card .status.err { color:var(--danger); }
|
||||
|
||||
/* ── Inbox + admin lists ── */
|
||||
.section {
|
||||
background:var(--surface); border:1px solid var(--border);
|
||||
border-radius:4px; padding:16px;
|
||||
}
|
||||
.section h2 {
|
||||
font-family:var(--pixel); font-size:11px; color:var(--accent);
|
||||
text-transform:uppercase; margin-bottom:10px;
|
||||
display:flex; align-items:center; gap:8px;
|
||||
}
|
||||
.section h2 .count {
|
||||
background:var(--bg); padding:2px 6px; border-radius:2px;
|
||||
font-size:9px; color:var(--muted);
|
||||
}
|
||||
.section .empty { font-size:11px; color:var(--dim); font-style:italic; padding:8px 0; }
|
||||
/* ─────────────────────────────────────────────────────────────────
|
||||
Inbox + lists
|
||||
───────────────────────────────────────────────────────────────── */
|
||||
|
||||
.inbox-item {
|
||||
display:flex; align-items:flex-start; gap:10px;
|
||||
padding:10px 0; border-bottom:1px solid var(--border);
|
||||
padding:12px 10px; margin: 0 -10px;
|
||||
border-bottom:1px solid var(--border);
|
||||
transition: background .12s;
|
||||
border-radius:var(--radius);
|
||||
}
|
||||
.inbox-item:hover {
|
||||
background: color-mix(in srgb, var(--raised) 70%, transparent);
|
||||
}
|
||||
.inbox-item:last-child { border-bottom:none; }
|
||||
.inbox-item .meta {
|
||||
@@ -167,95 +312,218 @@ main {
|
||||
font-size:11px; color:var(--text); white-space:pre-wrap;
|
||||
word-break:break-word; max-height:120px; overflow:auto;
|
||||
}
|
||||
.inbox-item .from { font-family:var(--pixel); font-size:9px; color:var(--accent2); }
|
||||
.inbox-item .from {
|
||||
font-family:var(--pixel); font-size:9px; color:var(--accent2);
|
||||
display:inline-flex; align-items:center; gap:6px;
|
||||
margin-bottom:4px;
|
||||
}
|
||||
.inbox-item .from .ph { font-size:11px; }
|
||||
.badge-new {
|
||||
display:inline-block;
|
||||
background:var(--accent); color:var(--bg);
|
||||
padding:1px 6px; border-radius:var(--radius);
|
||||
font-family:var(--pixel); font-size:8px;
|
||||
text-transform:uppercase; letter-spacing:0.05em;
|
||||
margin-left:4px;
|
||||
}
|
||||
.inbox-item .del {
|
||||
background:none; border:none; color:var(--dim); cursor:pointer;
|
||||
padding:4px; font-size:14px; flex-shrink:0;
|
||||
padding:6px; font-size:14px; flex-shrink:0;
|
||||
border-radius:var(--radius);
|
||||
transition: color .15s, background .15s;
|
||||
}
|
||||
.inbox-item .del:hover {
|
||||
color:var(--danger);
|
||||
background: color-mix(in srgb, var(--danger) 10%, transparent);
|
||||
}
|
||||
.inbox-item .del:hover { color:var(--danger); }
|
||||
|
||||
/* User row (admin) */
|
||||
.user-row {
|
||||
display:flex; align-items:center; gap:12px;
|
||||
padding:10px 0; border-bottom:1px solid var(--border);
|
||||
padding:12px 10px; margin: 0 -10px;
|
||||
border-bottom:1px solid var(--border);
|
||||
border-radius:var(--radius);
|
||||
transition: background .12s;
|
||||
}
|
||||
.user-row:hover {
|
||||
background: color-mix(in srgb, var(--raised) 70%, transparent);
|
||||
}
|
||||
.user-row:last-child { border-bottom:none; }
|
||||
.user-row .info { flex:1; }
|
||||
.user-row .name { font-family:var(--pixel); font-size:11px; color:var(--bright); }
|
||||
.user-row .meta { font-size:10px; color:var(--muted); margin-top:2px; }
|
||||
.user-row .name {
|
||||
font-family:var(--pixel); font-size:11px; color:var(--bright);
|
||||
display:flex; align-items:center; gap:6px;
|
||||
}
|
||||
.user-row .meta { font-size:10px; color:var(--muted); margin-top:3px; }
|
||||
.user-row .badge {
|
||||
background:var(--accent); color:var(--bg);
|
||||
padding:1px 6px; font-family:var(--pixel); font-size:8px;
|
||||
text-transform:uppercase; border-radius:2px;
|
||||
padding:2px 7px; font-family:var(--pixel); font-size:8px;
|
||||
text-transform:uppercase; border-radius:var(--radius);
|
||||
}
|
||||
.user-row .actions { display:flex; gap:6px; }
|
||||
.user-row .actions .btn { padding:4px 10px; font-size:9px; }
|
||||
.user-row .actions .btn { padding:5px 10px; font-size:9px; }
|
||||
|
||||
/* User-row icon */
|
||||
.user-row .user-icon { font-size:20px; color:var(--accent); flex-shrink:0; }
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────────
|
||||
Empty state — used by inbox, users list, etc.
|
||||
───────────────────────────────────────────────────────────────── */
|
||||
|
||||
.empty-state {
|
||||
display:flex; flex-direction:column; align-items:center;
|
||||
justify-content:center; padding:36px 12px; gap:8px;
|
||||
color:var(--dim); text-align:center;
|
||||
}
|
||||
.empty-state .icon {
|
||||
font-size:38px; color:var(--dim); opacity:0.6;
|
||||
}
|
||||
.empty-state .label {
|
||||
font-family:var(--pixel); font-size:10px; color:var(--muted);
|
||||
text-transform:uppercase; letter-spacing:0.05em;
|
||||
}
|
||||
.empty-state .hint {
|
||||
font-size:11px; color:var(--dim); line-height:1.5;
|
||||
max-width:280px;
|
||||
}
|
||||
.empty {
|
||||
font-size:11px; color:var(--dim); font-style:italic; padding:8px 0;
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────────
|
||||
Loading skeleton (used by inbox / users list while fetching)
|
||||
───────────────────────────────────────────────────────────────── */
|
||||
|
||||
.skeleton-row {
|
||||
display:flex; align-items:center; gap:10px; padding:12px 10px;
|
||||
border-bottom:1px solid var(--border);
|
||||
}
|
||||
.skeleton-row:last-child { border-bottom:none; }
|
||||
.skel {
|
||||
background: linear-gradient(90deg,
|
||||
color-mix(in srgb, var(--raised) 60%, transparent) 0%,
|
||||
color-mix(in srgb, var(--raised) 90%, transparent) 50%,
|
||||
color-mix(in srgb, var(--raised) 60%, transparent) 100%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton 1.4s ease-in-out infinite;
|
||||
border-radius:2px; height:10px; width:60%;
|
||||
}
|
||||
.skel.short { width:30%; }
|
||||
.skel.tall { height:14px; }
|
||||
@keyframes skeleton {
|
||||
0% { background-position:200% 0; }
|
||||
100% { background-position:-200% 0; }
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────────
|
||||
Account view — secret display + input row
|
||||
───────────────────────────────────────────────────────────────── */
|
||||
|
||||
.account-secret-row {
|
||||
display:flex; gap:8px; align-items:stretch;
|
||||
}
|
||||
.account-secret-row .input { flex:1; }
|
||||
.account-secret-block { margin-bottom:14px; }
|
||||
.account-divider {
|
||||
text-align:center; color:var(--dim);
|
||||
font-family:var(--pixel); font-size:9px;
|
||||
text-transform:uppercase; letter-spacing:0.05em;
|
||||
margin: 4px 0;
|
||||
}
|
||||
.account-secret-grid {
|
||||
display:grid; grid-template-columns:1fr; gap:14px;
|
||||
}
|
||||
.account-secret-actions {
|
||||
margin-top:24px; padding-top:14px; border-top:1px solid var(--border);
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────────
|
||||
Create user form (admin)
|
||||
───────────────────────────────────────────────────────────────── */
|
||||
|
||||
.create-user-form {
|
||||
display:flex; flex-direction:column; gap:10px;
|
||||
display:flex; flex-direction:column; gap:12px;
|
||||
padding-top:14px; border-top:1px solid var(--border); margin-top:14px;
|
||||
}
|
||||
.create-user-form .field-label {
|
||||
font-family:var(--pixel); font-size:9px; color:var(--dim);
|
||||
text-transform:uppercase; display:block; margin-bottom:4px;
|
||||
}
|
||||
.create-user-form input[type="text"],
|
||||
.create-user-form input[type="password"] {
|
||||
width:100%;
|
||||
background:var(--bg); border:1px solid var(--border); color:var(--bright);
|
||||
padding:8px 11px; font-size:12px;
|
||||
font-family:'JetBrains Mono', monospace; border-radius:3px;
|
||||
}
|
||||
.create-user-form input:focus { border-color:var(--accent); outline:none; }
|
||||
.create-user-form label.checkbox {
|
||||
font-family:var(--pixel); font-size:9px; color:var(--dim);
|
||||
text-transform:uppercase;
|
||||
display:flex; align-items:center; gap:6px; cursor:pointer;
|
||||
}
|
||||
.create-user-form label.checkbox input[type="checkbox"] {
|
||||
accent-color:var(--accent); cursor:pointer;
|
||||
}
|
||||
.create-user-form .actions {
|
||||
display:flex; gap:8px; align-items:center;
|
||||
margin-top:4px;
|
||||
display:flex; gap:8px; align-items:center; margin-top:4px;
|
||||
}
|
||||
|
||||
.toast {
|
||||
/* ─────────────────────────────────────────────────────────────────
|
||||
Toast — single + stack support
|
||||
───────────────────────────────────────────────────────────────── */
|
||||
|
||||
#toast-stack {
|
||||
position:fixed; top:20px; right:20px;
|
||||
background:var(--accent); color:var(--bg);
|
||||
padding:10px 18px; border-radius:3px;
|
||||
font-family:var(--pixel); font-size:11px;
|
||||
opacity:0; transition:opacity .3s; z-index:100; pointer-events:none;
|
||||
display:flex; flex-direction:column; gap:8px;
|
||||
z-index:200; pointer-events:none;
|
||||
}
|
||||
.toast.show { opacity:1; }
|
||||
.toast.err { background:var(--danger); }
|
||||
.toast {
|
||||
background:var(--surface); color:var(--bright);
|
||||
padding:10px 14px; border-radius:var(--radius-lg);
|
||||
font-family:var(--pixel); font-size:10px;
|
||||
display:flex; align-items:center; gap:8px;
|
||||
border:1px solid var(--border);
|
||||
box-shadow: var(--shadow-popover);
|
||||
pointer-events:auto;
|
||||
transform: translateX(120%);
|
||||
transition: transform .25s cubic-bezier(0.16, 1, 0.3, 1), opacity .25s;
|
||||
opacity:0;
|
||||
min-width:180px; max-width:320px;
|
||||
}
|
||||
.toast.show { transform: translateX(0); opacity:1; }
|
||||
.toast.fade-out { opacity:0; transform: translateX(20%); }
|
||||
.toast .toast-icon { font-size:14px; flex-shrink:0; }
|
||||
.toast .toast-msg { line-height:1.3; word-break:break-word; }
|
||||
.toast.info { border-color: color-mix(in srgb, var(--accent) 60%, var(--border)); }
|
||||
.toast.info .toast-icon { color: var(--accent); }
|
||||
.toast.ok { border-color: color-mix(in srgb, var(--ok) 60%, var(--border)); }
|
||||
.toast.ok .toast-icon { color: var(--ok); }
|
||||
.toast.err { border-color: color-mix(in srgb, var(--danger) 60%, var(--border)); }
|
||||
.toast.err .toast-icon { color: var(--danger); }
|
||||
|
||||
.notice { padding:10px 14px; border-radius:3px; font-size:11px; margin-bottom:12px; }
|
||||
.notice.info { background:rgba(56,189,248,0.1); border:1px solid var(--accent); color:var(--accent); }
|
||||
.notice.warn { background:rgba(251,113,133,0.1); border:1px solid var(--danger); color:var(--danger); }
|
||||
/* ─────────────────────────────────────────────────────────────────
|
||||
Notices
|
||||
───────────────────────────────────────────────────────────────── */
|
||||
|
||||
.notice {
|
||||
padding:10px 14px; border-radius:var(--radius); font-size:11px;
|
||||
margin-bottom:12px;
|
||||
display:flex; align-items:flex-start; gap:8px;
|
||||
line-height:1.5;
|
||||
}
|
||||
.notice .ph { font-size:14px; flex-shrink:0; margin-top:1px; }
|
||||
.notice.info { background:rgba(56,189,248,0.08); border:1px solid color-mix(in srgb, var(--accent) 60%, var(--border)); color:var(--accent); }
|
||||
.notice.warn { background:rgba(251,113,133,0.08); border:1px solid color-mix(in srgb, var(--danger) 60%, var(--border)); color:var(--danger); }
|
||||
|
||||
/* ─────────────────────────────────────────────────────────────────
|
||||
Phosphor icon defaults
|
||||
───────────────────────────────────────────────────────────────── */
|
||||
|
||||
/* ── Phosphor icons ── */
|
||||
.ph { display:inline-block; vertical-align:-2px; }
|
||||
header .nav-btn .ph { font-size:11px; vertical-align:-1px; margin-right:2px; }
|
||||
header h1 + .ph { font-size:12px; }
|
||||
.btn .ph { font-size:12px; vertical-align:-2px; margin-right:2px; }
|
||||
.btn .ph { font-size:12px; vertical-align:-2px; }
|
||||
.section h2 .ph, .compose-card h2 .ph { font-size:13px; vertical-align:-2px; }
|
||||
|
||||
/* ── Default nav layout (desktop) ── */
|
||||
/* ─────────────────────────────────────────────────────────────────
|
||||
Default nav layout (desktop)
|
||||
───────────────────────────────────────────────────────────────── */
|
||||
|
||||
#nav-buttons:not(.hidden) {
|
||||
display:flex; gap:6px; align-items:center;
|
||||
}
|
||||
#nav-burger {
|
||||
display:none; /* desktop hides the burger */
|
||||
display:none;
|
||||
padding:5px 10px; font-size:11px;
|
||||
}
|
||||
|
||||
/* ── Mobile: collapse nav into a burger menu ── */
|
||||
/* ─────────────────────────────────────────────────────────────────
|
||||
Mobile: collapse nav into a burger menu
|
||||
───────────────────────────────────────────────────────────────── */
|
||||
|
||||
@media (max-width: 700px) {
|
||||
/* Hide identity tag on mobile — it's redundant with the menu */
|
||||
#nav-user { display:none !important; }
|
||||
/* Burger replaces the inline nav row */
|
||||
#nav-burger { display:inline-flex !important; align-items:center; }
|
||||
/* Default-hide the nav row; .menu-open re-shows it as a popover */
|
||||
#nav-buttons:not(.hidden) { display:none; }
|
||||
#nav-buttons.menu-open {
|
||||
display:flex !important;
|
||||
@@ -267,22 +535,19 @@ header h1 + .ph { font-size:12px; }
|
||||
margin-top:6px;
|
||||
background:var(--surface);
|
||||
border:1px solid var(--border);
|
||||
border-radius:4px;
|
||||
box-shadow:0 8px 24px rgba(0,0,0,0.4);
|
||||
border-radius:var(--radius-lg);
|
||||
box-shadow: var(--shadow-popover);
|
||||
padding:6px;
|
||||
gap:4px;
|
||||
min-width:160px;
|
||||
min-width:180px;
|
||||
z-index:50;
|
||||
}
|
||||
#nav-buttons.menu-open .nav-btn {
|
||||
text-align:left;
|
||||
width:100%;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
/* Anchor the popover to the header row */
|
||||
header { position:relative; }
|
||||
|
||||
/* Admin view on mobile: stack the two columns. The desktop layout
|
||||
has its own grid; here we just let the section scroll naturally. */
|
||||
#view-admin .section {
|
||||
display:block !important;
|
||||
padding:14px 12px;
|
||||
@@ -291,32 +556,33 @@ header h1 + .ph { font-size:12px; }
|
||||
#view-admin .section .create-user-form { margin-top:14px; }
|
||||
}
|
||||
|
||||
/* ── Desktop layout ── */
|
||||
/* ─────────────────────────────────────────────────────────────────
|
||||
Desktop layout (≥ 901px)
|
||||
───────────────────────────────────────────────────────────────── */
|
||||
|
||||
@media (min-width: 901px) {
|
||||
|
||||
/* Subtle background gradient gives the page some depth */
|
||||
body {
|
||||
background:
|
||||
radial-gradient(ellipse at top left, rgba(56,189,248,0.04), transparent 50%),
|
||||
radial-gradient(ellipse at bottom right, rgba(129,140,248,0.04), transparent 50%),
|
||||
radial-gradient(ellipse at top left, rgba(56,189,248,0.05), transparent 50%),
|
||||
radial-gradient(ellipse at bottom right, rgba(129,140,248,0.05), transparent 50%),
|
||||
var(--bg);
|
||||
}
|
||||
|
||||
/* Wider canvas, more breathing room */
|
||||
main {
|
||||
max-width: 1200px;
|
||||
padding: 32px 32px 48px;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
/* Header: bigger title, more padding */
|
||||
header {
|
||||
padding: 14px 32px;
|
||||
}
|
||||
header h1 {
|
||||
font-size: 15px;
|
||||
letter-spacing: 1px;
|
||||
font-size: 16px;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
header .brand-icon { font-size:20px; }
|
||||
header .nav-btn {
|
||||
padding: 7px 12px;
|
||||
}
|
||||
@@ -324,10 +590,10 @@ header h1 + .ph { font-size:12px; }
|
||||
padding: 4px 10px;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 3px;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
/* Compose view: 2-column grid (compose | inbox) */
|
||||
/* Compose view: 2-column grid */
|
||||
#view-compose:not(.hidden) {
|
||||
display: grid !important;
|
||||
grid-template-columns: minmax(0, 1.7fr) minmax(0, 1fr);
|
||||
@@ -339,31 +605,28 @@ header h1 + .ph { font-size:12px; }
|
||||
.compose-card,
|
||||
.section {
|
||||
padding: 22px 24px;
|
||||
box-shadow:
|
||||
0 1px 2px rgba(0,0,0,0.3),
|
||||
0 8px 24px -8px rgba(0,0,0,0.4);
|
||||
box-shadow: var(--shadow-card);
|
||||
border-color: color-mix(in srgb, var(--border) 70%, var(--accent) 12%);
|
||||
}
|
||||
.compose-card h2,
|
||||
.section h2 {
|
||||
font-size: 12px;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 6px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Big textarea for desktop typing */
|
||||
.compose-card textarea {
|
||||
.compose-card .field textarea {
|
||||
min-height: 320px;
|
||||
font-size: 14px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
.compose-card input,
|
||||
.compose-card select {
|
||||
.compose-card .field input,
|
||||
.compose-card .field select {
|
||||
padding: 9px 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.compose-card .field label {
|
||||
.compose-card .field > label {
|
||||
font-size: 10px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
@@ -377,11 +640,11 @@ header h1 + .ph { font-size:12px; }
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Inbox card: stick to top of viewport, hover lifts it */
|
||||
/* Inbox card sticks under the header */
|
||||
#inbox-section {
|
||||
position: sticky;
|
||||
top: 24px;
|
||||
transition: transform .15s, box-shadow .15s;
|
||||
transition: box-shadow .15s;
|
||||
}
|
||||
#inbox-section:hover {
|
||||
box-shadow:
|
||||
@@ -390,7 +653,7 @@ header h1 + .ph { font-size:12px; }
|
||||
0 0 0 1px color-mix(in srgb, var(--accent) 20%, transparent);
|
||||
}
|
||||
|
||||
/* Admin view: 2-column grid (user list | create form) */
|
||||
/* Admin: 2-column grid */
|
||||
#view-admin:not(.hidden) .section {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 360px;
|
||||
@@ -411,44 +674,17 @@ header h1 + .ph { font-size:12px; }
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* User row polish: hover lift */
|
||||
.user-row {
|
||||
padding: 14px 12px;
|
||||
margin: 0 -12px;
|
||||
border-radius: 3px;
|
||||
transition: background .12s;
|
||||
}
|
||||
.user-row:hover {
|
||||
background: color-mix(in srgb, var(--raised) 70%, transparent);
|
||||
}
|
||||
.user-row .name {
|
||||
font-size: 12px;
|
||||
}
|
||||
.user-row .name { font-size: 12px; }
|
||||
|
||||
/* Auth forms (setup/login) — vertically center on viewport */
|
||||
/* Auth-forms: full vertical centering, larger icon */
|
||||
.auth-form {
|
||||
margin-top: 64px;
|
||||
padding: 40px 32px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
box-shadow:
|
||||
0 1px 2px rgba(0,0,0,0.3),
|
||||
0 16px 48px -16px rgba(0,0,0,0.5);
|
||||
max-width: 420px;
|
||||
margin-top: 88px;
|
||||
padding: 44px 36px;
|
||||
max-width: 440px;
|
||||
}
|
||||
.auth-form h2 {
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Buttons: subtle press */
|
||||
.btn { transition: all .12s; }
|
||||
.btn-primary:hover {
|
||||
filter: brightness(1.15);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px -4px rgba(56,189,248,0.5);
|
||||
}
|
||||
.btn-secondary:hover { transform: translateY(-1px); }
|
||||
.btn-danger:hover { transform: translateY(-1px); }
|
||||
.auth-form h2 { font-size: 16px; margin-bottom: 8px; }
|
||||
.auth-form .auth-icon { font-size: 44px; }
|
||||
}
|
||||
Reference in new issue
Block a user