feat(announcements): shared announcement centre components - #24
Merged
Conversation
… note single-instance rule - theme.ts: add borderClass back to PriorityTheme so Task 2's coloured left edge reads it from the same map instead of a second, independently maintained border-colour map. Critical references --announcement-alert directly instead of duplicating its rgb triplet; major/normal ported unchanged from sphere-backoffice/src/lib/announcementTheme.ts. - theme.test.ts: new test pinning all three priorities including borderClass, plus a source-text regression guard (mirrors backoffice's announcementTheme.test.ts) that no pillClass/borderClass line contains an unresolved interpolation artefact. - useAnnouncements.ts: export a named UseAnnouncementsResult return type instead of an inferred shape, and document at the top of the hook that it is meant to be called once per app with its values passed down, since the session-modal-once guarantee is module-level state.
…f an opacity modifier Replace border-[var(--announcement-alert)]/28 with a plain literal border-[var(--announcement-alert-border)], backed by a new tokens.css custom property that bakes in the 0.28 alpha directly. Removes reliance on Tailwind's opacity modifier applied to a var() arbitrary value, a form with no precedent elsewhere in this codebase and no guarantee of support across every consumer's Tailwind version. major/normal already used no modifier and needed no change. theme.test.ts now also asserts no borderClass carries an opacity modifier.
…aria attrs - useAnnouncements: re-arm the alive ref on every effect mount instead of only setting it false on cleanup. StrictMode's dev mount->cleanup->mount cycle left it permanently false, so isLoading never cleared in any host wrapped in <StrictMode> (sphere, sphere-quest-frontend). - AnnouncementBell: close the popover when a row is clicked. The click lands inside rootRef so the outside-click handler never catches it, leaving the popover open behind whatever modal onOpenItem triggers. - AnnouncementBell: add aria-haspopup/aria-expanded to the bell trigger so screen readers get a signal the popover opened. Adds regression tests for all three (StrictMode render, popover-closes-on row-click, aria attribute assertions).
The popover row centre-cropped the 3:1 hero banner into a 48px square, showing readers a different framing than the one the author picked, with no way to control it. Every row now renders only the type glyph — smaller (34px) and quieter than before — so rows are structurally identical and the banner keeps its one job in AnnouncementModal, which has the width to show it uncropped. The glyph is now selectable per announcement rather than fixed by type: ANNOUNCEMENT_ICONS/ANNOUNCEMENT_ICON_KEYS define a closed set of lucide icons (the five type defaults plus a handful of extras), and iconForAnnouncement(type, icon) resolves an explicit override, falling back to the type default for anything unset or unrecognised so a bad key can never render an empty square.
…terval useAnnouncements fetched the feed exactly once per mount, so a long-lived tab could show a stale unread badge indefinitely. Add two refresh triggers - visibilitychange to 'visible', and a 30-minute setInterval that skips its tick while the document is hidden - both cleaned up on unmount and both gated on `enabled`. A background refresh must never open a modal (mid-send / mid-address-entry is the wrong moment to interrupt), so `load` now takes a 'mount' | 'refresh' mode: only 'mount' may claim the module-level session flag and set autoOpenId. The exported `refresh()` uses the same non-opening semantics. Bootstrap allowance and one-modal-per-session behavior on mount are unchanged.
The View all announcements footer sits inside the bell's rootRef, so neither the outside-click handler nor Escape ever fires for it - the same blind spot handleRowClick already guards against one button higher. Navigating does not clean it up either: every portal routes its announcement centre under the same layout as the bell, so the bell stays mounted across the navigation and the popover hangs over the destination page until something unrelated closes it.
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.
Shared UI for the platform announcement centre: the bell and its popover, the announcement modal, the row, a small Markdown renderer, the feed hook, and the design tokens they use. Consumed by sphere, sphere-dev-portal and sphere-quest-frontend; authored in sphere-backoffice.
What is in here
AnnouncementBell— unread badge, popover list, "View all announcements" footer (rendered only whenonViewAllis supplied).AnnouncementModal— full announcement with hero, body and per-portal CTA; focus trap.AnnouncementRow— one entry, shared by the popover and the archive page so both look identical.useAnnouncements— feed, unread count, read state, auto-open selection, background refresh on tab-visible and on a 30-minute interval.theme.ts/icons.ts— priority and type styling from brand tokens, plus the admin-selectable icon set with a per-type default.types.ts— the client port (getFeed,getArchive,markRead,markAllRead,recordClick) each portal implements.media:announcementadded as a media owner type, so the composer can upload hero images through the existing pipeline.Notes for review
criticalandmajorauto-open as a modal;normalwaits in the bell. One modal per session.onViewAll(06f6951). Its regression test lives here, and nothing in the consuming portals would catch its absence — so any consumer pinning this library must pin a version at or above the one containing that commit.theme.test.tsasserts no${appears in any pill class.npm run typecheckandnpm run buildare clean.npm run lintcannot run in this repo at all — eslint is not in devDependencies and there is no config; pre-existing and tracked separately.After merge
Publish via the "Publish to npm" workflow, then the four consumers swap their
file:../sphere-uilinks for that version and regenerate their lockfiles.