feat(notices): host-driven notice banners on the dashboard home - #2667
Open
camielvs wants to merge 8 commits into
Open
feat(notices): host-driven notice banners on the dashboard home#2667camielvs wants to merge 8 commits into
camielvs wants to merge 8 commits into
Conversation
This was referenced Aug 26, 2026
Collaborator
Author
|
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. |
This was referenced Aug 26, 2026
🎩 PreviewA preview build has been created at: |
camielvs
commented
Aug 26, 2026
camielvs
commented
Aug 26, 2026
camielvs
commented
Aug 26, 2026
camielvs
commented
Aug 26, 2026
camielvs
commented
Aug 26, 2026
camielvs
commented
Aug 26, 2026
camielvs
force-pushed
the
banners-03-contract
branch
from
August 26, 2026 23:45
a100783 to
eb0c6de
Compare
camielvs
force-pushed
the
banners-04-strip
branch
2 times, most recently
from
August 27, 2026 00:44
91b5f62 to
45113d5
Compare
camielvs
force-pushed
the
banners-03-contract
branch
from
August 27, 2026 01:18
d9ad21b to
4fb20f2
Compare
camielvs
force-pushed
the
banners-04-strip
branch
from
August 27, 2026 01:18
45113d5 to
69c580a
Compare
camielvs
marked this pull request as ready for review
August 28, 2026 00:02
camielvs
force-pushed
the
banners-04-strip
branch
2 times, most recently
from
August 28, 2026 23:30
ec2d04d to
8d1683b
Compare
camielvs
force-pushed
the
banners-03-contract
branch
from
August 28, 2026 23:30
4fb20f2 to
189fa37
Compare
Renders notices from the host contract as a strip directly under the top nav, so they are visible on every page rather than only on the dashboard home. The strip scrolls horizontally when there are more notices than fit, sorts them most-severe-first, and publishes its own height so full-height layouts stay inside the viewport. "Hide notices" clears the strip. A notice the host marked dismissible stays hidden across reloads; one it did not comes back on the next load, since the host is still asking for it to be shown. Replaces the old dashboard-only AnnouncementBanners, which read a plain array off the window and is deleted here along with its config module. Note for reviewers: the store lands complete, but its read-tracking and per-notice dismiss are unused until the header UI in the next PR.
Every notice in the strip can now be taken off it, whatever the host said about dismissibility, because the inbox keeps it reachable. Hiding is persisted for a dismissible notice and session-only for a mandatory one, the same split hideStrip already used in bulk. The strip's control is an eye-off "Hide notice", distinct from the inbox's destructive "Dismiss", so the two actions do not look alike. isStripHidden becomes hasHiddenBanners, since a partly-hidden strip must still offer to bring the rest back. Register both new hooks in react-compiler.config.js — src/hooks is still opt-out, so a new top-level hook file gets no coverage by default. The compiler also takes care of the identity of the object useBannerInbox returns, so a consumer can safely put it in a dep array. dismissBanner now enforces the same rule hideStrip does: only a notice the host marked dismissible is retired permanently, everything else is session-only. handleStorage ignores the synthetic same-tab event typedStorage dispatches on every write, so an action publishes once instead of twice. A real cross-tab event still refreshes, which is now pinned by a test. isOpen moves into the cached snapshot rather than being read off module state during render, and the tab-focus refresh is throttled. BannerCard replaces bodyClassName with a clampBody flag, so the card owns the classes and the strip only states its intent. The body keeps scrolling inside the card with the action pinned below it. resetBannerStateForTests clears the snapshot cache, the session sets, the open flag and the refresh watcher, so the suites stop reserving ids to avoid each other. With banners.ts fully wired, its knip ignore comes off. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follows the contract rename below it. Notices are the domain concept; the banner row is one surface they appear on, so the store exposes both: `notices` is everything the reader still has, `banners` the subset showing on the page. Storage keys move with it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reviewer feedback: match the announcements this replaces — inline with the page content, dashboard home only, rather than a strip above every route. Each notice takes one column of the dashboard's three-column grid, so the cards line up with the panels beneath them. The content-offset publisher goes with the strip; nothing sits between the top bar and the content any more. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reviewer feedback: the action should read as a button, not a text link. It stays an anchor underneath — it navigates to an external URL, so a real <button> would be the wrong element and would cost the middle-click, copy-link and new-tab behaviour a link gets for free. Button asChild gives it the button's chrome while the accessibility tree still sees a link. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Hiding gave a reader a way out of a notice the publisher marked mandatory. It cost a third persisted id list, a bulk restore toggle, and a second dismiss affordance on every card, for a case the publisher already controls through is_dismissible and ends_at. Banners now behave as the announcements they replace: an X on the notices the host allows, nothing on the rest. InfoBox needs no change at all as a result.
camielvs
force-pushed
the
banners-03-contract
branch
from
August 28, 2026 23:51
189fa37 to
46d176a
Compare
camielvs
force-pushed
the
banners-04-strip
branch
from
August 28, 2026 23:51
8d1683b to
e2ef400
Compare
Read state and the open/closed state of the notice inbox move to the PR that introduces the inbox. What is left here is what the announcements it replaces did: show the active notices, and retire the ones the host marks dismissible.
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.

