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

+138 -11
View File
@@ -30,6 +30,15 @@ const mediaItems = [
},
];
function altName(kind: 'Image' | 'GIF', sender: string, timestamp: number): string {
const date = new Intl.DateTimeFormat(undefined, { day: 'numeric', month: 'short' }).format(timestamp);
return `${kind} from u/${sender}, ${date}`;
}
const aliceAlt = altName('Image', 'alice', mediaItems[0].timestamp);
const charlieAlt = altName('GIF', 'charlie', mediaItems[1].timestamp);
const daveAlt = altName('Image', 'dave', mediaItems[2].timestamp);
const idleStatus = {
isSyncing: false,
lastSyncAt: now - 120_000,
@@ -62,6 +71,11 @@ function renderMedia() {
return { ...view, queryClient };
}
/** The card's footer toggle carries aria-pressed; the image button does not. */
function revealToggle(sender: string, pressed: boolean) {
return screen.getByRole('button', { name: `Reveal media from ${sender}`, pressed });
}
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => {
cleanup();
@@ -91,8 +105,8 @@ describe('media page', () => {
await waitFor(() => expect(resolveMedia).toBeTypeOf('function'));
resolveMedia?.(HttpResponse.json(mediaItems));
const aliceImage = await screen.findByRole('img', { name: 'Media from alice' });
const charlieImage = screen.getByRole('img', { name: 'Media from charlie' });
const aliceImage = await screen.findByRole('img', { name: aliceAlt });
const charlieImage = screen.getByRole('img', { name: charlieAlt });
for (const profileLink of screen.getAllByRole('link', { name: 'alice' })) {
expect(profileLink).toHaveAttribute('href', 'https://www.reddit.com/u/alice');
}
@@ -100,17 +114,44 @@ describe('media page', () => {
expect(charlieImage).toHaveClass('is-blurred');
expect(credentials.every((value) => value === 'same-origin')).toBe(true);
await user.click(screen.getByRole('button', { name: 'Reveal media from alice' }));
await user.click(revealToggle('alice', false));
expect(aliceImage).not.toHaveClass('is-blurred');
expect(charlieImage).toHaveClass('is-blurred');
await user.click(screen.getByRole('button', { name: 'Reveal all media' }));
expect(charlieImage).not.toHaveClass('is-blurred');
// Turning reveal-all back off keeps the earlier individual reveal intact.
await user.click(screen.getByRole('button', { name: 'Blur all media' }));
expect(charlieImage).toHaveClass('is-blurred');
expect(aliceImage).not.toHaveClass('is-blurred');
firstView.unmount();
installMediaApi();
renderMedia();
expect(await screen.findByRole('img', { name: 'Media from alice' })).toHaveClass('is-blurred');
expect(await screen.findByRole('img', { name: aliceAlt })).toHaveClass('is-blurred');
});
it('reveals blurred media when its preview surface is clicked', async () => {
installMediaApi();
const user = userEvent.setup();
renderMedia();
const aliceImage = await screen.findByRole('img', { name: aliceAlt });
expect(aliceImage).toHaveClass('is-blurred');
// The image surface itself acts as a reveal control while blurred.
const imageButton = screen
.getAllByRole('button', { name: 'Reveal media from alice' })
.find((button) => button.classList.contains('media-preview-button'));
expect(imageButton).toBeDefined();
expect(imageButton).toBeEnabled();
await user.click(imageButton!);
expect(aliceImage).not.toHaveClass('is-blurred');
// Once revealed, the same surface opens the full-size preview instead.
await user.click(screen.getByRole('button', { name: 'View media from alice larger' }));
expect(screen.getByRole('dialog', { name: 'Media from alice' })).toBeInTheDocument();
});
it('opens revealed media in a dismissible full-size preview', async () => {
@@ -118,16 +159,43 @@ describe('media page', () => {
const user = userEvent.setup();
renderMedia();
await user.click(await screen.findByRole('button', { name: 'Reveal media from alice' }));
await screen.findByRole('img', { name: aliceAlt });
await user.click(revealToggle('alice', false));
await user.click(screen.getByRole('button', { name: 'View media from alice larger' }));
const dialog = screen.getByRole('dialog', { name: 'Media from alice' });
expect(within(dialog).getByRole('img', { name: 'Media from alice' })).toHaveAttribute('src', '/media/one.jpg');
expect(within(dialog).getByRole('img', { name: aliceAlt })).toHaveAttribute('src', '/media/one.jpg');
await user.keyboard('{Escape}');
expect(screen.queryByRole('dialog', { name: 'Media from alice' })).not.toBeInTheDocument();
});
it('navigates the preview across the visible items with buttons and arrow keys', async () => {
installMediaApi();
const user = userEvent.setup();
renderMedia();
await screen.findByRole('img', { name: aliceAlt });
await user.click(screen.getByRole('button', { name: 'Reveal all media' }));
await user.click(screen.getByRole('button', { name: 'View media from alice larger' }));
let dialog = screen.getByRole('dialog', { name: 'Media from alice' });
expect(within(dialog).getByRole('button', { name: 'Previous media' })).toBeDisabled();
expect(within(dialog).getByText('1 of 3')).toBeInTheDocument();
await user.click(within(dialog).getByRole('button', { name: 'Next media' }));
dialog = screen.getByRole('dialog', { name: 'Media from charlie' });
expect(within(dialog).getByRole('img', { name: charlieAlt })).toHaveAttribute('src', '/media/two.gif');
expect(within(dialog).getByText('u/charlie')).toBeInTheDocument();
await user.keyboard('{ArrowRight}');
dialog = screen.getByRole('dialog', { name: 'Media from dave' });
expect(within(dialog).getByRole('button', { name: 'Next media' })).toBeDisabled();
await user.keyboard('{ArrowLeft}');
expect(screen.getByRole('dialog', { name: 'Media from charlie' })).toBeInTheDocument();
});
it('links media cards to their account-owned conversations', async () => {
installMediaApi();
renderMedia();
@@ -149,11 +217,11 @@ describe('media page', () => {
await user.selectOptions(screen.getByRole('combobox', { name: 'Conversation' }), 'alice');
expect(screen.getAllByRole('img')).toHaveLength(1);
expect(screen.getByRole('img', { name: 'Media from alice' })).toBeInTheDocument();
expect(screen.getByRole('img', { name: aliceAlt })).toBeInTheDocument();
await user.selectOptions(screen.getByRole('combobox', { name: 'Conversation' }), 'all');
await user.selectOptions(screen.getByRole('combobox', { name: 'Sender' }), 'charlie');
expect(screen.getByRole('img', { name: 'Media from charlie' })).toBeInTheDocument();
expect(screen.getByRole('img', { name: charlieAlt })).toBeInTheDocument();
await user.selectOptions(screen.getByRole('combobox', { name: 'Sender' }), 'all');
await user.selectOptions(screen.getByRole('combobox', { name: 'Media type' }), 'gif');
@@ -167,6 +235,49 @@ describe('media page', () => {
expect(screen.getByText('No media matches these filters.')).toBeInTheDocument();
});
it('shows a result count and clears filters from both the panel and the empty state', async () => {
installMediaApi();
const user = userEvent.setup();
renderMedia();
await screen.findByRole('img', { name: aliceAlt });
expect(screen.getByText('Showing 3 of 3 items')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Clear filters' })).not.toBeInTheDocument();
await user.selectOptions(screen.getByRole('combobox', { name: 'Conversation' }), 'alice');
expect(screen.getByText('Showing 1 of 3 items')).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'Clear filters' }));
expect(screen.getAllByRole('img')).toHaveLength(3);
expect(screen.getByRole('combobox', { name: 'Conversation' })).toHaveValue('all');
await user.selectOptions(screen.getByRole('combobox', { name: 'Sender' }), 'charlie');
await user.selectOptions(screen.getByRole('combobox', { name: 'Media type' }), 'image');
const emptyState = screen.getByText('No media matches these filters.').closest('section');
expect(emptyState).not.toBeNull();
await user.click(within(emptyState!).getByRole('button', { name: 'Clear filters' }));
expect(screen.getAllByRole('img')).toHaveLength(3);
});
it('paginates large galleries behind a Load more control', async () => {
const manyItems = Array.from({ length: 60 }, (_, index) => ({
id: `item-${index}`, chatUsername: 'alice', chatRoomId: `room-${index}`, sender: 'alice',
timestamp: now - index * 60_000, originalUrl: `https://media.example/${index}.jpg`,
fileName: `${index}.jpg`, relativePath: `${index}.jpg`, createdAt: now - index * 60_000,
urlPath: `/media/${index}.jpg`,
}));
installMediaApi(manyItems);
const user = userEvent.setup();
renderMedia();
await screen.findByText('Showing 48 of 60 items');
expect(screen.getAllByRole('img')).toHaveLength(48);
await user.click(screen.getByRole('button', { name: 'Load more (12 remaining)' }));
expect(screen.getAllByRole('img')).toHaveLength(60);
expect(screen.getByText('Showing 60 of 60 items')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: /Load more/ })).not.toBeInTheDocument();
});
it('orders media by received time', async () => {
server.use(
http.get('/api/media', () => HttpResponse.json([mediaItems[2], mediaItems[0], mediaItems[1]])),
@@ -175,9 +286,9 @@ describe('media page', () => {
renderMedia();
expect((await screen.findAllByRole('img')).map((item) => item.getAttribute('alt'))).toEqual([
'Media from alice',
'Media from charlie',
'Media from dave',
aliceAlt,
charlieAlt,
daveAlt,
]);
});
@@ -230,6 +341,22 @@ describe('media page', () => {
expect(await screen.findByRole('alert')).toHaveTextContent('Sync already in progress');
});
it('auto-dismisses the sync success notice after five seconds', async () => {
installMediaApi();
server.use(http.post('/api/media/sync', () => HttpResponse.json({ success: true })));
const user = userEvent.setup();
renderMedia();
await user.click(await screen.findByRole('button', { name: 'Sync new media' }));
const notice = await screen.findByText('Media sync completed.');
expect(notice).toHaveAttribute('role', 'status');
await waitFor(
() => expect(screen.queryByText('Media sync completed.')).not.toBeInTheDocument(),
{ timeout: 7_000 },
);
}, 15_000);
it('refreshes shared sync status as soon as a long-running mutation starts', async () => {
let statusRequests = 0;
let resolveSync: ((response: Response) => void) | undefined;