Skip to content

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 into
InspectorHub:mainfrom
important-new:feat/settings-nav-selection-controls
Jul 20, 2026
Merged

feat: settings section-nav + selection-control unification (#250) and Google Places address autocomplete + property auto-fill (#198, #200)#259
important-new merged 12 commits into
InspectorHub:mainfrom
important-new:feat/settings-nav-selection-controls

Conversation

@important-new

Copy link
Copy Markdown
Contributor

Summary

Two independent feature areas, landed together on one branch.

1. Settings section-nav + selection-control unification (closes #250)

  • Sticky scroll-spy section nav on the long settings pages (communication,
    workspace, security, schedule, booking, advanced, profile). A new
    SectionNav component renders a TabStrip in a pinned container driven by a
    new useScrollSpy hook; sections are data-driven and drop out of the nav
    automatically 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 a
    vestigial overflow-y-auto that made it a non-scrolling scroll-container,
    breaking position: sticky and scroll listeners app-wide.
  • RadioCardGroup — a new shared-ui card-style single-select primitive
    (<fieldset role="radiogroup">, native radios, roving arrow-key focus).
  • Selection-control convergence — the communication page's inconsistent
    "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 autofill
backends:

  • resources/places BFF — a token-relay resource route proxying
    autocomplete/details (the browser never calls /api/places/* directly).
  • AddressAutocomplete — a debounced, keyboard-navigable combobox with a
    per-typing-session Google billing token.
  • GoogleMap — an interactive marker map that fails closed (renders
    nothing) when the browser Maps key or coordinates are absent. The key is
    surfaced via the root loader (useRouteLoaderData("root")). Loads the Maps
    JS SDK from Google's CDN (a documented CSP exception).
  • Wizard wiring ([Feature]: Address autocomplete and precise location display on map #198) — the new-inspection Property step uses the
    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).
  • "Fetch property details" ([Feature]: One-Click Property Information Retrieval (Auto-fill) #200) — an editor button that looks up
    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_KEY are unset — the address input stays
usable 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, CHANGELOG 1.0.0, /status semver, upgrade guide,
deprecation policy — shipped via #237; the live release-please Release PR
confirms it). No changes here; #124 can be closed.

Testing

  • Full type-check, full lint (incl. tenant-scope / knip / i18n / naming gates)
  • 3482 API unit tests, 1413 web unit tests, worker-runtime tests
  • Production build (bundle-size gate)
  • Playwright E2E for the address-autocomplete + autofill wiring and degradation

🤖 Generated with Claude Code

important-new and others added 12 commits July 20, 2026 16:01
…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
@important-new
important-new merged commit 63bb997 into InspectorHub:main Jul 20, 2026
5 checks passed
@important-new
important-new deleted the feat/settings-nav-selection-controls branch July 20, 2026 13:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant