Unified search: prerendered→live hydration (modes + diagnostic attribute)#5177
Conversation
HydratableCard renders an HTML-backed search row inert, then swaps it for a live <CardRenderer> on a host-side gesture: none (stay inert), hover, click, or touch. The trigger is a listener owned by the rendering path itself — not the operator-mode overlay — so hydration behaves identically in operator mode, host mode, and published views. A full live row (no inert HTML) resolves its instance immediately; an error rendering never hydrates. Hydration resolves links.self through the getCard resource (one GET, deposit in the Store, track live) and renders the live card under the collection's render type. The optional cardComponentModifier from the card context is applied to whichever element is shown, so the overlay's ElementTracker anchors to the inert HTML and re-anchors to the live card after the swap. A data-hydration attribute carries the gesture (or "hydrated") for diagnosis. Tests cover host mode, published view, and operator-mode overlay coexistence (re-registration with the ElementTracker), plus the none and error cases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fc4b80158e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Preview deploymentsHost Test Results 1 files 1 suites 1h 53m 4s ⏱️ Results for commit 0b23d64. Realm Server Test Results 1 files ± 0 1 suites ±0 10m 6s ⏱️ -48s Results for commit 0b23d64. ± Comparison against earlier commit cbbc07e. |
There was a problem hiding this comment.
Pull request overview
Introduces a new host-side HydratableCard component to support unified-search rows that initially render inert prerendered HTML and optionally “hydrate” into a live CardRenderer on a user gesture, while preserving operator-mode overlay tracking via cardComponentModifier.
Changes:
- Added
HydratableCardcomponent implementing gesture-based hydration (none/hover/click/touch) and a diagnosticdata-hydrationattribute. - Integrated Store-backed live resolution via
getCardonce hydration is triggered (or immediately for fully-live rows). - Added integration tests covering host mode, published view, operator-mode overlay re-anchoring,
none, and error rows.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| packages/host/app/components/card-search/hydratable-card.gts | New component that swaps inert HTML for live card rendering on gesture and reuses overlay element tracking. |
| packages/host/tests/integration/components/hydratable-card-test.gts | New integration tests validating hydration behavior and operator-mode ElementTracker re-registration. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…source type - Consume `getCard` through `GetCardContextName` (a `@cached` getter so the provider is injected before it runs) instead of importing the resource helper directly, so the component uses the Store the active host context provides. - Guard the `cardContext` read with an `isDestroying`/`isDestroyed` check plus an owner-destroyed catch, so reading the context while the component is being torn down can't throw. - Carry a `type` arg (`card` | `file-meta`) through to `getCard`, and resolve the live instance as `BaseDef`, so a full live `file-meta` row renders its `FileDef` instead of nothing. - Drop the redundant explicit tracker on the live `CardRenderer`: a delegate-rendered card registers itself through the card context, so the inert→live swap re-anchors the overlay on its own. The inert HTML still needs the explicit tracker (raw HTML isn't a card component). Tests provide `GetCardContext`, and the operator-mode test asserts precisely that the re-registered live element carries the live card instance. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ve instance Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s hydration - liveCard short-circuits before touching cardResource when there's no id to resolve, so an inert row that never hydrates (e.g. mode none) never creates a getCard resource. The resource is `@cached` (one per component) and parented to the component, so getCard drops its Store reference on teardown — no leak. - Keep the live render wrapper-free: the diagnostic / test attributes ride `...attributes` onto the card-api container (which spreads them regardless of the userland template), so no extra element is needed (which would perturb grid / child-selector styling). Locked in with a `data-hydration="hydrated"` assertion after the swap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| let event = | ||
| mode === 'hover' | ||
| ? 'mouseenter' | ||
| : mode === 'touch' | ||
| ? 'touchstart' | ||
| : 'click'; |
There was a problem hiding this comment.
How about a focus event?
There was a problem hiding this comment.
[Claude Code 🤖] Good call — done in 306ff3f. In hover mode the trigger now also listens for focusin (it bubbles, unlike focus, so keyboard focus landing anywhere in the card subtree counts), hydrating identically for pointer-hover and keyboard-focus. Added a test asserting a focusin hydrates the inert HTML and flips data-hydration to hydrated, same as mouseenter.
The hover gesture hydrated only on `mouseenter`, leaving keyboard users on the inert prerendered HTML. In hover mode the trigger now also listens for `focusin` (which bubbles, so focus landing anywhere in the card subtree during keyboard navigation counts), hydrating identically for pointer and keyboard. Test: in hover mode a `focusin` hydrates the inert HTML into the live card and flips `data-hydration` to `hydrated`, exactly as `mouseenter` does. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
burieberry
left a comment
There was a problem hiding this comment.
Here's from claude -- do as you see fit:
Must fix (confirmed bugs)
#1—hydrationIdstale on@cardIdchange → change to@tracked private hydrated = false#2—OWNER_DESTROYED_ERROR = 'destroyed'→ replace with the full string fromprerendered-card-search.gts
Should fix (pre-existing, surfaced by this PR)
#3—search-content.gtshas= 'OWNER_DESTROYED_ERROR'(the name, not the value) — broken guard, easy one-liner fix
Worth a comment at minimum
#4— the@component == nullfast-path has no gesture gate; a comment explaining that full live rows always resolve immediately would prevent a future caller from misusing@isErrorreactively
Leave for follow-up (cleanup, no runtime impact today)
#5—noopCardModifierduplication,trackElementguard duplication,@typesnapshot — none break current behavior
… match - Track hydration as a boolean instead of the `@cardId` captured at gesture time, so a recycled HydratableCard whose `@cardId` changes resolves the new card rather than a stale captured one. - Match the full owner-destroyed message in the `trackElement` guard instead of the loose `destroyed` substring, so an unrelated error isn't swallowed. - Fix the same guard in `search-content.gts`, which compared against the literal name `OWNER_DESTROYED_ERROR` (so it never matched) instead of the actual error message. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
[Claude Code 🤖] Thanks — worked through these (cbbc07e + 306ff3f):
|
Phase 1 / Stream B (host rendering) of Unify prerendered-card-search and card-search: prerendered→live hydration for unified search results.
Behavior
An HTML-backed unified search row ships an identity-only
card+ inertrendered-html— the live data is not in the response, so making it a live, running card costs oneGET links.self.HydratableCardowns that lifecycle:<CardRenderer @codeRef={{renderType}}>on a host-side gesture:none(stay inert),hover,click, ortouch. The mode is a host UX choice and never travels on the wire.rendered-htmlelement, owned by the rendering path — not the operator-mode overlay — so it behaves identically in operator mode, host mode, and published views with no separate fallback.links.selfthrough thegetCardresource (one GET, deposit in the Store, track live) and renders the live card under the collection's render type.data-hydrationattribute carries the gesture (orhydratedafter the swap) for diagnosis — no user-facing affordance.Operator-mode coexistence
The overlay (container interaction) and hydration (making the card live) are orthogonal layers. The optional
cardComponentModifierfrom the card context is applied to whichever element is shown, so the overlay'sElementTrackeranchors to the inert HTML and re-anchors to the live card after the swap — the inert element unregisters as the live element registers. Host mode / published views have no such context, so a no-op stands in.Demo
This is a screen capture of the hydration
Screencast.from.2026-06-09.21-42-04.mp4
Tests
Integration tests render
HydratableCarddirectly (the full search-UI migration is a later step), covering:GET links.self, the card enters the Store, inert HTML swaps to a liveCardRenderer.ElementTracker(exactly one tracked element, now the live one).nonestays inert, marksdata-hydration="none", and never fetches; error rows never hydrate.All 5 pass locally against the host test-services stack.