githubnext/chopinPublic

Let's compose rich plans together

Animate lazy navigation dialogs on first open #335

MergedMaggieAppletonmerged 3 commits intomainfromrefs/pull/335/head
  1. MA

    ## Why Lazy-loaded navigation dialogs mounted already open once their chunk arrived, so the first open snapped in with no transition. Search also showed a short "Searching" panel, then jumped taller when results arrived.

    ## What changed - `@starting-style` on `.motion-modal.is-open` and its content, so first paint always animates. - After 1.5s idle the sidebar (already lazy) dynamically imports `dialog-prefetch`, which warms the five dialog chunks and the Search recent list. Initial bundle 255803 B raw / 80578 B gzip locally (CI gzip runs about 900 B higher). - Search keeps its last unfiltered results, keyed by user, project set and the document catalogue identity (so create/rename/archive/delete/restore invalidate it), and opens on them while revalidating; clearing a query restores them instead of the empty state; the result list reserves the loading/empty height. - Reduced motion unchanged (existing rule removes transitions).

    ## Screenshots

    Search dialog over the Projects sidebar after first open:

    ![Search dialog with Projects sidebar](https://github.com/user-attachments/assets/fb6f738b-a1a7-476d-8faf-7ff80c3dc9ed)

    ## Testing - `bun run types`, `bun run build` (budget passes), `bun test apps/web` (500 pass). - `bun run ci`: only the design-contract dynamic-owner check fails (see below). E2E left to CI.

    ## Needs a design-contract review `apps/web/src/navigation-shell.tsx` in `scripts/design-contract/exceptions/dynamic-web.json`: line positions shifted (new dialog chunk list and idle prefetch effect above the pinned class sites). No class or style logic changed. New file hash: 45afb74b5e6b46c4f0d35ec3a753643545d462ee433e2cb2ed4385772a2b346c.

    🤖 Generated with [Claude Code](https://claude.com/claude-code)

  2. MA
    Author

    **Review:** Changes needed

    Checked in a headless browser (fake server, 1440x900 and 390x844) against main (8795). One small regression in the search cache; the motion fix itself is good.

    **Motion: works.** Per-frame opacity sampled via rAF on first open after a fresh load: - main: `1.00` on the first frame (snaps in). - this PR: `0.00, 0.15, 0.28, 0.40, 0.51 ... 0.97` with content scale `0.96 -> 1`, same for Add project, Search, Rename and Delete permanently, both before the 1.5s prefetch fires (open immediately) and after it. Phone is the same. `prefers-reduced-motion: reduce` stays at `1.00` (no transition). - New document dialog: could not reach it in the fake environment (it only shows with 2+ writable projects and no current project; the fake user always lands in a document). It uses the same `NavigationDialog`/`.motion-modal` path, so I expect it to behave identically, but I did not observe it.

    **Bundle: fine.** Entry chunk is unchanged at 255974 B; the five dialog chunks total ~13 kB, are fetched with dynamic `import()` after 1.5s, and `index.html` has no new `modulepreload`. The `.catch(() => {})` keeps a failed prefetch silent.

    **Search cache** - Cross-project / archived: key is `includeArchived + repositoryIds`, so a different project set or archived mode never reuses it. OK. - Leak between users: cache is module-level and not keyed by user, but `signOut` ends in `location.assign("/")` (full reload), so it cannot survive sign-out or account switch. OK in practice; cheap hardening is to include `userId` in `recentKey`. - Failed projects are not cached. OK.

    **Findings** 1. (Should fix) `document-search-dialog.tsx` effect, line `if (query.trim() || lastRecent?.key !== recentKey) setSearch({ status: "loading" })`. When the user types a query and then clears it, the state still holds the previous filtered result and the effect no longer sets `loading` because a cache exists. Observed: type `zzzz`, backspace x4 -> palette shows "No documents yet" (the empty-state copy, wrong) for the whole revalidation (with a 1.5s API delay, until ~1.5s). On main it showed "Searching documents". Fix: when the query is empty and a cache exists, `setSearch({ status: "ready", results: lastRecent.results, failedProjectIds: [] })` instead of leaving the old state. 2. (Minor, expected tradeoff) Stale cache: archive a document and reopen search with a slow API; the archived document is still listed (and clickable) until revalidation finishes (~1.5s in my test, then it disappears). Same for rename/delete. Consider invalidating `lastRecent` when `documentAction`/delete/create completes, or accept it. 3. (Minor) "Stable height" only holds between loading and empty (84px reserve). The first-ever search still jumps 132px -> ~368px when results arrive, because nothing primes the cache. Warming it in the idle prefetch would fix that, or leave as is. 4. (Info) `bun run ci`: only the design-contract dynamic-owner hash failures for `navigation-shell.tsx` (lines 232, 808, 916); these are the expected review-hash failures.

  3. MA
    Author

    **Review addendum:** the `container` job fails in `bun run build` on the initial-JS gzip budget: `initial JavaScript 255970 B raw / 81514 B gzip exceeds budget 256000 B raw / 81500 B gzip` (14 B over; the local build only checked raw size). The `format, lint, types, tests` job fails on the expected design-contract hashes. Please trim the entry chunk by a few dozen bytes (for example, drop the `dialogChunks` indirection comment-free tuple in favour of a smaller helper, or move the idle-prefetch effect into an already-lazy module) before merging. `e2e` passes.

  4. MA
    Author

    Addressed review: (1) clearing a query restores cached results, not the empty state; (2) prefetch/warm moved out of the entry chunk (now 255803 B raw / 80578 B gzip locally, was 80628 which failed CI at 81514); (3) cache is keyed on the catalogue identity so any document mutation invalidates it; (4) idle prefetch warms the recent list; (5) userId added to the key. Limitation: prefetch lives in the sidebar, so on phones it starts once the drawer has mounted.

  5. MA
    Author

    **Review:** Looks good

    Re-verified at c4b8c12b in a headless browser (fake server, slow `/channels` API where noted): 1. Type `zzzz` then clear with a 1.5s API delay: shows the cached list immediately (368px), not "No documents yet". 2. Archive a document, reopen Search with a slow API: the archived row is gone (palette shows "Searching documents" until fresh results, no stale row). 3. First-ever Search after idle (6s): opens at 367px with results at 100ms, no 132 -> 368px jump. Note: if Search is opened while the warm request is still in flight (about 2.5s after load) it shows "Searching" briefly, which is fine. 4. First-open animation after moving prefetch to `dialog-prefetch.ts`: Add project, Search, Rename, Delete permanently all fade `0.00 -> 0.15 -> 0.28 -> ...` with scale `0.96 -> 1`, both at 0s and 2.5s after load; phone opens the same way; reduced motion stays at `1.00`. 5. Build: initial JS 255803 B raw / 80578 B gzip, within budget.

    Remaining: only the design-contract hash review for `navigation-shell.tsx` (expected).

  6. MaggieAppletonreferenced this in Move signed-out and lazy-only code out of the initial bundle #354last month on GitHub

  7. MaggieAppletonhead-ref-force-pushed 1de90d464384last month on GitHub

  8. MaggieAppletonauto-rebase-enabledlast month on GitHub

  9. MaggieAppletonmerged 128cc23d6234last month on GitHub

  10. MaggieAppletonclosedlast month on GitHub

  11. MaggieAppletonhead-ref-deletedlast month on GitHub