feat: dashboard UI/UX overhaul from full review

Shared foundation:
- New Modal component on native <dialog>.showModal() (focus trap, inert
  background, Escape, focus restore, scroll lock); all eight dialogs and
  both globalThis.confirm calls migrated to it
- Design tokens for success/warning/danger surfaces; pill danger variant;
  .sr-only utility; toast layering above dialog backdrops; not-allowed
  cursor for validity-disabled buttons

Conversations/attention: auto-scroll with jump-to-latest, per-conversation
draft persistence, suggestion undo, 30s auto-refresh with refresh controls,
unread emphasis and counts, blur-consistent share dialog, empty-accounts
guidance, collapsed user lookup, relative timestamps, danger pills for
critical items

Media/library/outreach: click-blurred-to-reveal, lightbox with arrow
navigation and captions, dirty-form preservation across refetches, upload
validation with thumbnail preview, save feedback, result counts with
load-more and clear-filters, ticking countdowns from a single clock

Overview/performance/operations/settings: mutation errors inside the
confirm dialog, live sending-window countdown, offline-aware loading
states, per-panel loading/error with retry, 7/30/90-day range toggle,
console scroll yielding with shown/retained counts, risky-save matching
by subreddit name instead of index, useBlocker unsaved-changes guard,
single-pass validation with focus-to-first-error, access panel JSON
validation and input reset

Tests: 806 -> 844 passing; both typechecks clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jamie BruntonandClaude Fable 5 committed 2026-08-10 09:07:42 +01:00
1 parent 82be197b79
commit e984bf20ac
46 files changed
+2462 -495

No files matched your search

@@ -1,4 +1,4 @@
import { useRef } from 'react';
import { useEffect, useRef, useState } from 'react';
import { RedditUsernameLink } from '../../components/reddit-username-link.js';
import {
useImportRedditSessionMutation,
@@ -27,16 +27,27 @@ export function AccessPanel() {
const accountsQuery = useRedditAccountsQuery();
const importMutation = useImportRedditSessionMutation();
const importInput = useRef<HTMLInputElement>(null);
const [importFileError, setImportFileError] = useState<string | null>(null);
const resetImportMutation = importMutation.reset;
useEffect(() => {
if (!importMutation.isSuccess) return;
const timer = globalThis.setTimeout(() => resetImportMutation(), 5_000);
return () => globalThis.clearTimeout(timer);
}, [importMutation.isSuccess, resetImportMutation]);
async function importSession(file: File | undefined) {
if (!file) return;
setImportFileError(null);
let exported: unknown;
try {
const exported: unknown = JSON.parse(await file.text());
importMutation.mutate(exported);
exported = JSON.parse(await file.text());
} catch {
importMutation.reset();
importMutation.mutate({});
setImportFileError("That file isn't valid JSON. Export a fresh session dump from the extension and try again.");
return;
}
importMutation.mutate(exported);
}
if (statusQuery.isPending) {
@@ -60,6 +71,13 @@ export function AccessPanel() {
<p className="settings-copy">Add each Reddit account with its own extension session dump. Content and sync state stay owned by that account in the database. Any account with an expired login pauses while waiting for a fresh session dump; other accounts keep running.</p>
{username ? <p className="settings-copy">Verified account: <strong><RedditUsernameLink username={username} /></strong>.</p> : null}
<div aria-label="Reddit accounts" className="reddit-account-list">
{accountsQuery.isPending ? <p className="settings-copy" role="status"><span className="spinner" /> Loading Reddit accounts…</p> : null}
{accountsQuery.isError ? (
<div className="settings-notice error" role="alert">
<span>Could not load Reddit accounts: {errorMessage(accountsQuery.error)}</span>{' '}
<button className="button secondary" onClick={() => void accountsQuery.refetch()} type="button">Retry accounts</button>
</div>
) : null}
{(accountsQuery.data ?? []).map((account) => (
<div className={account.isRunning ? 'reddit-account-row is-active' : 'reddit-account-row'} key={account.accountId}>
<div><strong><RedditUsernameLink username={account.redditUsername} /></strong><span>{account.isRunning ? 'Running independently' : 'Waiting for a refreshed session'}</span></div>
@@ -69,11 +87,24 @@ export function AccessPanel() {
{accountsQuery.isSuccess && accountsQuery.data.length === 0 ? <p className="settings-copy">No Reddit accounts have been added yet.</p> : null}
</div>
<div className="access-actions">
<input accept="application/json" aria-label="Reddit session export" className="sr-only" onChange={(event) => void importSession(event.target.files?.[0])} ref={importInput} type="file" />
<input
accept="application/json"
aria-label="Reddit session export"
className="sr-only"
onChange={(event) => {
const file = event.target.files?.[0];
// Reset the input so picking the same file again re-triggers change.
event.target.value = '';
void importSession(file);
}}
ref={importInput}
type="file"
/>
<button className="button primary" disabled={importMutation.isPending} onClick={() => importInput.current?.click()} type="button">{importMutation.isPending ? 'Adding account...' : 'Add or refresh account'}</button>
<button className="button secondary" disabled={statusQuery.isFetching} onClick={() => void statusQuery.refetch()} type="button">{statusQuery.isFetching ? 'Checking sessions...' : 'Recheck accounts'}</button>
<button aria-busy={statusQuery.isFetching ? 'true' : undefined} className="button secondary" disabled={statusQuery.isFetching} onClick={() => void statusQuery.refetch()} type="button">{statusQuery.isFetching ? 'Checking sessions...' : 'Recheck accounts'}</button>
</div>
<p className="settings-copy">Open Reddit Chat in Chrome for the account you want to add, wait for the extension badge to show OK, then upload its JSON dump. It contains login credentials; do not share it.</p>
{importFileError ? <div className="settings-notice error" role="alert">{importFileError}</div> : null}
{importMutation.isSuccess ? <div className="settings-notice success" role="status"><span>Reddit session imported and verified.</span> <span>Account runtime is running independently.</span></div> : null}
{importMutation.isError ? <div className="settings-notice error" role="alert">{errorMessage(importMutation.error)}</div> : null}
</section>