# @roofmaxx/form

> This document covers both **how to install and configure** the form on any website and **how the form works internally** in full technical detail.

Latest version **0.115.0** (published 2026-09-17) · MIT license · 0 weekly downloads

## Install

```sh
npm install @roofmaxx/form
pnpm add @roofmaxx/form
yarn add @roofmaxx/form
bun add @roofmaxx/form
```

## Health

**Score 55/100 (C)** — status: active.

Positive: no vulnerabilities; recently updated; high maintenance score.

Warnings: low downloads; no types; no esm support; pre 1.0.

## Facts

| | |
|---|---|
| Version | 0.115.0 |
| Published | 2026-09-17 |
| First published | 2022-02-10 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | none |
| Module format | CommonJS |
| Dependencies | 14 |
| Unpacked size | 572.9 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Maintainers | gkarakitsios |

## Links

- npm: https://www.npmjs.com/package/@roofmaxx/form
- npm.io page: https://npm.io/package/@roofmaxx/form

## Dependencies (14)

- [ky](https://npm.io/package/ky.md) ^0.28.6
- [react](https://npm.io/package/react.md) ^17.0.0
- [react-dom](https://npm.io/package/react-dom.md) ^17.0.0
- [react-use](https://npm.io/package/react-use.md) ^17.3.1
- [react-imask](https://npm.io/package/react-imask.md) ^6.2.2
- [react-popper](https://npm.io/package/react-popper.md) ^2.2.5
- [@popperjs/core](https://npm.io/package/@popperjs/core.md) ^2.11.2
- [@roofmaxx/types](https://npm.io/package/@roofmaxx/types.md) *
- [@roofmaxx/utils](https://npm.io/package/@roofmaxx/utils.md) *
- [react-hook-form](https://npm.io/package/react-hook-form.md) ^7.17.4
- [@headlessui/react](https://npm.io/package/@headlessui/react.md) ^1.4.1
- [@roofmaxx/tsconfig](https://npm.io/package/@roofmaxx/tsconfig.md) *
- [@tailwindcss/forms](https://npm.io/package/@tailwindcss/forms.md) ^0.4.0
- [@roofmaxx/components](https://npm.io/package/@roofmaxx/components.md) *

## Recent versions

- 0.115.0 (latest) — 2026-09-17
- 0.114.0 — 2026-09-14
- 0.113.0 — 2026-09-14
- 0.112.0 — 2026-09-14
- 0.111.0 — 2026-09-14
- 0.110.0 — 2026-09-13
- 0.109.0 — 2026-09-13
- 0.108.0 — 2026-08-21
- 0.107.0 — 2026-08-13
- 0.106.0 — 2026-08-13
- 0.105.0 — 2026-07-25
- 0.104.0 — 2026-06-23
- 0.103.0 — 2026-06-10
- 0.102.0 — 2026-05-22
- 0.101.0 — 2026-05-18
- … 100 more at https://npm.io/package/@roofmaxx/form/versions

## README

# Roof Maxx ReactJs Form

This document covers both **how to install and configure** the form on any website and **how the form works internally** in full technical detail.

The form is a multistep lead-capture widget built with **ReactJS + Tailwind CSS**, integrated with three external services:

- **HubSpot Forms API** — lead submission and CRM
- **Roof Maxx Connect `map_status` API** — real-time dealer lookup by location
- **Google Maps Places API** — address autocomplete with city/state/ZIP extraction
- **Zippopotam.us API** — ZIP code existence validation

---

## Table of Contents

1. [Installation](#installation)
2. [Usage & Configuration](#usage--configuration)
3. [UTM Priority Logic](#utm-priority-logic)
4. [Examples of Use](#examples-of-use)
5. [How the Form Works — Step by Step](#how-the-form-works--step-by-step)
6. [Conditional Field Logic](#conditional-field-logic)
7. [State & Data Persistence](#state--data-persistence)
8. [Dealer Finder Logic](#dealer-finder-logic)
9. [HubSpot Submission](#hubspot-submission)
10. [Redirect Logic](#redirect-logic)
11. [Google Maps Integration](#google-maps-integration)
12. [Component Architecture](#component-architecture)
13. [Styling & Theming](#styling--theming)
14. [Build & Development](#build--development)
15. [Third-Party Services Reference](#third-party-services-reference)

---

## Installation

### **-> STEP 1** — Add the script tag to your page

#### Latest Files

Copy and paste the codes below preferably in the `<head>` of your page:

```html
<script type="module" src="https://unpkg.com/@roofmaxx/form/dist/form.js"></script>
```

> If you are facing issues with the form try inserting the JS script (not the CSS) in the **footer** of your page OR add the **defer** value _(defer means the script executes after the document has been parsed)_

```html
<script defer type="module" src="https://unpkg.com/@roofmaxx/form/dist/form.js"></script>
```

```html
<link rel="stylesheet" href="https://unpkg.com/@roofmaxx/form/dist/form.css" />
```

#### Specific Version Files

To pin a specific version, specify it in both the script and link tags:

```html
<script type="module" src="https://unpkg.com/@roofmaxx/form@0.3.0/dist/form.js"></script>
```

```html
<link rel="stylesheet" href="https://unpkg.com/@roofmaxx/form@0.3.0/dist/form.css" />
```

### **-> STEP 2** — Register your domain

Please send an email to <a href="mailto:Techsupport@roofmaxx.com">techsupport@roofmaxx.com</a> with your base domain where the form will be installed. For example if you have the form installed at **www.domain.com/test** or **www.test.domain.com** please send us **domain.com**.

---

## Usage & Configuration

Place the following HTML at the location on your page where you want the form to appear:

<!-- prettier-ignore -->
```html
<div id="roofmaxx-form"
    data-utm-source=""
    data-utm-campaign=""
    data-utm-medium=""
    data-utm-content=""
    data-utm-term=""
    data-page-variant=""
    data-zip-code=""
    data-dealer-id=""
    data-source-vendor=""
    data-deal-type="" 
    data-self-generated=""
    data-lead-type=""
    data-microsite-type=""
    data-thank-you-page=""
    data-sorry-page=""
    data-video-form=""
></div>
```

All configuration is passed via `data-*` attributes on the wrapper element. No JavaScript configuration is required.

### Data Attributes Reference

| Attribute | Purpose | Default |
|-----------|---------|---------|
| `data-dealer-id` | Force a specific dealer ID (skips the API location lookup) | — |
| `data-source-vendor` | Name of the marketing vendor (e.g. `"XYZ"`) | — |
| `data-thank-you-page` | Full URL to redirect to after a successful submission | `https://roofmaxx.com/thank-you` |
| `data-sorry-page` | Full URL to redirect to when no dealer is nearby (out-of-area). If empty, the thank-you page is used in all cases | — |
| `data-video-form` | Set to `"false"` to hide the Wistia intro video. **Video is shown by default.** | `"true"` |
| `data-zip-code` | Pre-fill the ZIP code field in Step 2 | — |
| `data-deal-type` | Deal type code forwarded to the dealer API and HubSpot (e.g. `"RMCL-F"`, `"MICRO"`) | — |
| `data-lead-type` | Lead type code forwarded to HubSpot | `"C"` |
| `data-program-type` | Program type code forwarded to HubSpot | — |
| `data-self-generated` | Self-generated lead flag (e.g. `"SG"` for dealer sites) | — |
| `data-microsite-type` | Microsite type forwarded to HubSpot | `"Non-Microsite"` |
| `data-page-variant` | Page variant code forwarded to HubSpot | — |
| `data-utm-source` | UTM source (see UTM priority logic below) | — |
| `data-utm-campaign` | UTM campaign | — |
| `data-utm-medium` | UTM medium | — |
| `data-utm-content` | UTM content | — |
| `data-utm-term` | UTM term | — |

---

## UTM Priority Logic

The form applies the following priority order when collecting UTM parameters:

1. **Both URL and data-attributes present** → data-attributes win
2. **Only URL** → URL UTMs are used
3. **Only data-attributes** → data-attributes are used
4. **Neither** → empty values are sent

---

## Examples of Use

---

### Internal Use — Roof Maxx Corporate

#### For roofmaxx.com (Website)

In all cases (dealer found or not) the user is redirected to **roofmaxx.com/thank-you**.

<!-- prettier-ignore -->
```html
<div id="roofmaxx-form"
    data-utm-source=""
    data-utm-campaign=""
    data-utm-medium=""
    data-utm-content=""
    data-utm-term=""
    data-page-variant=""
    data-zip-code=""
    data-dealer-id=""
    data-source-vendor=""
    data-deal-type="" 
    data-self-generated=""
    data-lead-type=""
    data-microsite-type=""
    data-thank-you-page=""
    data-sorry-page=""
    data-video-form=""
></div>
```

#### For roofmaxx.com/find-a-dealer/state OR roofmaxx.com/find-a-dealer/state/microsite

In all cases the user is redirected to **roofmaxx.com/find-a-dealer/thank-you**.

**For the find-a-dealer / state-location page:**

```html
<div
    id="roofmaxx-form"
    data-utm-source=""
    data-utm-campaign=""
    data-utm-medium=""
    data-utm-content=""
    data-utm-term=""
    data-page-variant=""
    data-zip-code=""
    data-dealer-id=""
    data-source-vendor=""
    data-deal-type="RMCL-F"
    data-self-generated=""
    data-lead-type=""
    data-microsite-type=""
    data-thank-you-page=""
    data-sorry-page=""
    data-video-form=""
></div>
<script>
    /* Add dealer/company ID to HS form */
    if (typeof roofmaxx_company_id !== "undefined" && roofmaxx_company_id) {
        $("#roofmaxx-form").attr("data-dealer-id", roofmaxx_company_id);
    }
</script>
```

**For Microsites:**

```html
<div
    id="roofmaxx-form"
    data-utm-source=""
    data-utm-campaign=""
    data-utm-medium=""
    data-utm-content=""
    data-utm-term=""
    data-page-variant=""
    data-zip-code=""
    data-dealer-id=""
    data-source-vendor=""
    data-deal-type="MICRO"
    data-self-generated=""
    data-lead-type=""
    data-microsite-type=""
    data-thank-you-page=""
    data-sorry-page=""
    data-video-form=""
></div>
<script>
    /* Add dealer/company ID to HS form */
    if (typeof roofmaxx_company_id !== "undefined" && roofmaxx_company_id) {
        $("#roofmaxx-form").attr("data-dealer-id", roofmaxx_company_id);
    }
</script>
```

---

### Internal Use — HubSpot Landing Pages

**1. Corporate landing page — same redirect regardless of dealer:**

<!-- prettier-ignore -->
```html
<div id="roofmaxx-form"
    data-utm-source=""
    data-utm-campaign=""
    data-utm-medium=""
    data-utm-content=""
    data-utm-term=""
    data-page-variant="" 
    data-zip-code=""
    data-dealer-id=""
    data-source-vendor=""
    data-deal-type="" 
    data-self-generated=""
    data-lead-type=""
    data-microsite-type=""
    data-thank-you-page="https://exampleHubspot.com/thank-you"
    data-sorry-page=""
    data-video-form=""
></div>
```

**2. Corporate landing page — different pages for dealer found vs. not found:**

<!-- prettier-ignore -->
```html
<div id="roofmaxx-form"
    data-utm-source=""
    data-utm-campaign=""
    data-utm-medium=""
    data-utm-content=""
    data-utm-term=""
    data-page-variant="" 
    data-zip-code=""
    data-dealer-id=""
    data-source-vendor=""
    data-deal-type="" 
    data-self-generated=""
    data-lead-type=""
    data-microsite-type=""
    data-thank-you-page="https://exampleHubspotYES.com/thank-you"
    data-sorry-page="https://exampleHubspotNO.com/thank-you"
    data-video-form=""
></div>
```

**3. Vendor (XYZ) landing page — same redirect regardless of dealer:**

<!-- prettier-ignore -->
```html
<div id="roofmaxx-form"
    data-utm-source=""
    data-utm-campaign=""
    data-utm-medium=""
    data-utm-content=""
    data-utm-term=""
    data-page-variant="" 
    data-zip-code=""
    data-dealer-id=""
    data-source-vendor="XYZ"
    data-deal-type="" 
    data-self-generated=""
    data-lead-type=""
    data-microsite-type=""
    data-thank-you-page="https://exampleHubspot.com/thank-you"
    data-sorry-page=""
    data-video-form=""
></div>
```

**4. Vendor (XYZ) landing page — different pages for dealer found vs. not found:**

<!-- prettier-ignore -->
```html
<div id="roofmaxx-form"
    data-utm-source=""
    data-utm-campaign=""
    data-utm-medium=""
    data-utm-content=""
    data-utm-term=""
    data-page-variant="" 
    data-zip-code=""
    data-dealer-id=""
    data-source-vendor="XYZ"
    data-deal-type="" 
    data-self-generated=""
    data-lead-type=""
    data-microsite-type=""
    data-thank-you-page="https://exampleHubspotYES.com/thank-you"
    data-sorry-page="https://exampleHubspotNO.com/thank-you"
    data-video-form=""
></div>
```

---

### Dealer Use

Placing the form on a dealer's own website or landing page with a custom thank-you page.

> It is recommended to always supply a custom `data-thank-you-page`. Without one the user is redirected to **roofmaxx.com/thank-you** and conversion tracking on the dealer's own site will not work correctly.

- In case you use UTM parameters and you don't want to use the UTMs in the URL, insert them as data-attributes. These hardcoded UTM parameters are only sent when the form is submitted.
- Dealer ID example: `2452308196`

<!-- prettier-ignore -->
```html
<div id="roofmaxx-form"
    data-utm-source=""
    data-utm-campaign=""
    data-utm-medium=""
    data-utm-content=""
    data-utm-term=""
    data-page-variant="" 
    data-zip-code=""
    data-dealer-id="2452308196"
    data-source-vendor=""
    data-deal-type="" 
    data-self-generated="SG"
    data-lead-type=""
    data-microsite-type=""
    data-thank-you-page="https://dealers.com/thank-you"
    data-sorry-page=""
    data-video-form=""
></div>
```

---

## How the Form Works — Step by Step

The form is a React application that mounts inside `#roofmaxx-form` and guides the user through three steps before submitting a lead to HubSpot.

### Step 1 — Roof Information

Collects details about the property and roof condition.

**Question 1 — Property Type** *(always shown, required)*

| Option | Internal Value |
|--------|---------------|
| Single-Family Home | `SingleFamilyHome` |
| Condominium / Apartment | `MultiFamily` |
| Commercial | `Commercial` |

**Question 2 — Manage Multiple Properties?** *(shown only when `MultiFamily` or `Commercial` is selected)*

| Option | Internal Value |
|--------|---------------|
| Yes | `Yes` |
| No | `No` |

**Question 3 — Property Unit Count** *(shown only when Question 2 is `Yes`)*

| Option | Internal Value |
|--------|---------------|
| 1–10 units | `1To10` |
| 11–50 units | `11To50` |
| 51+ units | `51Plus` |
| Not Sure | `Unknown` |

**Question 4 — Asphalt Shingle Roof?** *(always shown, required)*

| Option | Internal Value |
|--------|---------------|
| Yes | `Yes` |
| No | `No` |
| Not Sure | `NotSure` |

**Question 5 — Roof Age** *(always shown, required)*

| Option | Internal Value |
|--------|---------------|
| 0–4 years | `ZeroToFour` |
| 5–15 years | `FiveToFifteen` |
| 15+ years | `FifteenPlus` |
| Not Sure | `Unknown` |

On submit the answers are saved to `localStorage` and the form advances to Step 2.

---

### Step 2 — Location

Collects the property address to power the dealer lookup.

| Field | Type | Notes |
|-------|------|-------|
| Street Address | Text (autocomplete) | Google Maps Places API — selecting a suggestion auto-fills city, state, and ZIP |
| ZIP Code | Text | Exactly 5 numeric digits, required. Non-numeric characters are blocked as you type. Validated against Zippopotam.us on blur. |
| City | Text | Auto-filled from address selection, or entered manually |
| State | Dropdown | All 50 US states + DC |

**ZIP code validation:**
- Non-numeric characters are stripped in real time as the user types.
- `validateZipExists()` calls the Zippopotam.us API to confirm the ZIP is a real US postal code. It is registered as a react-hook-form `validate` rule, so it runs when Step 2 is submitted and blocks advancing to Step 3 — regardless of whether the ZIP was typed, filled by address autocomplete, or prefilled via `data-zip-code`. Results are cached per ZIP.
- On submit, a format guard (`/^\d{5}$/`) in `findDealer()` acts as a final fallback.
- If Zippopotam.us is unreachable, the check is skipped silently so valid submissions are never blocked.

**Error messages:**
- Wrong format (non-numeric or not 5 digits): `"Please enter a valid 5-digit ZIP code"`
- Valid format but ZIP does not exist: `"ZIP code not found. Please enter a valid US ZIP code"`

**On submit:**
- If `data-dealer-id` was provided, the location lookup is skipped.
- Otherwise, `findDealer(location, dealTypeCode)` is called against the Roof Maxx Connect API.
- The returned dealer and `LocationStatus` are saved to `localStorage` for use on the thank-you page.
- The dealer's ID from the lookup is **not** submitted to HubSpot — only a `dealer_id` set explicitly via `data-dealer-id` or `?dealer_id=` is sent. This prevents pre-assigning a dealer before downstream routing completes.
- The form advances to Step 3.

---

### Step 3 — Contact Information

Collects the lead's personal details.

| Field | Type | Validation |
|-------|------|------------|
| First Name | Text | Required |
| Last Name | Text | Required |
| Email | Email | Required, regex validated |
| Phone | Masked text | Required, format `(###) ###-####`, area code must start with 2–9 |
| SMS Opt-In | Checkbox | Optional |

**On submit:**
1. Combines all data from `localStorage` (roof info, location, dealer) with the contact fields.
2. Builds the HubSpot payload (see [HubSpot Submission](#hubspot-submission)).
3. Posts the lead to the HubSpot API.
4. Redirects the user (see [Redirect Logic](#redirect-logic)).
5. Clears all form-related `localStorage` keys (`resetForm`).

---

## Conditional Field Logic

All conditional display logic is driven by `react-hook-form`'s `watch()` in `roof-information-step.tsx`:

```
propertyType === MultiFamily OR Commercial
  → show "Manage Multiple Properties?"

  manageProperty === Yes
    → show "Property Unit Count?"
```

Hidden fields are also excluded from validation, so the user is never blocked by a field they cannot see.

---

## State & Data Persistence

Form state is stored in `localStorage` so that progress survives a page refresh.

| Key | Contents |
|-----|---------|
| `roofInformation` | All Step 1 answers (JSON) |
| `location` | Address, ZIP, city, state from Step 2 (JSON) |
| `dealer` | Dealer object returned by the API (JSON) |
| `status` | `LocationStatus` string from the API |
| `step` | Current step index: `0`, `1`, or `2` |

On successful final submission all keys are cleared via `resetForm()`.

---

## Dealer Finder Logic

`packages/utils/src/dealers.ts` exports two functions:

**`validateZipExists(zipCode)`** — registered as a `validate` rule on the ZIP field, so it runs on Step 2 submit. Hits the Zippopotam.us API to confirm the ZIP is a real US postal code. Returns `true` if valid or if the service is unreachable, `false` if the ZIP does not exist.

**`findDealer(location, dealType)`** — called on Step 2 submit. Contains a format guard that throws `INVALID_ZIP` for non-5-digit values, then calls the Roof Maxx Connect API:

```
GET https://roofmaxxconnect.com/api/v1/map-status
    postal_code, city, state, address, deal_type, api_token
```

The API responds with a `LocationStatus` and a `Dealer` object.

### LocationStatus Values

| Status | Meaning |
|--------|---------|
| `TERRITORY` | Active dealer in the user's area |
| `CA1` | Close enough — dealer covers this area |
| `CA2` | On the boundary — dealer may cover this area |
| `HOLD` | Area on hold, no active dealer |
| `ERROR` | API error or no response |
| `CORPCOMMERCIAL` | Corporate commercial account territory |

`isDealerNearby()` returns `true` only for `TERRITORY` and `CA1`. This result drives the redirect decision.

### Dealer Slug Attribution

`form.tsx` also checks the current URL for the pattern `/find-a-dealer/{state}/{city}`. When found, the slug is forwarded to HubSpot as an attribution signal.

---

## HubSpot Submission

`packages/utils/src/hubspot.ts` submits to:

```
POST https://api.hsforms.com/submissions/v3/integration/submit/6044336/{formId}
```

- **Portal ID:** `6044336`
- **Form ID:** `3e57be1b-f9df-4263-9cf4-8c11d67427fd`

### Fields Sent to HubSpot

| HubSpot Field | Source |
|---------------|--------|
| `email`, `firstname`, `lastname` | Step 3 |
| `phone`, `sms_opt_in_checkbox` | Step 3 |
| `address`, `zip`, `city`, `state` | Step 2 |
| `property_type`, `roof_age`, `shingle_roof` | Step 1 |
| `manage_property`, `property_unit_count` | Step 1 (conditional) |
| `dealer_id` | Only when explicitly set via `data-dealer-id` attribute or `?dealer_id=` URL param — **not** from the automatic dealer lookup |
| `had_explicit_dealer_id` | `true` if dealer_id was explicitly provided, `false` if it came from the lookup |
| `utm_source`, `utm_campaign`, `utm_medium`, `utm_content`, `utm_term` | Data attributes / URL |
| `steps_completed`, `page_variant` | Internal form metadata |
| `lead_type_code`, `program_type_code` | Data attributes |
| `microsite_type`, `self_generated` | Data attributes |
| `deal_type_code`, `source_vendor` | Data attributes |

The submission also includes:
- The HubSpot tracking cookie (`hubspotutk`) for contact deduplication
- The client's IP address (fetched asynchronously from `api64.ipify.org`)
- The current page URI and title as context

---

## Redirect Logic

After a successful HubSpot submission:

1. A URL is built by appending all collected form fields as query parameters to `data-thank-you-page`.
2. **If** `data-sorry-page` is set **AND** `isDealerNearby()` returns `false` → redirect to the sorry page.
3. **Otherwise** → redirect to the thank-you page.

### Thank-you page URL parameters

Key parameters passed to the thank-you page for routing decisions:

| Parameter | Value | Notes |
|-----------|-------|-------|
| `dealer_found` | `"true"` / `"false"` | Whether a dealer is available — either explicitly assigned or found via lookup |
| `dealer_status` | `LocationStatus` string | Raw status from the dealer API |
| `dealer_bill_com_customer_name` | string | Dealer's Bill.com customer name (if a dealer was found) — used to route Enterprise/PMI leads |
| `dealer_id` | string | Only present when explicitly set via `data-dealer-id` or `?dealer_id=` |

All `http://` URLs are automatically upgraded to `https://` before redirecting.

---

## Google Maps Integration

Step 2 wraps the location fields with `@react-google-maps/api`'s `LoadScript` component.

The `StreetInput` component:
- Uses the Google Places `AutocompleteService` to fetch address predictions as the user types (debounced 300 ms).
- Renders a dropdown of predictions with keyboard navigation (arrow keys, Enter, Escape).
- On selection, calls `PlacesService.getDetails()` to extract structured address components (street number, route, city, state, ZIP).
- Auto-fills the ZIP, city, and state fields from the selected result.

---

## Component Architecture

The project is a Turborepo monorepo with three shared packages consumed by the form app.

### Monorepo Structure

```
roofmaxx/
├── apps/form/          # The embeddable React form application
└── packages/
    ├── components/     # Reusable UI components
    ├── types/          # Shared TypeScript types and enums
    └── utils/          # Service functions (HubSpot, dealer API, URL helpers)
```

### Key UI Components (`packages/components`)

| Component | Description |
|-----------|-------------|
| `Input` | Text input with error state styling |
| `StreetInput` | Google Places autocomplete input |
| `PhoneInput` | Masked phone input — format `(###) ###-####`, first digit 2–9 |
| `Select` | Styled select dropdown |
| `Spinner` | Animated 4-dot loading indicator |
| `IconRadioGroup` / `IconRadioItem` | Icon-based radio button groups used for Step 1 questions |
| `StepCard` | Animated step container with 500 ms slide transition |
| `StepSelector` | Numbered progress indicator — allows jumping back to completed steps |
| `FormGroup`, `FormLabel`, `FormError` | Form layout primitives |

### TypeScript Types (`packages/types`)

| Type | Description |
|------|-------------|
| `Step` enum | `RoofInformation (0)`, `Location (1)`, `ContactInformation (2)` |
| `RoofInformation` | Step 1 answer shape |
| `Location` | Step 2 address shape |
| `Contact` | Step 3 contact shape |
| `Dealer` | Dealer object returned by the API |
| `LocationStatus` | Enum of possible dealer proximity statuses |
| `HubSpotFields` | Generic field mapping for HubSpot submission |

---

## Styling & Theming

Tailwind CSS is used with a `tw-` class prefix to avoid collisions with the host page's styles. The `important: true` option is set so form styles always win over host-page styles.

### Brand Color Palette

| Token | Hex | Usage |
|-------|-----|-------|
| `roofmaxx-primary` | `#003057` | Dark blue — primary buttons and headers |
| `roofmaxx-secondary` | `#0088BB` | Light blue — focus states and accents |
| `roofmaxx-accent` | `#CC6C27` | Orange — warning and info states |
| `roofmaxx-accent2` | `#D68A52` | Light orange |
| `roofmaxx-accent3` | `#66B8D6` | Cyan |

### Animations

| Animation | Duration | Used by |
|-----------|----------|---------|
| Step slide in/out | 500 ms | `StepCard` (Headless UI Transition) |
| Icon hover scale | 300 ms | `IconRadioItem` |
| Spinner (popout/translate/popin) | Custom keyframes | `Spinner` component |

---

## Build & Development

The project uses **Turborepo** for task orchestration and **Parcel 2** as the bundler.

**Install dependencies** (run from the repository root — do not run `npm install` in subdirectories):

```bash
npm install
```

**Start dev server** (http://localhost:3000):

```bash
npm run dev
```

**Production build** (outputs to `apps/form/dist/`):

```bash
npm run build
```

The compiled output is a global-format IIFE bundle (`dist/form.js` + `dist/form.css`). No module system is required on the host page.

---

## Third-Party Services Reference

| Service | Purpose | Key / Endpoint |
|---------|---------|----------------|
| HubSpot Forms API | Lead submission | Portal `6044336` · Form `3e57be1b-f9df-4263-9cf4-8c11d67427fd` |
| Google Maps Places API | Address autocomplete | Key: `AIzaSyA6XzSKqUYfuG97HDpQmrDZXBHVHM8vbYs` |
| Roof Maxx Connect API | Dealer lookup | `https://roofmaxxconnect.com/api/v1/map-status` |
| Zippopotam.us | ZIP code existence validation | `https://api.zippopotam.us/us/{zipcode}` — free, no key required |
| ipify | Client IP address | `https://api64.ipify.org?format=json` |
| Wistia | Intro video | Video ID: `zfdfuxftgy` |

To rotate any key or endpoint, update the relevant source file:

| Key | File |
|-----|------|
| HubSpot Portal & Form ID | `packages/utils/src/hubspot.ts` |
| Google Maps API Key | `apps/form/src/steps/location-step.tsx` |
| Dealer API endpoint & token | `packages/utils/src/dealers.ts` |
| Wistia Video ID | `apps/form/src/components/video-form.tsx` |

---
_Source: https://npm.io/package/@roofmaxx/form · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
