Repository navigation
Dashboard: one-line lists, HTML mail in threads, label colors and received events - #143
Merged
Merged
Conversation
…t fields without a focus ring Text fields and editors now mark focus with the caret alone; buttons, links and other controls keep the keyboard-only ring. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sent email page drew its events strip inline, with its own copy of the event tones. The received email page needs the same nodes, so the node (tile, pulse, pill, time, detail), the connector with its duration label and the loading placeholder now live beside the event glyphs, and the sent page's duplicate tone map folds into emailStatusColor. The sent strip renders as before. A connector with no time to tell keeps a blank label so its line stays at the tiles' height, and a wrapping detail breaks a long word instead of clipping it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The received page wrote its own text-only strip: no tiles, no time between steps, a long "Delivered to <address>" label that could overflow its node, and on phones the label squeezed into the tile column while the connectors vanished. Each step is now a sent-strip node: a tile toned and drawn by what the step found, a short pill, the time with its UTC stamp, and a detail line that wraps an inbox address. Each connector carries the time since the last timed step, blank into a scan or screening, which keep no time, or into a step listed after a later one. The newest step pulses. Spam is amber as in the list, not red: it is filed away, not held. The webhook step takes its tone from the delivery status the webhooks page uses. Released and Delivered become short pills with who released it and the inbox as details, and the screening scores reuse the review table's wording. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…loads The received page's skeleton had no events strip, so the strip appeared from nothing once the email loaded, and it drew three authentication badges side by side where the page stacks four. It now holds the strip with the sent page's placeholder and stacks a badge per authentication check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A long first recipient wrapped onto a second line and pushed "+N" under the status tile, so rows ran 43 to 60px. The To cell is now one line: the address ends in an ellipsis with the whole address in a tooltip, and "+N" sits right after it, listing the other recipients in its tooltip. The envelope tile names the status in the badge's own words. The lead cell (tile, value, "+N", muted tail) is one component, so the Inboxes and All received lists can share it. To and Status move to 35/14% so a long address and its "+N" still fit at 1440px; the loading skeleton follows, tile included, so rows keep their 43px when data arrives. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lead cells took two lines (the address, then a muted line with the name, expiry or forwarding) and addresses wrapped, so rows ran 32 to 66px. Each row now leads with a muted tile (All received, catch-all, agent or people inbox, named in its tooltip) and keeps the name, expiry and forwarding after the address; when the column runs out that tail gives way first, then the address ends in an ellipsis. The tiles stay muted: Needs attention already says when a row wants a look, and it keeps to one line too, taking width from the inbox column instead of wrapping. All received keeps the first clause of its description, and the catch-all row shortens its link to "turn off", with the domain in its tooltip, so both fit beside their values at 1440px. Type and Needs attention narrow to 90px and 22% to leave the addresses room. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The list folded mail still being screened into "held" and labelled it Quarantined, and never showed suspicious mail. receiving.list now returns pending, quarantined and suspicious, each true when any inbox the message reached has that verdict, from one aggregate per row (a lateral join on the membership's primary key) in place of a subquery per flag. The From cell leads with a tile whose glyph and tone follow the most urgent state (virus, quarantined, being screened, spam, suspicious, scanner failed, else clean), and the subject keeps a badge for every state but clean. No virus verdict means no scanner is configured, which reads as clean; the Clean filter now leaves out mail whose scanner failed, so it lists exactly the rows whose tile reads clean. From and Inbox stay on one line each, the attachment count gets a paperclip, and the code and link chips say what was found. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dashboard is about to fold a reply's quoted history in received HTML, using the same markers the agents' read already cuts: the quote classes Gmail, Thunderbird, Yahoo and Proton write, and the ids Outlook puts where the original starts. They now live once, as exported constants in the import-free quotes module, which a new receiving-quotes subpath makes safe to import from client code. The read pipeline builds its matchers from them; the stopSpelling marker counts on any element rather than only on <hr>, the only element Outlook puts it on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The received preview gets a DOM pass before it is framed: the message's <base>, <link> and http-equiv or referrer metas go, since they would override the frame's own policy or, like a preconnect, leak a read receipt; it learns whether the message would load remote images; and a reply also comes without its quoted history, unless the quote is all there is or it holds a forward. Plain-text bodies fold their trailing quote with core's splitTextQuotes, the rules the agents' read uses. The lockdown can now let images in over https once the reader asks, and a remembered "always for this domain" choice lives in localStorage under one key prefix, every access guarded. Only a sender whose authentication ties the message to its From domain may have images load unasked, so a spoofed From never gets them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The thread pane showed only a message's text part, and nothing at all for HTML-only mail. MailFrame now renders the HTML in the card, and the Received email page's preview uses the same frame instead of a fixed 640px box: - the sandbox never allows scripts, forms or top navigation; allow-same-origin only lets the dashboard measure the document and read its links; - the frame takes its content's height, measured on load, on each image load or error and on width changes, with no inner scrollbar; wide mail scrolls sideways inside its own box; - remote images stay blocked behind a bar offering "Show images" and, for an authenticated sender, "Always for" its domain; - hovering or focusing a link shows where it goes in the app's tooltip; - a reply's quoted history folds under a "•••" toggle, in HTML and text; - unstyled mail gets a mail client's sans default instead of the browser's serif. Held mail keeps its whole text and no HTML, as it is read to decide on a release; draft cards are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e core module The dashboard is about to offer the same 11 label colors core picks from, say how many labels an inbox can hold, and say how long Trash keeps a thread. Those values lived in modules that import node:crypto and the database, which a client component cannot load. They now live in inboxes/organize.ts, which imports nothing and ships as @millionsend/core/inbox-organize; labels.ts and limits.ts read them from there, so each value still has one copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Creating a label from the thread pane will let the person pick its color, as the REST API already allows; without one, core still picks from its palette. The color is checked with the shared HEX_COLOR_RE, which labels.update now uses too instead of its own copy of the pattern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pam, Restore and the Trash note Read rows rendered nothing in the dot column, so they looked indented next to unread ones; they now show the hollow ring, and both marks say "Read" or "Unread" on hover and to screen readers (an agent inbox keeps its longer note on the dot). "Close" was the only text button among the pane's boxed controls. It is now an icon button with a tooltip, as Refresh is, and Esc closes the pane too, unless the key was already handled, was typed into a field or the reply editor, or belongs to an open menu, dialog, drawer or tooltip. Those layers are read as the key goes down, since one that closes on it may be gone by the time it reaches window. Focus left in the closing pane goes back to the thread's row instead of falling to the page. Spam gets a one-click "Not spam" and Trash a "Restore", both moving the thread to Inbox as Move does, and Trash says how long it keeps a thread, with the number read from core. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…it from the Labels menu The dashboard could only make labels in a random color and had no way to rename, recolor or delete one. Typing a new name in the Labels menu now shows the palette under the create row, with a random pick preselected so Enter alone still creates, and a dashed swatch that opens the native color picker for any other color. A pencil on the hovered or arrowed-to row, or →, swaps the menu to that label's edit view: name, the same swatches, and Delete with a confirm in place. A name another label has is refused as it is typed; the server's own refusals show inline too. The swatches are radios, so the palette is one Tab stop walked with the arrow keys; while the menu holds controls besides its search, Tab cycles through them (loopTab, shared with dialogs through the FOCUSABLE list now in lib/keyboard). GroupedMultiSelect gains these as optional hooks; the Emails filter and the webhook event picker render pixel for pixel as before. Thread rows and the pane header now show labels as one LabelBadge, the sans neutral pill with the label's dot; the header's keeps its ✕, except in an expired inbox, which is read-only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Esc typed into the reply editor never closes the pane, but its toolbar buttons sit outside the editable area, so Esc there did. Closing the pane unmounts the composer before its debounced local draft is written, which can drop the last words typed. Anywhere in the message editor now counts as the field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # apps/web/src/app/(dashboard)/inboxes/[id]/threads-tab.tsx # apps/web/src/app/(dashboard)/inboxes/received/[id]/received-email.tsx
The thread pane closes on an Esc that reaches the dashboard's window, but the message body now renders in a frame, and keys pressed in a frame's document never reach the parent window. After a click on the message's text, or once a focused link's tip was dismissed, Esc did nothing. The frame already listens to its document for the link tip, so an Esc the tip doesn't take is re-sent from the iframe element. Page handlers see it as an Esc on the frame, and the pane closes with focus back on the row. An Esc typed in a field inside the message still goes nowhere. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The thread pane took the "Always for" domain, and the address in each message's header, from the first <...> in the From value. A display name may carry an address of its own, quoted, as in "Bank <alerts@bank.example>" <promo@attacker.example>: mail that authenticated as attacker.example read as bank.example, so a reader who had chosen "Always for bank.example" would get the attacker's remote images loaded unasked. The address is now the angle-addr the value ends with, the one authentication vouched for, as senderName already reads it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ng quotes The frame parses the prepared markup again, and some trees read back differently: MathML and SVG text turned into a referrer meta and a preconnect link the strip had seen as text, and a declarative shadow root or a nested srcdoc frame held a document no check reached. The strip now runs until a parse changes nothing, drops nested frames and shadow-root templates, and gives every link target _blank and rel noopener: a named target kept an opener, which in Firefox let the sender's page send the dashboard tab elsewhere. Show images allowed any https image, and the frame shares the dashboard's origin, so an image on the dashboard's own host went out with the reader's session (a CSV export per image, say). The policy now lists only the hosts the message names, never the dashboard's. WebKit runs no listener on a document whose frame allows no scripts, the dashboard's included, so in Safari the link tip, the re-measure and menu dismissal never ran. The frame now follows its document with a ResizeObserver (content that grows after load, such as an opened <details>, is no longer clipped) and reads the hovered or focused link from the dashboard while the pointer is over the frame or a link inside has focus. A click into the message closes open menus and pinned tooltips, as a click anywhere else does. Esc from inside the message still cannot reach the dashboard in WebKit. The link tip shows the host whole, the domain its owner registered in bold: a look-alike host buries the real domain at its end, past what a four-line cut showed. Show images and Always for move focus into the message instead of dropping it with the bar. Folding takes only the quotes after the last thing the sender wrote, so inline answers keep their questions (Gmail's answers inside its quote included); hidden text and zero-width characters no longer count as written, and a message whose subject names a forward (FW:, Fwd:, ENC:, RV:) is never folded, in HTML or text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The thread pane now renders each message's HTML, but the thread read returned it byte for byte, with cid: references a frame can never load: a logo in an Outlook or Apple Mail signature showed as a broken image, and no Show images could fix it. The dashboard's read now inlines them as data URLs, as the received email's own read does, under the same size cap; past it the references stay. The public API keeps the HTML as received. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Needs attention cell went one line by refusing to wrap, which made its column as wide as its longest text: at 920 to 1024px windows the Inbox column was left with no room, so rows showed only their tiles. The cell now ends in an ellipsis inside its own 22%, and the Inbox column keeps 160px while the other columns give way first. The catch-all row's "turn off" link sat at the end of its tail, the first thing cut, and stayed in the Tab order while out of sight. LeadCell takes an action that follows the tail whole: the tail's words give way before the address, and the link always shows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Not spam, Restore and Move closed the pane with focus inside it, and the moved thread's row left the list with the refetch, so focus fell to the page and the next Tab started from the sidebar. Focus now moves to the row beside the thread, or to the folder's button when there is none. A label's ✕ in the pane header hands focus to a neighbouring ✕, or to the Labels menu, before its badge goes. Esc closing the pane could throw away typing: an open draft edit lives only in the card, so Esc leaves the pane open while one is being edited; and the reply composer's draft, mirrored to local storage on a short timer, is now written when the composer unmounts rather than dropped with the timer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…readers The custom-color swatch and the edit view's Back button carried native titles, which never show on keyboard focus: the app's tooltip sat at modal level, under the portaled menu. Tooltips are now the top layer, and both controls use them. The pencil is hidden from assistive tech, and → was the only keyboard way into rename, recolor and delete, so a screen reader never learned the edit view existed. Each label row now describes → and declares it as its key shortcut, and the edit view is a group named "Edit label". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 64-character label name cap was declared three times: the labels picker, the router's create and update inputs, and core's own default. It is now LABEL_NAME_MAX in core's client-safe organize module, next to the palette and the label limit, read by all three. The broadcast and template previews take their canvas color from PREVIEW_CANVAS, as the received-mail frame does, instead of their own copies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A message held, released and delivered fills the strip with six or seven steps, wider than the page at 1440: Delivered, the newest node with the pulse, ended past the edge and the Webhook node was out of view, with macOS overlay scrollbars giving no hint. When the strip overflows it now opens scrolled to its end, as the phone layout already leads with the latest event; a refetch leaves the reader's own scroll be. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Polishes the inbox and list screens of the dashboard: one-line rows with icon tiles, HTML mail rendered in the thread pane, label colors you can pick and edit, and the received email's events drawn like the sent ones. Every icon whose meaning isn't obvious gets a tooltip, and text fields no longer draw a focus ring.
Lists
components/lead-cell.tsx) gives the Emails, Inboxes and All received lists the same row: a 30px tile, the address on one line (ellipsized, full value in a tooltip), an optional muted tail, and "+N". Every row is 43px.receiving.listnow returnspending,quarantinedandsuspiciousinstead ofheld(one aggregate per row). Mail still being screened no longer shows "Quarantined".Threads, folders and labels
@millionsend/core/inbox-organize).LabelBadge.HTML mail
message.htmlby default inMailFrame. HTML-only mail now shows a body. The Received email page's Preview uses the same frame. The frame:allow-popups allow-popups-to-escape-sandbox allow-same-origin, with no scripts, forms or top navigation;noopener noreferrer, so a named target can't keep an opener to the dashboard;<base>,<link>, http-equiv and referrer metas, nested frames and declarative shadow roots are stripped until the markup parses the same twice.Received email events
EventNode,EventGapandEventStripSkeleton). Each node has a tile and an icon per step, timed gaps, the pulse on the newest step, and a Delivered pill with the inbox address as its detail.Focus
Text fields and editors show no focus ring; the caret marks focus. This covers inputs, textareas, the reply and template editors, the code find bar and the chip input. Buttons, links, tabs and other controls keep the keyboard-only ring.
Tests
🤖 Generated with Claude Code