Home Valuation

The Panda website "How much is my home worth" flow (/how-much-is-my-home-worth), complete and designable (design: colors, background, shape, address bar, report layout; a CMA page with no lead form is MarketAnalysis,…

Fully headlessClient component@pandaidx/widgets 1.14.1

Anatomy

Every piece of Home Valuation as it nests in code. A root holds the data and state; its parts and hooks only work inside it. HomeValuationRoot must render in every layout (MLS and consent rules).

<PandaProvider>Mounted once in app/layout.tsx
<HomeValuation />complete widget

The Panda website "How much is my home worth" flow (/how-much-is-my-home-worth), complete and designable (design: colors, background,…

<HomeValuationRoot>Holds the data and state its parts and hooks read
<ValuationSearch />

Step 1: headline + address pill (Google predictions); picking an address runs the flow.

<ValuationLeadForm />

The lead form step that gates the report (gate="before-results"), with Change address; renders only on that step.

<ValuationReport />

The report of the address on screen (estimate, market, history, comparables, share, the after-results form).

<ValuationSteps />

The step on screen (search → form → report) in one part; with the root open dialog / sheet the report opens over the…

<ValuationSearchInput />

Only the address pill (no headline), for your own hero; picking an address runs the flow.

<ValuationReportHeader />

Report block (inside <ValuationReport> children): address, Back, home facts, Share.

<ValuationEstimateCard />

Report block: estimate value, confidence badge, $/sq ft, comps used and the low–high range (nothing without an…

<ValuationNoEstimate />

Report block: why there is no estimate (unit required, no record, not enough comps).

<ValuationMarketSection />

Report block: market tiles + trend and distribution charts side by side.

<ValuationMarketStats />

Report block: market label, sales count and the lower / typical / upper $/sq ft tiles.

<ValuationTrendChart />

Report block: median $/sq ft by quarter with the home's line.

<ValuationHistogram />

Report block: $/sq ft distribution with the home's bar.

<ValuationSaleHistory />

Report block: the home's past sales.

<ValuationCompsMap />

Report block: comparable sales as markers on a map (height; price pills by default).

<ValuationComps />

Report block: comparable sales list (each one values that home next).

</HomeValuationRoot>
</PandaProvider>

Installation

  1. 1
    npm install <tarball or @pandaidx/widgets>
    Terminal
    npm install https://github.com/pandaidx/panda-widgets/releases/download/v1.14.1/pandaidx-widgets-1.14.1.tgz
  2. 2
    import "@pandaidx/widgets/styles.css" in app/layout.tsx
  3. 3
    Wrap the site in <PandaProvider publicKey apiUrl preset theme> (app/layout.tsx)
  4. 4
    In each page (Server Component) call the widget's server loader, then render the widget with its data
  5. 5
    Allow the images: next.config images.remotePatterns [{ protocol: "https", hostname: "**" }] (listing and blog photos come from several CDNs)

Usage

HomeValuation
// app/(leads)/how-much-is-my-home-worth/page.tsx (no site header/footer): the websites page
import { HomeValuation } from "@pandaidx/widgets/client"
import { fetchForm, fetchHomeValuation } from "@pandaidx/widgets/server"
const config = { publicKey: process.env.NEXT_PUBLIC_PANDA_PUBLIC_KEY!, apiUrl: process.env.NEXT_PUBLIC_PANDA_API_URL }
export default async function HomeValuationPage({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {
const [form, initialValuation] = await Promise.all([
fetchForm({ ...config, spot: "home-valuation" }),
fetchHomeValuation({ ...config, searchParams: await searchParams }),
])
return <HomeValuation form={form} initialValuation={initialValuation} />
}
// A dark homepage section: the report first, the form under it, with a comps map
// <HomeValuation layout="inline" tone="dark" gate="after-results" showMap
// labels={{ title: "What is your home worth?", resultsFormTitle: "Get a professional valuation" }} />

Examples

A designed valuation page: dark section, square gold button, split report

HomeValuation
<HomeValuation
layout="inline"
design={{
background: "oklch(0.19 0 0)",
surface: "oklch(0.24 0 0)",
border: "oklch(1 0 0 / 0.14)",
accent: "oklch(0.8 0.11 85)",
shape: "square",
align: "center",
search: { style: "underline", shadow: "none", button: "label", buttonLabel: await getText({ id: "valuation-search-button", label: "Valuation · Search · Button", text: "Get my estimate" }) },
report: { layout: "split", style: "flat" },
}}
form={form}
initialValuation={initialValuation}
/>

Search in a hero, report in a side panel

HomeValuation
<HomeValuationRoot open={{ mode: "sheet", size: "lg" }} gate="after-results">
<section className="grid min-h-[70vh] place-items-center bg-muted px-6">
<ValuationSteps />
</section>
</HomeValuationRoot>

Your own two-column page

HomeValuation
<HomeValuationRoot gate="none">
<div className="mx-auto grid max-w-6xl gap-10 lg:grid-cols-[360px_1fr]">
<ValuationSearch />
<ValuationReport />
</div>
</HomeValuationRoot>

Your own report: only the estimate and the comps on a map (blocks inside ValuationReport)

HomeValuation
<HomeValuationRoot gate="before-results" showMap>
{/* The input stays; ValuationLeadForm and ValuationReport render only on their step. */}
<ValuationSearchInput className="mx-auto max-w-xl" />
<ValuationLeadForm />
<ValuationReport className="mx-auto max-w-3xl">
<ValuationReportHeader />
<div className="grid gap-6 lg:grid-cols-2">
<ValuationEstimateCard />
<ValuationCompsMap height="320px" />
</div>
<ValuationComps />
</ValuationReport>
</HomeValuationRoot>

CMA: full-screen map with a floating side panel (the reference layout; draw it your own way)

HomeValuation
"use client"
import { HomeValuationRoot, ValuationReport, ValuationCompsMap, ValuationEstimateCard, ValuationMarketStats, ValuationTrendChart, ValuationComps, useValuationReport } from "@pandaidx/widgets/client"
const Range = () => {
const report = useValuationReport()
const estimate = report?.valuation.estimate
if (!report || !estimate) return null
const { formatCurrency } = report
return (
<div className="flex justify-between text-sm"><span>Min {formatCurrency(estimate.low)}</span><span>Max {formatCurrency(estimate.high)}</span></div>
)
}
export const CmaMap = ({ valuation }) => (
<HomeValuationRoot gate="none" initialValuation={valuation} compHops={false}>
<ValuationReport again={false} className="relative h-dvh gap-0">
{/* The map fills the screen; the panel floats over it. Same pill for the home as for the comparables. */}
<ValuationCompsMap height="100%" frame={false} className="min-h-0 flex-1" marker="pill" radius="9999px" subject="pill" onSelect="popup" renderPopup={(comp) => (<div className="rounded-xl bg-background p-3 shadow-lg">{comp.address}</div>)} />
<aside className="absolute inset-y-4 left-4 flex w-[22rem] max-w-[calc(100%-2rem)] flex-col gap-5 overflow-y-auto rounded-2xl bg-background p-5 shadow-xl">
<ValuationEstimateCard />
<Range />
<ValuationMarketStats />
<ValuationTrendChart />
<ValuationComps />
</aside>
</ValuationReport>
</HomeValuationRoot>
)

A block drawn your own way with the hook

HomeValuation
"use client"
import { useValuationReport } from "@pandaidx/widgets/client"
export const BigNumber = () => {
const report = useValuationReport()
if (!report?.valuation.estimate) return null
const { valuation, formatCurrency } = report
return <p className="text-6xl font-semibold">{formatCurrency(valuation.estimate.value)}</p>
}
// <ValuationReport><BigNumber /><ValuationComps /></ValuationReport>

The site's address search (client component, styled to the site)

MarketAnalysis
"use client"
import { useAddressSearch } from "@pandaidx/widgets/client"
// The site's own address search: its input, list and button in the site's style. Picking an address opens the CMA page.
export const AddressSearch = ({ placeholder, buttonLabel }: { placeholder: string; buttonLabel: string }) => {
const { query, setQuery, suggestions, loading, pending, select, submit } = useAddressSearch({ href: "/home-market-analysis" })
return (
<div role="search" className="relative mx-auto w-full max-w-xl">
<div className="flex items-center border-b border-foreground/30">
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
onKeyDown={(event) => event.key === "Enter" && submit()}
placeholder={placeholder}
aria-label={placeholder}
autoComplete="off"
className="h-14 flex-1 bg-transparent outline-none"
/>
<button type="button" disabled={pending} onClick={() => submit()} className="px-4 text-sm uppercase tracking-wide">
{buttonLabel}
</button>
</div>
{suggestions.length ? (
<ul className="absolute inset-x-0 top-full z-10 bg-background shadow-lg" aria-busy={loading}>
{suggestions.map((suggestion) => (
<li key={suggestion.placeId}>
<button type="button" className="w-full px-4 py-3 text-left hover:bg-muted" onClick={() => select(suggestion)}>
{suggestion.address}
</button>
</li>
))}
</ul>
) : null}
</div>
)
}
// A unit typed in the input ("#4611", "Apt 4A") rides along: Google drops it from the predictions.

The CMA page: search on top, report under it, an optional lead form after it

MarketAnalysis
// app/home-market-analysis/page.tsx: the site's search on top, the CMA under it, an optional lead form after it
import { MarketAnalysis, PandaForm } from "@pandaidx/widgets/client"
import { fetchForm, fetchHomeValuation } from "@pandaidx/widgets/server"
import { AddressSearch } from "@/components/AddressSearch"
import { getText } from "@/lib/panda-text"
const config = { publicKey: process.env.NEXT_PUBLIC_PANDA_PUBLIC_KEY!, apiUrl: process.env.NEXT_PUBLIC_PANDA_API_URL }
export default async function Page({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {
// Null without ?placeId= / ?listingId=: only the search shows.
const [valuation, form] = await Promise.all([
fetchHomeValuation({ ...config, searchParams: await searchParams }),
fetchForm({ ...config, spot: "home-valuation" }),
])
return (
<main>
<section className="px-6 py-20">
<h1 className="text-center text-4xl">{await getText({ id: "cma-title", label: "CMA · Heading · Title", text: "What is your home worth?" })}</h1>
<AddressSearch placeholder={await getText({ id: "cma-placeholder", label: "CMA · Search · Placeholder", text: "Enter your address" })} buttonLabel={await getText({ id: "cma-button", label: "CMA · Search · Button", text: "Analyze" })} />
</section>
<MarketAnalysis valuation={valuation} design={{ report: { layout: "split" } }} />
{valuation && form ? (
<section className="mx-auto max-w-xl px-6 py-16">
<PandaForm form={form} source="home-valuation" contextData={{ address: valuation.address, estimate: valuation.valuation?.estimate?.value }} />
</section>
) : null}
</main>
)
}

A CMA section on the homepage: the search alone, the report on its own page

MarketAnalysis
<section className="bg-muted px-6 py-24">
<h2 className="mb-8 text-center text-3xl">{await getText({ id: "home-cma-title", label: "Home · CMA · Title", text: "See what homes like yours sold for" })}</h2>
<AddressSearch placeholder={await getText({ id: "home-cma-placeholder", label: "Home · CMA · Placeholder", text: "Enter your address" })} buttonLabel={await getText({ id: "home-cma-button", label: "Home · CMA · Button", text: "Analyze" })} />
</section>
// AddressSearch uses useAddressSearch({ href: "/home-market-analysis" }): picking an address opens the page with the report.

API reference

HomeValuation

The Panda website "How much is my home worth" flow (/how-much-is-my-home-worth), complete and designable (design: colors, background, shape, address bar, report layout; a CMA page with no lead form is MarketAnalysis, not this widget): an address pill with Google predictions (GET /v1/places/autocomplete, the key stays on the API), the comparable-sales "Panda Estimate" over the tenant's own MLS feeds (GET /v1/home-valuation: estimate + low/high range and confidence, market snapshot, $/sq ft trend and distribution, the home's sale history, comparable sales; a city or zip shows the area market) and the tenant's home-valuation lead form sent through POST /v1/forms/submit with the estimate as lead context (the CRM lead card shows it, like websites). Clicking a comparable values that home (Back returns); Share copies a link that reopens the estimate; "Get Another Estimate" restarts. gate places the form before the report (websites), after it, or turns it off; layout="split" is the websites full-height page with the aside photo (render it without the site header/footer), card / inline embed it in any section. Shared links (?listingId= / ?placeId=&unit=) open on the report through fetchHomeValuation. Never submit its form from automated tests: each submit creates a real contact.

Import from @pandaidx/widgets/client · renders inside PandaProvider

PropTypeDefaultDescription
formobject—Lead form from the server loader. Omit it to load formId / the home-valuation spot in the browser; null = no form assigned (the form step shows "No Form Available").
formIdstring—A form of the tenant by id instead of the home-valuation spot (browser load when form is omitted).
initialValuationobject—A shared estimate (?listingId= / ?placeId=&unit= of the page URL): the widget opens on the report, skipping the form (websites behavior). Null = start on the address search.
gate"before-results" | "after-results" | "none""before-results"before-results (websites): address → lead form → report (the valuation is computed first and sent with the lead). after-results: address → report with the form under it (its own success message). none: no form. A visitor who sent the form is not asked again for the next address. A signed-in contact (the <PandaProvider> session) never sees the form in any mode: the report shows right away and each estimate is logged to that contact automatically (nothing to pass).
layout"split" | "card" | "inline" | "banner""split"split: the websites page (full height; aside photo left and the steps right on desktop, the photo behind a white card on phones; × back to closeHref). card: the steps in a card. inline: the steps only (transparent, on your section). banner: a section with asideImage behind a color overlay (design.overlay), light text, and the report opens in a side sheet unless open says otherwise (a homepage call to action).
designobject—Look of the whole widget, the address bar and the report: every value optional, unset keeps the theme (DESIGN.md) and the websites look. Colors are any CSS color (oklch(...), #111, a DESIGN.md var(--ds-color-primary)); the text and placeholder colors are derived from the surface they sit on unless foreground is set. Finer control: the stable parts home-valuation.headline | search | search-input | search-button | estimate | panel | chart and their --panda-home-valuation-* variables (see the parts table).
asidebooleantruesplit only: show the photo column (the design is optional; off = the steps alone on a plain surface).
asideImagestring"https://media.pandaidx.com/platform/images/app/home-valuation-hero-cf4baecb.jpeg"Photo of the aside (next/image with sizes 50vw desktop / 100vw phones). Allow its host in next.config images.
asideImageAltstring"Lead page Hero Image"Alt text of the aside photo.
closeHrefstring"/"split only: href of the × link (null hides it).
tone"light" | "dark""light"Surface the widget sits on: dark = light text, dark card and a dark form (use it on dark sections).
labelsobject—Flow copy overrides (defaults: the websites copy / howMuchIsMyHomeWorth.* dictionary of the provider locale).
reportLabelsobject—Report copy overrides (estimateTitle, low, high, pricePerSf, basedOnComps with {{count}}, marketTitle, trendTitle, compsTitle, compsHint, historyTitle, unitRequiredTitle/Description, noRecordTitle/Description, notEnoughCompsTitle/Description, disclaimer…).
showMarketbooleantrueMarket snapshot (typical / lower / upper $/sq ft) and the trend + distribution charts.
showHistorybooleantrueThe home's own sale history.
showCompsbooleantrueComparable sales list.
showMapbooleanfalseMap of the comparable sales (price pills around the home's pin) above the comps list. Off in the websites flow.
mapHeightstring"288px"Height of the comps map (CSS length).
mapobject—Design of the comparable markers on the map (everything the widget lets you change; nothing else about the markers exists): { marker: "pill" (default, rounded price label) | "tag" (label with an arrow tip) | "dot" (round, with icon) | "pin" (teardrop), label: "price" (default) | "pricePerSqft" | "address" | "none" | (comp) => string | null, color, textColor, radius (CSS length: 0px square, 6px soft, default fully round), size (px), icon (inline SVG for a dot), subject: "pin" | "dot" | false | "pill" (a marker styled like the comparables with the estimate as text) | { marker, label, color, textColor, radius, size, icon } (the home's own marker; unset color, text color and radius follow the comparables') , frame: false (no border: a full-bleed map), onSelect: "analyze" (a tap values that home next, the default) | "popup" (opens renderPopup) | "none", renderPopup: (comp, { analyze }) => ReactNode (your own card over the marker: address, price, a button calling analyze(); client component code only; the popup has no surface of its own, so give your card a background, radius and shadow), zoom, scrollWheelZoom, showZoom }. Marker colors, corners, padding, text size, border and shadow also answer to the map.pin part variables (--panda-map-pin-bg/fg/radius/padding/font-size/border/shadow). A change not listed here (a different map engine, clustering rules, a custom pin image) cannot be done: say so to the owner, never report it as done.
compHopsbooleantrueA comparable (list row or map pill) values that home next; Back walks back.
showResultSearchbooleantrueAddress search above the report (another estimate without leaving the result).
showSharebooleantrueShare button beside the address (a link that reopens this estimate).
formDesignobject—Lead form design, the PandaForm props: fieldVariant, size, radius, labelStyle, gap, buttonVariant, buttonWidth, align, columns, submitLabel, classNames (form slots).
onValuationfunction—(data) => void with every valuation shown (address picked, comparable hop): { address, valuation, placeId?, unit? }.
onLeadfunction—(data) => void after the lead form is sent (analytics, a thank-you).
classNamesobject—Host classes appended to slots: root, aside, panel, card, headline, search, form, result.
openobject—Where the report opens: in place (default), { mode: "dialog" | "sheet", size, side } over the search, or { mode: "page", href: "/home-value" } (navigates with the shareable estimate query; that page renders HomeValuation with fetchHomeValuation({ searchParams }) so it opens on the report).
preset"default" | "template1"—Preset for this widget only (defaults to the provider preset).
variant"default" | "template1"—Deprecated alias of preset.
themeobject—Token overrides for this widget only, merged over the provider theme (same shape as PandaProvider.theme).

MarketAnalysis

A comparative market analysis (CMA) of one home, and nothing else: the Panda Estimate with its low/high range and confidence, the market snapshot and $/sq ft trend and distribution, the home's sale history, the comparable sales with a map of them around the home, and Share. No address bar, no lead form, no mode: the page composes the rest. The address search is the site's own, built with useAddressSearch (Google predictions from GET /v1/places/autocomplete; picking one navigates to this page with ?placeId=&unit=); the lead form, if the page wants one, is a regular PandaForm (spot home-valuation or any form) placed before or after the report. Load the home on the page with fetchHomeValuation({ searchParams }) and pass it as valuation (null with no address in the URL: the widget renders nothing and the search shows alone). Clicking a comparable analyzes that home (Back returns). A signed-in contact has each analysis logged to them in Panda. Designable like the rest of HomeValuation (design: background, foreground, surface, border, accent, shape, width, report layout; the stable parts home-valuation.estimate | panel | chart). Never call /v1/places/autocomplete or /v1/home-valuation yourself.

Import from @pandaidx/widgets/client · renders inside PandaProvider

PropTypeDefaultDescription
valuationobject—The home to analyze (?listingId= / ?placeId=&unit= of the page URL). Null / omitted: nothing renders.
designobject—Look of the report: every value optional, unset keeps the theme (DESIGN.md). Colors are any CSS color (oklch(...), #111, a DESIGN.md var(--ds-color-primary)); text colors are derived from the surface unless foreground is set.
tone"light" | "dark""light"Surface the widget sits on: dark = light text and a dark report (use it on dark sections).
reportLabelsobject—Report copy overrides (estimateTitle, low, high, pricePerSf, basedOnComps with {{count}}, marketTitle, trendTitle, compsTitle, compsHint, historyTitle, unitRequiredTitle/Description, noRecordTitle/Description, notEnoughCompsTitle/Description, disclaimer…). Pass await getText({ id, label, text }) for each.
showMarketbooleantrueMarket snapshot (typical / lower / upper $/sq ft) and the trend + distribution charts.
showHistorybooleantrueThe home's own sale history.
showCompsbooleantrueComparable sales list.
showMapbooleantrueMap of the comparable sales (price pills around the home's pin) above the comps list.
mapHeightstring"288px"Height of the comps map (CSS length).
mapobject—Design of the comparable markers on the map (everything the widget lets you change; nothing else about the markers exists): { marker: "pill" (default, rounded price label) | "tag" (label with an arrow tip) | "dot" (round, with icon) | "pin" (teardrop), label: "price" (default) | "pricePerSqft" | "address" | "none" | (comp) => string | null, color, textColor, radius (CSS length: 0px square, 6px soft, default fully round), size (px), icon (inline SVG for a dot), subject: "pin" | "dot" | false | "pill" (a marker styled like the comparables with the estimate as text) | { marker, label, color, textColor, radius, size, icon } (the home's own marker; unset color, text color and radius follow the comparables') , frame: false (no border: a full-bleed map), onSelect: "analyze" (a tap values that home next, the default) | "popup" (opens renderPopup) | "none", renderPopup: (comp, { analyze }) => ReactNode (your own card over the marker: address, price, a button calling analyze(); client component code only; the popup has no surface of its own, so give your card a background, radius and shadow), zoom, scrollWheelZoom, showZoom }. Marker colors, corners, padding, text size, border and shadow also answer to the map.pin part variables (--panda-map-pin-bg/fg/radius/padding/font-size/border/shadow). A change not listed here (a different map engine, clustering rules, a custom pin image) cannot be done: say so to the owner, never report it as done.
compHopsbooleantrueA comparable (list row or map pill) analyzes that home next; Back walks back.
showSharebooleantrueShare button beside the address (a link that reopens this analysis).
onValuationfunction—(data) => void with every analysis shown (the page's home, a comparable hop): { address, valuation, placeId?, unit? }.
classNamesobject—Host classes appended to slots: root, result.
preset"default" | "template1"—Preset for this widget only (defaults to the provider preset).
variant"default" | "template1"—Deprecated alias of preset.
themeobject—Token overrides for this widget only, merged over the provider theme (same shape as PandaProvider.theme).

Parts

<ValuationSearch />

Step 1: headline + address pill (Google predictions); picking an address runs the flow.

<ValuationLeadForm />

The lead form step that gates the report (gate="before-results"), with Change address; renders only on that step.

<ValuationReport />

The report of the address on screen (estimate, market, history, comparables, share, the after-results form). Nothing before an address is valued. With children: your own report from the report blocks (the disclaimer and the after-results form are appended); again={false} hides Get another estimate and Back (a page with no address search).

<ValuationSteps />

The step on screen (search → form → report) in one part; with the root open dialog / sheet the report opens over the search.

<ValuationSearchInput />

Only the address pill (no headline), for your own hero; picking an address runs the flow.

<ValuationReportHeader />

Report block (inside <ValuationReport> children): address, Back, home facts, Share.

<ValuationEstimateCard />

Report block: estimate value, confidence badge, $/sq ft, comps used and the low–high range (nothing without an estimate).

<ValuationNoEstimate />

Report block: why there is no estimate (unit required, no record, not enough comps).

<ValuationMarketSection />

Report block: market tiles + trend and distribution charts side by side.

<ValuationMarketStats />

Report block: market label, sales count and the lower / typical / upper $/sq ft tiles.

<ValuationTrendChart />

Report block: median $/sq ft by quarter with the home's line.

<ValuationHistogram />

Report block: $/sq ft distribution with the home's bar.

<ValuationSaleHistory />

Report block: the home's past sales.

<ValuationCompsMap />

Report block: comparable sales as markers on a map (height; price pills by default). Design them with marker (pill | tag | dot | pin), label (price | pricePerSqft | address | none | a function), color, textColor, radius, size, icon, subject (pin | dot | pill | a marker object, so the home can look like the comparables), onSelect (analyze | popup | none), renderPopup(comp, { analyze }), zoom, scrollWheelZoom, showZoom, frame; or map on the root for every map of the report.

<ValuationComps />

Report block: comparable sales list (each one values that home next).

Hooks

useHomeValuation()inside HomeValuationRoot

{ step: "search" | "form" | "result", data, pick(data), reset(), goBack(), selectComp(comp), shareUrl, busy, labels, reportLabels, gate } — import from @pandaidx/widgets/client

useValuationReport()inside HomeValuationRoot

null outside <ValuationReport> / before a result, else { valuation: { address, kind, subject { bedrooms, bathrooms, livingArea, yearBuilt, propertySubType, saleHistory[] }, estimate { value, low, high, pricePerSf, confidence, compsUsed } | null, market { label, sales, months, medianPricePerSf, p25PricePerSf, p75PricePerSf, trend[], histogram[] } | null, comps[] , reason }, labels, formatCurrency(n), formatNumber(n), formatDate(iso), onCompSelect, emptyState, subjectFacts } — inside <ValuationReport> children: draw any report block your own way

useAddressSearch()inside MarketAnalysis

useAddressSearch({ href, minLength?: 3, debounce?: 200 }) → { query, setQuery(value), suggestions: { placeId, address }[] (up to 5), loading, available, pending, select(suggestion) (navigates to href?placeId=&unit= and returns the URL; a unit typed in query rides along), submit() (opens the first suggestion: Enter) } — the site's own address input and suggestions list over Google predictions (the key stays on the API); href is the route of the page that renders <MarketAnalysis>. Client component only; never fetch /v1/places/autocomplete yourself.

Server data

Loaders run in a Server Component and come from @pandaidx/widgets/server. Typed data is for a page you design yourself, rendered inside the root.

fetchHomeValuation()widget loader

The estimate of a shared link (GET /v1/home-valuation?listingId= or ?placeId=&unit=) from the page's searchParams; null when the URL has neither or it fails (never throws). Load the form with fetchForm({ spot: "home-valuation" }). Returns HomeValuationData | null → initialValuation

getHomeValuation()typed data

Typed estimate of an MLS listing (listingId) or a Google place (placeId + unit), or of a shared link (searchParams). Render it inside <HomeValuationRoot initialValuation={data}> with <ValuationReport again={false}> blocks or useValuationReport(). initialValuation opens straight on the report (no gate before it): use gate="after-results" to keep the lead form under the report. Never call /v1/home-valuation yourself.