Unified invite landing for collection and document invites - #638
Unified invite landing for collection and document invites#638icarusz wants to merge 21 commits into
Conversation
Both invite entry paths (#/join-collection/... and #/share/...) now land on
one InviteLanding card: kicker, inviter line, title, display-only payload
preview, what-is-QuartoHub explainer, and a single CTA. Signed-out users see
the card (not the login wall) with a GIS "Continue with Google" CTA that
round-trips back to the invite via the pre-auth hash; signed-in users get
one-click join/open. Share routes no longer auto-connect on load: the URL is
still scrubbed on mount, but connection happens on the CTA click. After
joining, the user lands in the editor on the intended file with a one-time
dismissible welcome banner (per-target localStorage dismissal).
- routing.ts: ShareRoute gains from=/preview=; JoinCollectionRoute gains
preview=/start= (base64url JSON codec in utils/invitePreview.ts, versioned,
capped at 3 projects x 2 files, display-only). Legacy URLs parse unchanged;
malformed payloads degrade to undefined. savePreAuthHash takes an explicit
hash; clearPreAuthHash drops it once an invite is consumed.
- App.tsx: pendingShare captured at boot; connectToSharedProject hoisted with
{quiet, addToSet} opts; collection CTA subscribes then opens the start=
target (else home); invite-first silent root creation now covers share
invitees; ephemeral preview boots keep eager connect.
- JoinCollectionLanding (name/color form) deleted; identity comes from the
Google-account name upgrade with anonymous fallback.
- Sender side: buildInviteUrl embeds preview from cached peek summaries and
start=first project's first file; buildShareableUrl gains {from, preview},
wired at both call sites (ProjectsHome menu, Editor ShareDialog).
- AuthProvider SignInButtonProps gains text variant ('continue_with').
- Dev harness pages for all landing variants + welcome banner.
Tests: TDD throughout (routing/codec/component/banner suites written first,
verified red). 476 unit + 118 integration + 133 wasm green except the
pre-existing main breakage tracked in bd-qvjc6gdp (useAutomergeSync mock,
BranchBar). e2e share-link specs updated to click through the landing; suite
71/72 (BranchBar again). Verified end-to-end in a real browser against a
local hub: share link -> landing -> CTA -> editor on file -> banner, and
collection invite -> join -> editor on start target -> banner.
Design handoff bundle (spec + authoritative mocks) committed at
design_handoff_invite_landing/; plan at
claude-notes/plans/2026-09-01-unified-invite-landing.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…xdcxbpq)
Andrew's manual testing surfaced invite previews showing "0 files" for
populated projects. Cause: two layers dropped the cached summary when an
entry was copied into a collection. ProjectsHome's entryFor projected only
{indexDocId, syncServer, description}, and addProjectToSet discarded
entry.summary on write. The collection copy therefore had no file counts
until the project's next open (updateProjectSummaryEverywhere then healed
every copy — which is why reopening "fixed" it).
Both layers now carry the summary: entryFor includes it when the source
entry has one, and addProjectToSet persists it on create and adopts it on
re-add only when the existing entry has none (freshness stays owned by
updateProjectSummaryInSet, so a stale re-add can never clobber a live
summary).
TDD: three new schema tests written first and verified red; 75/75 green
after. Verified live against a local hub: adding a project to a brand-new
collection immediately shows "2 files" on the copy, and the collection's
invite preview payload decodes with the real fileCount without reopening.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-fxdcxbpq) Direction change from Andrew's design review: the invite is to a collection, so joining should land on the QuartoHub home screen with that collection's section promoted to the top — not cold-open the first document's editor. - handleCollectionCta: subscribe, then navigate home; the start-target editor open is gone. ProjectsHome gains promoteCollectionId (session- scoped, set on join) and sorts that section first. - InviteLanding: startName prop and "Join and open X" CTA removed; the collection CTA reads "Join <name>" (or "Join collection" on legacy links). - buildInviteUrl no longer emits start= (the parser still tolerates it on links already in the wild — verified live against one). - Welcome banner: now scoped to the invite target — a document invite's banner shows only on that project; a collection invite's banner shows on the first project opened from that collection, reworded to "Carlos invited you." (the "suggested starting here" claim died with start=). TDD: banner-copy test updated first and verified red; 235 affected tests green after. Verified live against a local hub: join CTA lands on home with the joined collection sorted above an older one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Andrew's rule from design review: if the user is about to go through the
Google sign-in flow for the first time, frame the invite as joining; if
they are already signed in, one click just opens the thing.
- Signed out: a "Join to collaborate on <target>" lead-in renders above
the Google button (whose label GIS locks to "Continue with Google").
The target is the file name for document invites (from the preview,
falling back to the title) and the collection name otherwise.
- Signed in: the CTA is "Open <name>" for both kinds ("Open collection"/
"Open document" on legacy links); the busy label is "Opening…". The
collection join still happens on click — only the verb changed.
TDD: CTA-matrix tests updated first; 18/18 green, CSS lint at baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The header's hand-drawn stacked-bars mark is replaced with the app's actual quarto-icon.svg (the segmented circle), tinted for dark mode via --logo-filter like the other uses of the asset. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Andrew's manual testing: clicking "People & invite…" in a collection's ⋯ menu did nothing. The activation click bubbled from the MenuItem to the Menu root's closer, whose onClose (closeAllMenus) also resets membersFor — cancelling the popover in the same React batch that requested it. Marked the item keepOpen (its onSelect already closes the menu explicitly), matching the component's documented escape hatch. TDD: new ProjectsHome.integration.test.tsx drives the real menu path (actions button -> menu item -> popover role=dialog), verified red then green; full integration suite 119/119. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Captured from the dev harness pages so reviewers can assess the revised invite screens next to the design mocks without running the branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
Implemented screensLight and dark, for review without checking out the branch. The design mocks these implement are in Collection invite — signed out"Join to collaborate on …" leads into the sign-in button (GIS owns that button's label).
Collection invite — signed inOne click; lands on the projects home with this collection promoted to the top.
Project invite — signed outA
Project invite — signed inThe payload lists the project's contents (opened file first, then other paths, then the total), the project-level analogue of the collection card listing its projects.
Project invite — single-file projectNothing to list, so the payload box is omitted rather than showing a box around a lone count.
Departures from the 3a mocks
Captured from the dev-harness pages at 2x. The dark column earns its place: adding it is what surfaced the two theming defects fixed in Two behaviors still have no capture: the editor welcome banner and the post-join home with the promoted collection. The harness pages needed to show them honestly don't exist yet (the projects-home fixture has no collections), and earlier attempts implied the wrong thing, so they were removed rather than posted misleading; run the branch, or see 🤖 Generated with Claude Code |
|
I think I will spend some time (maybe early next week) harmonizing the different styles a bit. The new editor updates give it a different feel. |
The welcome-banner capture was a 40px bar above 900px of blank page (the harness renders the banner alone, and it was captured fullPage), and it did not show the inline rename its caption advertised. The projects-home capture could not show the behavior the PR describes: the harness fixture FAKE_COLLECTIONS holds only the root set, so the page renders 'Everything else' with no collection sections — nothing of the promoted-collection landing. Replacements need harness pages built for the purpose; removing them beats leaving reviewers with artifacts that imply the wrong thing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dcxbpq) Reviewing the PR screenshots turned up two theming defects in the new components. Both were invisible to the existing tests — jsdom does not resolve color-mix, and every capture was light-mode. 1. The payload preview box drew its border, row dividers, thumbnail frame, ruled texture, and filename chip from the brand *primitive* --posit-blue-light-1. Primitives do not flip with the theme, so in dark mode they resolved to #D1DBE5 on a #213D4F card — near-white grid lines. theme.css documents the layering (primitives → semantic → component); these now derive from the semantic --border-color via opaque color-mix. At 50% that reproduces the handoff's #D1DBE5 in light mode to within one channel, and gives a hairline in dark. 2. Surfaces were painted with --bg-subtle, which is translucent in *both* themes (rgba(0,0,0,.03) / rgba(255,255,255,.05)), against .claude/rules/hub-client-theme.md. The landing's page surface is now an opaque mix, the filename chip sits on --bg-modal, and the banner's tint mixes toward --editor-bg, the opaque surface it sits above. Adds e2e/invite-landing-theme.harness.spec.ts, which runs both themes (harness config runs in hub-client-e2e.yml) and pins the two invariants theme-neutrally: dividers stay under 2.5:1 against the surface behind them, and every painted surface is fully opaque. Verified as a real guard by reintroducing both defects: the dark-divider test and both banner tests fail, and — as with the original bug — the light-divider test still passes, which is exactly why this hid. Note the first version of the spec passed for the wrong reason: Chromium reports color-mix results as `color(srgb 0.81 …)` with 0-1 floats, which the colour parser read as 0-255 channels. Fixed before relying on it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Post-fix captures, clipped to the landing surface (no dead page space) at 2x, with a dark-mode counterpart for each of the four card states — the theme the earlier captures never covered, and where the defects fixed in ea9b46e were hiding. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ding # Conflicts: # hub-client/changelog.md
…(bd-fxdcxbpq)
Andrew's design review: a #/share/… link grants access to a whole
project (the index document) and merely opens at one file, so calling it
a DOCUMENT INVITATION mislabels it and leading with file-level detail
puts the emphasis in the wrong place.
- Kicker is PROJECT INVITATION; the invite kind is 'project' throughout
(component, banner, App, preview payload). The signed-out lead-in and
the CTA name the project, never the file the invite happens to open at
("Join to collaborate on Quarterly report", "Open project" on legacy
links). The welcome banner names the project too, rather than saying
"shared this document with you".
- The ruled-paper thumbnail is gone. It could only ever draw an empty
page — `preview=` deliberately carries no document content — so at
74px it read as a big empty box, worst of all on a single-file
project. The payload is now one mono row listing the project's
contents (opened file first, then other paths, then the total), which
is the project-level analogue of the collection card listing its
projects.
- With one file there is nothing to list, so the payload box is omitted
entirely: the title already names the project, and a box holding a
lone count is just a placeholder. New harness page
`invite-landing-project-single-file` shows that state.
- Fixes a pluralization bug this surfaced: fileSummary hardcoded
"${n} files", so a one-file project read "1 files" — on project cards
and inside collection rows.
- Wire format: the project marker is 'p'; the decoder still accepts the
'd' spelling from this feature's own development so links generated
while dogfooding keep resolving. Covered by a test.
Screenshots recaptured (the four document captures showed the removed
thumbnail); the single-file state is now among them, light and dark.
Green: typecheck, 1127 unit, 120 integration, 133 wasm, 6/6 theme
harness, 72/72 e2e, lint:css clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The commit was amended to drop stray package-lock churn, which changed its hash after the changelog entry was written. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>