Why
This is the PR that swaps the announcements system for the notices one. Everything below it was preparation; everything above it is optional.
What you get
AnnouncementBannersandsrc/config/announcements.tsare deleted.NoticeBannerstakes the exact slot<AnnouncementBanners />occupied — first child ofDashboardHomeView'sBlockStack, dashboard home only, nowhere else in the app.Scope of this PR is announcement parity, deliberately. A reader can see banners and dismiss the ones the host marked dismissible. That's it. The notice inbox is #2668 and hiding is #2681, so each can be judged — or dropped — on its own.
What parity buys over the deleted version, all of it a consequence of #2666 rather than new surface here:
variantno longer crashes the renderbodyrenders as Markdown, so a notice can link and emphasiseNoticeBannersanduseNoticesare React Compiler-enabled;AnnouncementBannerscould not be, because it read a mutable global and callednew Date()in renderTwo visible differences from the deleted banners, both intentional:
error→warning→success→info, rather than host array order, so an outage isn't below a nice-to-know.Reviewer notes
src/hooks/useNotices.tsis where the non-obvious code is:Dismissal is two-tier, and this is the bit worth reading.
dismissible: truefrom the host means "the reader may retire this permanently" →localStorage. A notice withoutdismissibleis mandatory, so it can be cleared for the current session only and returns on the next load. In this PR the banners only ever offer the X on dismissible notices, so the session tier has no UI yet — it's what #2668's inbox and #2681's hide build on, and it is tested directly.refreshis optional and this is the only caller. Onevisibilitychangelistener, throttled to at most once per 30s, callingsource.refresh?.(). Nothing in this feature polls on a timer — nosetIntervalis added anywhere. If the host doesn't implementrefresh, this is a no-op and notices simply update whenever the host publishes.Store rather than context. Module-level state +
useSyncExternalStore, because two unrelated subtrees read it (the banners here, the header button in #2668) and neither owns the other. It also gives a stable snapshot to the React Compiler and picks up the cross-tabstorageevent for free.Also here:
notices.ts'sTangleNoticeAction/TangleNoticeSourcestop being exported now that nothing outside the module names them, andknip.jsondrops bothsrc/config/announcements.tsandsrc/config/notices.tsfrom its ignore list — the first because the file is gone, the second because it now has real consumers.How to test
Install a source in the console and dispatch the event (see
src/config/NOTICES.md):masterwith no announcements installed.Where this sits
This is the merge point. Stopping here is a complete, shippable replacement for the announcements system. #2668 and #2681 add surface on top and are independently droppable.