feat: settings section-nav + selection-control unification (#250) and Google Places address autocomplete + property auto-fill (#198, #200)#259
Merged
important-new merged 12 commits intoJul 20, 2026
Conversation
…ectorHub#250) - SectionNav wired into settings-communication with 6 anchored sections (conditional email-secrets / managed-compliance drop out of the nav). - Fix shared app shell: remove overflow-y-auto from auth-layout <main>. It grew to content height and never scrolled, so it was a non-scrolling scroll-container that broke position:sticky (bar scrolled away) and stopped the scroll-spy listener from firing. The window is the real scroller. - useScrollSpy listens on window with capture:true so it fires regardless of which element scrolls (scroll events don't bubble). - E2E: renders, click-jump + scroll-spy highlight, sticky-pinned.
Wire SectionNav into workspace (7), security (7), schedule (6), booking (5), advanced (4), profile (4). Each section gets a stable id + scroll-mt-12 anchor; labels reuse existing panel/section heading messages. Conditional sections (schedule availability-heatmap) carry a visible flag so they drop out of the nav when absent. Billing deferred — its sections are small/conditional and rarely reach the >=3 threshold that shows the bar.
- SMS delivery mode: hand-rolled radio-cards -> RadioCardGroup (hidden smsMode input still carries the value into the form POST). - Email delivery mode: hand-rolled solid-purple segmented toggle -> DS SegmentedControl + hidden input (buttons don't post, so the hidden input carries emailMode). - Point of contact: bare native radios -> RadioCardGroup (real named radios post natively). Controlled value initialises from config, preserving behavior.
…ives - Email/SMS secrets provider pickers -> RadioCardGroup (existing hidden provider inputs stay authoritative; DS radios named _*Radio to avoid POST collision). - Google Calendar OAuth mode -> SegmentedControl (controlled-only; existing hidden googleOAuthMode input carries the value). - Calendar capability picker -> RadioCardGroup (URL-query driven, no form). - Inspection prefs (clone / auto-advance / required fields) -> RadioGroup. Left intentionally as-is: ManagedComplianceWizard (button+aria-pressed test contract), HolidayPolicyCards (aria-checked/testid test contract), workspace report-theme swatch grid — all already DS-token-compliant.
…ing (InspectorHub#198) Frontend foundation for Google Places address autocomplete over the existing /api/places/* backend: - resources/places: token-relay BFF loader (q -> suggestions, placeId -> structured AddressSelection); degrades to empty when the upstream key is unset so the address input stays usable as free text. - AddressAutocomplete: debounced, keyboard-navigable combobox with a per-typing-session Google billing token. - GoogleMap: interactive marker map, fail-closed when the browser Maps key or coordinates are absent; key surfaced via the root loader (useRouteLoaderData) rather than a new global. - AppEnv gains GOOGLE_MAPS_JS_API_KEY; CSP allows maps.googleapis.com / maps.gstatic.com (documented CDN exception). - @googlemaps/js-api-loader dependency (SDK loaded from Google's CDN). Co-located tests for the loader, autocomplete flow, and map fail-closed contract. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkL3cCs9TmQd7zueiAnJ8s
… wizard (InspectorHub#198) The wizard's Property step now uses AddressAutocomplete + GoogleMap. Picking a suggestion captures a structured, geocoded AddressSelection; editing the text by hand clears it so stale coordinates are never persisted. handleSubmit forwards the structured fields, and buildCreateInspectionJson maps them into the create payload (the server already accepts them and stamps addressGeocodedAt). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkL3cCs9TmQd7zueiAnJ8s
…nspectorHub#200) PropertyInfoForm gains a public-records lookup: the button relays the address to the Estated proxy via the editor BFF action (autofill-property-facts intent), then fills ONLY the property-fact fields the inspector hasn't set — a lookup never clobbers manual work — and commits the merged snapshot through the existing save-property-facts path. Surfaces which fields were filled, and degrades with a clear message when the provider key is unset or the address isn't found. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkL3cCs9TmQd7zueiAnJ8s
…/degradation (InspectorHub#198/InspectorHub#200) Real-browser Playwright coverage: the wizard renders the address combobox and accepts free text with no suggestion dropdown when GOOGLE_PLACES_API_KEY is unset; the editor 'Fetch property details' button relays the lookup and surfaces the not-configured notice when ESTATED_API_KEY is unset. Verifies mount + hydration + graceful degradation (the self-hoster default). Happy-path lookup logic is covered by the co-located unit tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkL3cCs9TmQd7zueiAnJ8s
…outer (InspectorHub#200) The InspectorHub#200 useFetcher addition made PropertyInfoForm require a data-router context, breaking the pre-existing property-info-form.test.tsx (which rendered it bare). Wrap all renders in a createRoutesStub router, fold the autofill-button tests into the same file, and drop the temporary duplicate PropertyInfoForm.test.tsx. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkL3cCs9TmQd7zueiAnJ8s
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two independent feature areas, landed together on one branch.
1. Settings section-nav + selection-control unification (closes #250)
workspace, security, schedule, booking, advanced, profile). A new
SectionNavcomponent renders aTabStripin a pinned container driven by anew
useScrollSpyhook; sections are data-driven and drop out of the navautomatically when absent, so the bar only appears when there are ≥3 visible
sections. Fixes an app-shell bug where the auth layout's
<main>had avestigial
overflow-y-autothat made it a non-scrolling scroll-container,breaking
position: stickyand scroll listeners app-wide.RadioCardGroup— a new shared-ui card-style single-select primitive(
<fieldset role="radiogroup">, native radios, roving arrow-key focus)."pick one" controls (a purple segmented pill, ad-hoc radio cards, and bare
native radios) and several other panels now use the design-system primitives
(
SegmentedControl/RadioCardGroup/RadioGroup) uniformly.2. Google Places address autocomplete + property auto-fill (closes #198, #200)
Frontend wiring over the existing
/api/places/*and property-facts autofillbackends:
resources/placesBFF — a token-relay resource route proxyingautocomplete/details (the browser never calls
/api/places/*directly).AddressAutocomplete— a debounced, keyboard-navigable combobox with aper-typing-session Google billing token.
GoogleMap— an interactive marker map that fails closed (rendersnothing) when the browser Maps key or coordinates are absent. The key is
surfaced via the root loader (
useRouteLoaderData("root")). Loads the MapsJS SDK from Google's CDN (a documented CSP exception).
autocomplete + map; picking a suggestion captures a structured, geocoded
address that persists through the create path (editing the text by hand
clears it so stale coordinates are never saved).
public-records property facts and fills only the fields the inspector
hasn't set (never clobbers manual entry).
Everything degrades gracefully when
GOOGLE_PLACES_API_KEY/GOOGLE_MAPS_JS_API_KEY/ESTATED_API_KEYare unset — the address input staysusable as free text, the map is hidden, and the autofill button shows a clear
"not configured" notice. That self-hoster default path is covered by a
real-browser E2E; the happy-path lookup logic is covered by co-located unit
tests.
Note on #124
The self-hoster release contract (#124) is already satisfied on
main(release-please workflow,
CHANGELOG1.0.0,/statussemver, upgrade guide,deprecation policy — shipped via #237; the live release-please Release PR
confirms it). No changes here; #124 can be closed.
Testing
🤖 Generated with Claude Code