feat(notices): notices button in the header - #2668
Conversation
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
🎩 PreviewA preview build has been created at: |
eee6c2b to
91b5f62
Compare
9e7df61 to
0769ace
Compare
91b5f62 to
45113d5
Compare
af61eb7 to
b02bd10
Compare
45113d5 to
69c580a
Compare
b02bd10 to
abcdac8
Compare
8464f5c to
ec2d04d
Compare
abcdac8 to
e5b71e0
Compare
ec2d04d to
8d1683b
Compare
1d161de to
f563d37
Compare
8d1683b to
e2ef400
Compare
f563d37 to
c5ba846
Compare
|
Weird. I definitely pushed the rework, as a three-column view. The question of 1 v 2 v 3 columns is tricky - 1 column is bad for short notices, whilst the others are bad for long notices. It's easy to change though, so once we dream up a solution we can iterate. |
c5ba846 to
f3e7cec
Compare
d5d72bf to
5d7456b
Compare
Adds a Megaphone button to the top bar that opens the full list of notices, with an unread count badge. Opening the list marks everything in it as read; notices the host marked dismissible can be removed one at a time from here. The list is also the way back once the strip has been hidden, which until now was a one-way door for the rest of the session. Its toggle mirrors the strip's, and the strip's own controls stay out of the way while the list is open.
…oggle The notices button now stays in the header whether or not there is anything to read, and opening it with nothing to show gives a "No notices" empty state. Previously the whole affordance unmounted when the list emptied, which also meant dismissing the last notice pulled the open popover out from under the reader. Cap the unread badge at 9+ so a host-supplied count cannot clip inside the badge; the trigger's label still carries the exact number. The banner toggle follows hasHiddenNotices, so it offers to bring the banners back as soon as one notice is hidden rather than only when all of them are. Move the viewport-fit sizing onto the PopoverContent primitive so no caller has to name Radix custom properties, and let the header row use the fill prop rather than a width class. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
5d7456b to
fcad184
Compare
f3e7cec to
685017f
Compare
morgan-wowk
left a comment
There was a problem hiding this comment.
🤖 Automated review
Approving. The header notices button wires to the same store as the banners (so a dismiss propagates to both surfaces), the unread count is exact in the aria-label while the badge caps at 9+, and the empty/active/unread states plus Radix keyboard/focus are covered — including that the popover stays reachable after its last notice goes.
One thing worth a quick visual pass before merge (not blocking): src/components/ui/popover.tsx adds max-w-(--radix-popover-content-available-width) max-h-(--radix-popover-content-available-height) … overflow-y-auto to the shared PopoverContent, i.e. every popover in the app. It only ever constrains to the available viewport and adds vertical scroll, so it's generally an improvement — but any existing popover that intentionally overflows its bounds (a nested menu/tooltip escaping) would now be clipped/scrolled. A sweep of the app's popovers would de-risk it.
Mbeaulne
left a comment
There was a problem hiding this comment.
Approved with one minor resilience fix.
| function openNoticeInbox(notices: readonly TangleNotice[]) { | ||
| const stored = readIds(); | ||
| const merged = [...new Set([...stored, ...notices.map(({ id }) => id)])]; | ||
| if (merged.length !== stored.length) storage.setItem(READ_KEY, merged); |
There was a problem hiding this comment.
🤖 This is an AI-generated code review comment.
getStorage().setItem intentionally swallows storage failures, so this cannot assume the merged IDs were persisted. I reproduced with Storage.prototype.setItem throwing: opening an inbox with one notice leaves unreadCount at 1 for the whole session. Please mirror the dismissal fallback with an in-memory set of read IDs, merge it in getReadIds, and add a storage-unavailable test.






Why
Banners live on the dashboard home only — same as the announcements they replace. Someone who opens a run page from the CLI and stays there never sees them. And there was no way to look at a notice again after clearing it off the page.
This adds a megaphone to the header: notices are reachable from any route, with an unread badge.
What you get
A
Popoveron aTooltipButtonin both top bars (v1AppMenu, v2AppMenuActions), listing every notice in full — unclamped, so a long body isn't cut off the way it is in a banner. Dismissible notices can be retired from here too. Empty state is "No notices"; the button stays in the header either way so it doesn't shift its neighbours around.Unread, and only unread, is what this PR adds to the state model: a notice is unread until the reader opens the popover, tracked by id in
localStorageunderread-notices. Badge caps at9+visually while the trigger'saria-labelkeeps the exact count ("Notices, 12 unread"). Reading is separate from dismissal, so opening the list doesn't clear anything from the banners.Reviewer notes
useNoticeInboxwrapsuseNoticesrather than duplicating it — it adds read-tracking and open state and forwardsnotices/dismissthrough. Same module-store +useSyncExternalStoreshape as #2667, for the same reason: the badge and the list are in different subtrees.Open state is deliberately in the store rather than
useState, because it has to survive the trigger unmounting when the route swaps between the v1 and v2 top bars. TheuseEffect(() => closeNoticeInbox, [])closes it on unmount so it can't come back open on a different page.One shared primitive changes:
popover.tsx, one line.PopoverContenthad a fixedw-72and no height ceiling, so a tall popover ran off the bottom of the viewport with no way to reach the rest. It now caps to Radix's available width/height and scrolls. This affects every popover in the app — worth confirming, though the change only ever removes overflow.Accessibility: the trigger's name carries the count, dismissal is a labelled button, and the popover keeps focus and stays reachable after its last notice goes (tested).
How to test
Install a source (see #2667 or
src/config/NOTICES.md), then:Where this sits
Additive on top of the #2667 merge point — the banners work without this.