Files
open-design/docs
edb6292f1c feat: post-update what's new card on home with release feed highlights (#5071)
* add post-update what's new card on home with release feed highlights

After an update, the home surface shows a one-time bottom-right card with
the release highlights (title, copy, optional image) and a "See what's new"
link. Content ships in the release feed metadata.json as an optional
whatsNew block sourced from tools/release/whats-new/<baseVersion>.json;
versions without highlights fall back to generic copy linking the release
notes. The card shows once per version, keyed on a locally stored
last-seen version, and never on fresh installs.

- packages/contracts: WhatsNewContent/WhatsNewResponse DTOs + analytics props
- daemon: GET /api/whats-new (channel feed fetch, 1h cache, stale/null
  degradation) + od whats-new CLI mirror
- web: WhatsNewPopup card (CSS Module, i18n chrome in 19 locales,
  surface_view/ui_click tracking), lastSeen decision logic in lib/whats-new
- release tooling: publish-metadata forwards the optional whatsNew block and
  fails loudly on malformed files; docs in tools/pack/AGENTS.md and
  tools/release/whats-new/README.md

* fix: gate whats-new card on active home view, soften Escape, validate locale overrides

Address review feedback on the post-update what's-new card:

- EntryShell passes `active={view === 'home'}` into WhatsNewPopup, and the
  component defers its fetch/show decision (and `page_name: 'home'`
  analytics) until Home is the active entry view, so direct routes like
  /projects no longer surface the card or emit home-page events.
- The document-level Escape listener now ignores already-handled events
  (`defaultPrevented`) and hides the card without marking the version as
  seen or firing the dismiss analytics event, so a stray Escape no longer
  permanently spends the once-per-version card.
- publish-metadata validates the optional `locales` block at publish time
  (plain-object shape, non-empty `title`/`body`, HTTPS `linkUrl`) so
  malformed locale overrides fail the publish loudly instead of being
  silently dropped by the daemon parser; README documents the contract.

Generated-By: looper 0.9.11 (runner=fixer, agent=claude-code)

* fix(release): validate whats-new URLs by parsing, not prefix check

Replace the startsWith("https://") checks in readWhatsNewBlock with a
shared assertHttpsUrl helper that trims, constructs new URL(...), and
requires protocol === "https:", mirroring the daemon readHttpsUrl parser
so values it would silently drop (e.g. "https://") fail the publish
loudly for imageUrl, linkUrl, and locale linkUrl.

Generated-By: looper 0.9.11 (runner=fixer, agent=claude-code)

* fix(web): stop whats-new card fetch race from swallowing the card on first Home load

The card's fetch effect armed its once-guard when the fetch STARTED, and the
effect teardown re-armed it — so React StrictMode's double-invoke (dev) or a
Home toggle mid-fetch cancelled the first fetch, the second effect run saw the
guard still set and bailed, and the resolved-but-cancelled fetch re-armed the
guard without ever showing the card. The card then never appeared until the
Home view toggled again.

Arm the guard only once a decision is actually REACHED (fetch resolved with a
payload); keep the `cancelled` flag purely as a state-update guard that no
longer re-arms the fetch. A superseded fetch drops and the surviving one
records the decision, so the card shows on first Home activation and a genuine
mid-fetch Home exit still retries on the next activation.

Red spec: apps/web/tests/components/WhatsNewPopup.test.tsx renders under
StrictMode and asserts the card appears — red on the previous effect, green now.

* feat: add silent updater preference

* fix: block posthog in beta dry runs

* refactor(whats-new): drive the home card from a dedicated R2 document

Source the post-update "what's new" card from a single hand-curated document
on a dedicated R2 bucket (https://whatsnew.open-design.ai/whats-new.json,
overridable via OD_WHATS_NEW_URL) instead of per-version release metadata.
Operators edit that one file after a release; there is no publish tooling.

- Show-once is keyed on the document's `id`, not the app version: the card
  shows whenever the id differs from the last one the client recorded, first
  sight on a fresh profile included. This removes the version gating that could
  never tell a fresh install from an existing profile upgrading across the
  feature boundary (the reviewer's blocking finding) -- content identity
  settles it instead.
- Daemon service just fetches the document and parses {id, ...content}; no
  channel feed or release-version matching. Graceful degrade to no card on any
  failure is preserved.
- Drop the now-dead release publish tooling: tools/release/whats-new/*, the
  whatsNew forwarding in publish-metadata.ts, and its tests. Ops flow documented
  in docs/whats-new.md.
- Remove the generic no-content fallback card and its two i18n keys; the card
  shows only when the document has content.

Contracts, `od whats-new` CLI, and analytics updated to the {version,id,content}
shape.

* style(whats-new): align the home card with the marketing design

Match the reference card: brand serif body copy (var(--serif), same face as
the home hero), body + CTA stacked in a left column with the illustration
stretching to the card's full height on the right so there is no dead space,
a subtle CTA, and a slightly wider, softer card. CSS-module + markup only;
no behavior change (test ids and flow are untouched).

* style(whats-new): drop the title row, float the close over the card

Per design feedback the "Design system sync" label row is removed so the serif
body copy starts at the top edge; the ✕ now floats top-right as a bordered chip
that stays legible over the illustration. The title still labels the dialog via
aria-label for screen readers. Test asserts on the body + dialog name instead.

* style(whats-new): version eyebrow + release title as the card copy

Per design feedback: the top row is the "Open Design x.y.z" version eyebrow
(left) with the close button (right); the release title renders as the main
serif line (1-2 lines) and the illustration stretches to the card height; the
long body is dropped from the card. CTA + release-notes action unchanged.

* fix(whats-new): gate the card to release channels so it can't intrude on CI

The R2 rearchitecture dropped the daemon's channel gate, so development/CI
builds fetched the live document and showed the card on Home. That broke the
P0 "settings dialog reachable" smoke (getByRole('dialog') matched both the
settings modal and the what's-new card, which also carries role="dialog") and
would have dirtied every Home visual baseline. Restore the gate: only
beta/prerelease/preview/stable fetch the document; other channels resolve to no
card and never touch the network. OD_WHATS_NEW_URL still opts any channel in
for e2e fixtures that exercise the card on purpose.

* fix(whats-new): use role="complementary" instead of "dialog" for the toast

The card is a non-modal, dismissible announcement — not a dialog. As
role="dialog" it collided with getByRole('dialog') in the P0 Settings smokes
(settings-dialog-reachable, AMR reopen, Local CLI Codex fallback), which
strict-mode-failed the moment the card rendered on Home. A complementary
landmark is the correct a11y role for supporting content and never matches a
dialog query, so those P0s pass regardless of whether the card is showing. The
daemon channel gate (5f17873e0) still keeps the card off development builds.

---------

Co-authored-by: free666799 <293857035+free666799@users.noreply.github.com>
Co-authored-by: PerishCode <perishcode@gmail.com>
Co-authored-by: lefarcen <935902669@qq.com>
2026-07-10 08:38:49 +00:00
..
…
…