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,…
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 widgetThe 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
- 1npm install <tarball or @pandaidx/widgets>Terminalnpm install https://github.com/pandaidx/panda-widgets/releases/download/v1.14.1/pandaidx-widgets-1.14.1.tgz
- 2import "@pandaidx/widgets/styles.css" in app/layout.tsx
- 3Wrap the site in <PandaProvider publicKey apiUrl preset theme> (app/layout.tsx)
- 4In each page (Server Component) call the widget's server loader, then render the widget with its data
- 5Allow the images: next.config images.remotePatterns [{ protocol: "https", hostname: "**" }] (listing and blog photos come from several CDNs)
Usage
// app/(leads)/how-much-is-my-home-worth/page.tsx (no site header/footer): the websites pageimport { 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
<HomeValuationlayout="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
<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
<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)
<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)
"use client"import { HomeValuationRoot, ValuationReport, ValuationCompsMap, ValuationEstimateCard, ValuationMarketStats, ValuationTrendChart, ValuationComps, useValuationReport } from "@pandaidx/widgets/client"const Range = () => {const report = useValuationReport()const estimate = report?.valuation.estimateif (!report || !estimate) return nullconst { formatCurrency } = reportreturn (<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
"use client"import { useValuationReport } from "@pandaidx/widgets/client"export const BigNumber = () => {const report = useValuationReport()if (!report?.valuation.estimate) return nullconst { valuation, formatCurrency } = reportreturn <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)
"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"><inputvalue={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
// app/home-market-analysis/page.tsx: the site's search on top, the CMA under it, an optional lead form after itimport { 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
<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
| Prop | Type | Default | Description |
|---|---|---|---|
| form | object | — | 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"). |
| formId | string | — | A form of the tenant by id instead of the home-valuation spot (browser load when form is omitted). |
| initialValuation | object | — | 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). |
| design | object | — | 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). |
| aside | boolean | true | split only: show the photo column (the design is optional; off = the steps alone on a plain surface). |
| asideImage | string | "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. |
| asideImageAlt | string | "Lead page Hero Image" | Alt text of the aside photo. |
| closeHref | string | "/" | 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). |
| labels | object | — | Flow copy overrides (defaults: the websites copy / howMuchIsMyHomeWorth.* dictionary of the provider locale). |
| reportLabels | object | — | Report copy overrides (estimateTitle, low, high, pricePerSf, basedOnComps with {{count}}, marketTitle, trendTitle, compsTitle, compsHint, historyTitle, unitRequiredTitle/Description, noRecordTitle/Description, notEnoughCompsTitle/Description, disclaimer…). |
| showMarket | boolean | true | Market snapshot (typical / lower / upper $/sq ft) and the trend + distribution charts. |
| showHistory | boolean | true | The home's own sale history. |
| showComps | boolean | true | Comparable sales list. |
| showMap | boolean | false | Map of the comparable sales (price pills around the home's pin) above the comps list. Off in the websites flow. |
| mapHeight | string | "288px" | Height of the comps map (CSS length). |
| map | object | — | 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. |
| compHops | boolean | true | A comparable (list row or map pill) values that home next; Back walks back. |
| showResultSearch | boolean | true | Address search above the report (another estimate without leaving the result). |
| showShare | boolean | true | Share button beside the address (a link that reopens this estimate). |
| formDesign | object | — | Lead form design, the PandaForm props: fieldVariant, size, radius, labelStyle, gap, buttonVariant, buttonWidth, align, columns, submitLabel, classNames (form slots). |
| onValuation | function | — | (data) => void with every valuation shown (address picked, comparable hop): { address, valuation, placeId?, unit? }. |
| onLead | function | — | (data) => void after the lead form is sent (analytics, a thank-you). |
| classNames | object | — | Host classes appended to slots: root, aside, panel, card, headline, search, form, result. |
| open | object | — | 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. |
| theme | object | — | 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
| Prop | Type | Default | Description |
|---|---|---|---|
| valuation | object | — | The home to analyze (?listingId= / ?placeId=&unit= of the page URL). Null / omitted: nothing renders. |
| design | object | — | 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). |
| reportLabels | object | — | 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. |
| showMarket | boolean | true | Market snapshot (typical / lower / upper $/sq ft) and the trend + distribution charts. |
| showHistory | boolean | true | The home's own sale history. |
| showComps | boolean | true | Comparable sales list. |
| showMap | boolean | true | Map of the comparable sales (price pills around the home's pin) above the comps list. |
| mapHeight | string | "288px" | Height of the comps map (CSS length). |
| map | object | — | 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. |
| compHops | boolean | true | A comparable (list row or map pill) analyzes that home next; Back walks back. |
| showShare | boolean | true | Share button beside the address (a link that reopens this analysis). |
| onValuation | function | — | (data) => void with every analysis shown (the page's home, a comparable hop): { address, valuation, placeId?, unit? }. |
| classNames | object | — | 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. |
| theme | object | — | 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 HomeValuationRootnull 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 MarketAnalysisuseAddressSearch({ 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 loaderThe 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 dataTyped 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.