Summary
Redesign of QuartoHub's two invite entry paths (bd-fxdcxbpq), per the design handoff in
design_handoff_invite_landing/(spec + mocks committed on this branch):InviteLandingcard for both invite kinds: kicker, inviter line, title, display-only payload preview, what-is-QuartoHub explainer, single CTA. ReplacesJoinCollectionLanding(the name/cursor-color form is gone; identity comes from the Google account with the anonymous fallback).buildInviteUrl/buildShareableUrlembedfrom=and a display-onlypreview=(base64url JSON, capped, never doc ids) built from the cached peek summaries. Legacy links parse unchanged.Out of scope (deferred per the handoff): zero-setup cold start, seeded first-run samples, revocable/role-scoped links, sender UI redesign.
Verification
e2e/invite-landing-theme.harness.spec.ts).origin/mainmerged in atf60e9ca8. Against the merged tree, every tier is green: typecheck clean, 1123 unit, 120 integration, 133 wasm, 72/72 Playwright e2e, 6/6 theme harness,npm run build:allclean,lint:cssclean.41da392c(bd-v51cly8i) fixed them, anda4c3fb9ccleared thelint:cssbaseline. Nothing is red on this branch.start=is correctly ignored.npm run build:wasmbeforetest:wasm, or main's new include-failure fixtures fail against a stale WASM artifact.Plan and running log:
claude-notes/plans/2026-09-01-unified-invite-landing.md. Screenshots of the implemented screens (light and dark) are in a comment below.🤖 Generated with Claude Code