@metron-labs/quote-react 0.1.16 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,31 +1,31 @@
1
- Copyright (c) 2026 Metron Labs Ltd. All rights reserved.
2
-
3
- This software and its accompanying files, including images and stylesheets (the
4
- "Software"), are proprietary to Metron Labs Ltd ("Metron Labs").
5
-
6
- Metron Labs grants you a non-exclusive, non-transferable licence to install the Software and
7
- include it in websites and applications you operate, solely to provide Metron Labs's services
8
- to your own customers, for as long as you hold a current agreement with Metron Labs that
9
- covers the Software. That agreement governs your use of the Software and takes
10
- precedence over this notice.
11
-
12
- Except as that agreement expressly permits, you may not:
13
-
14
- - copy, modify, merge or create derivative works of the Software;
15
- - distribute, publish, sublicense, sell or otherwise make the Software available to
16
- any third party, other than as part of your own website or application;
17
- - use the Software with any service other than Metron Labs'; or
18
- - reverse engineer the Software, except to the extent the law allows despite this
19
- restriction.
20
-
21
- Your licence ends automatically when your agreement with Metron Labs ends. You must then stop
22
- using the Software and remove it from your websites, applications and systems.
23
-
24
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
25
- INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
26
- PURPOSE AND NON-INFRINGEMENT, EXCEPT AS SET OUT IN YOUR AGREEMENT WITH Metron Labs. TO THE
27
- EXTENT PERMITTED BY LAW, METRON LABS IS NOT LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY
28
- ARISING FROM OR IN CONNECTION WITH THE SOFTWARE OR ITS USE, EXCEPT AS SET OUT IN THAT
29
- AGREEMENT.
30
-
31
- For licensing questions, contact your Metron Labs account manager.
1
+ Copyright (c) 2026 Metron Labs Ltd. All rights reserved.
2
+
3
+ This software and its accompanying files, including images and stylesheets (the
4
+ "Software"), are proprietary to Metron Labs Ltd ("Metron Labs").
5
+
6
+ Metron Labs grants you a non-exclusive, non-transferable licence to install the Software and
7
+ include it in websites and applications you operate, solely to provide Metron Labs's services
8
+ to your own customers, for as long as you hold a current agreement with Metron Labs that
9
+ covers the Software. That agreement governs your use of the Software and takes
10
+ precedence over this notice.
11
+
12
+ Except as that agreement expressly permits, you may not:
13
+
14
+ - copy, modify, merge or create derivative works of the Software;
15
+ - distribute, publish, sublicense, sell or otherwise make the Software available to
16
+ any third party, other than as part of your own website or application;
17
+ - use the Software with any service other than Metron Labs'; or
18
+ - reverse engineer the Software, except to the extent the law allows despite this
19
+ restriction.
20
+
21
+ Your licence ends automatically when your agreement with Metron Labs ends. You must then stop
22
+ using the Software and remove it from your websites, applications and systems.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
25
+ INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
26
+ PURPOSE AND NON-INFRINGEMENT, EXCEPT AS SET OUT IN YOUR AGREEMENT WITH Metron Labs. TO THE
27
+ EXTENT PERMITTED BY LAW, METRON LABS IS NOT LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY
28
+ ARISING FROM OR IN CONNECTION WITH THE SOFTWARE OR ITS USE, EXCEPT AS SET OUT IN THAT
29
+ AGREEMENT.
30
+
31
+ For licensing questions, contact your Metron Labs account manager.
package/README.md CHANGED
@@ -1,222 +1,222 @@
1
- # @metron-labs/quote-react
2
-
3
- Metron's quoting tools as React components you place in your own website:
4
-
5
- - **Quick Quote** — your customers upload a schedule, paste a materials list or photograph
6
- a handwritten one. Metron matches each line to products in your catalogue, then shows
7
- prices, quantities and a basket they can download as a spreadsheet.
8
- - **Estimating Tools** — 12 calculators (fencing, decking, brick and block, paint, timber,
9
- paving and more) that turn a customer's measurements into a priced list of your products.
10
-
11
- Both widgets call Metron's API straight from the browser, so you don't need your own server.
12
-
13
- ## Before you start
14
-
15
- Your Metron account manager will give you:
16
-
17
- | What | Example | Used for |
18
- | ---------------- | ----------- | -------------------------------------------- |
19
- | Client code | `ABCD-1234` | Identifies your catalogue. Not secret. |
20
- | API key | `boq_...` | Authenticates the widgets with Metron's API. |
21
- | npm access token | `npm_...` | Installing this package, which is private. |
22
-
23
- Tell Metron every web address the widgets will run on: production, staging and local
24
- development (for example `http://localhost:3000`). The API key only works from addresses
25
- registered to your account (see [Security](#security)).
26
-
27
- ## Requirements
28
-
29
- - React 18.3 or later, or React 19.
30
- - A bundler that can import images and CSS, such as Vite, Next.js, webpack 5 or Parcel.
31
- The widget images ship inside the package, and your build serves them like your own.
32
-
33
- Not using React? Ask Metron for the script-tag version, which works on any website.
34
-
35
- ## Install
36
-
37
- The package is private, so npm needs your access token. Add a `.npmrc` file to your
38
- project root:
39
-
40
- ```ini
41
- @metron-labs:registry=https://registry.npmjs.org/
42
- //registry.npmjs.org/:_authToken=${NPM_TOKEN}
43
- ```
44
-
45
- Set `NPM_TOKEN` in your shell and CI, then install:
46
-
47
- ```sh
48
- npm install @metron-labs/quote-react
49
- ```
50
-
51
- Don't commit the token itself; `${NPM_TOKEN}` reads it from the environment.
52
-
53
- ## Quick start
54
-
55
- ```tsx
56
- import { QuoteProvider, QuickQuote } from "@metron-labs/quote-react";
57
- import "@metron-labs/quote-react/styles.css";
58
-
59
- const quoteConfig = {
60
- apiBaseUrl: "https://api.metronlabs.ai",
61
- client: "ABCD-1234",
62
- apiKey: "boq_...",
63
- theme: { "--accent": "#0a7d3b" },
64
- };
65
-
66
- export default function QuotePage() {
67
- return (
68
- <QuoteProvider config={quoteConfig}>
69
- <QuickQuote />
70
- </QuoteProvider>
71
- );
72
- }
73
- ```
74
-
75
- For the calculators, render `<EstimatingTools />` in the same way.
76
-
77
- Import `styles.css` once, anywhere in your app. Without it the widgets render unstyled.
78
-
79
- ### Both widgets on one site
80
-
81
- Use one `QuoteProvider` per page, and put every widget on that page inside it:
82
-
83
- ```tsx
84
- <QuoteProvider config={quoteConfig}>{tab === "quote" ? <QuickQuote hideHeader /> : <EstimatingTools hideHeader />}</QuoteProvider>
85
- ```
86
-
87
- The widgets don't include site navigation. To offer both, link between them from your own
88
- menu or tabs.
89
-
90
- ### Next.js (App Router)
91
-
92
- The widgets are client components and already marked `"use client"`, so you can render them
93
- from a server component page. Import `styles.css` in your root `layout.tsx` or in the page
94
- itself.
95
-
96
- ### Keeping settings per environment
97
-
98
- To use different API keys for staging and production, read the config from environment
99
- variables at build time. For example, with Vite:
100
-
101
- ```ts
102
- const quoteConfig = {
103
- apiBaseUrl: import.meta.env.VITE_METRON_API_URL,
104
- client: import.meta.env.VITE_METRON_CLIENT,
105
- apiKey: import.meta.env.VITE_METRON_API_KEY,
106
- };
107
- ```
108
-
109
- ## Configuration
110
-
111
- `QuoteProvider` takes a single `config` prop:
112
-
113
- | Option | Type | Required | Description |
114
- | ------------ | ------------------------ | -------- | ------------------------------------------------------- |
115
- | `apiBaseUrl` | `string` | Yes | Metron's API address, e.g. `https://api.metronlabs.ai`. |
116
- | `client` | `string` | Yes | Your client code. |
117
- | `apiKey` | `string` | Yes | Your API key. |
118
- | `theme` | `Record<string, string>` | No | Colour and style overrides; see [Theming](#theming). |
119
-
120
- ## Widget props
121
-
122
- `QuickQuote` and `EstimatingTools` take the same optional props:
123
-
124
- | Prop | Default | Description |
125
- | ------------ | ------- | ----------------------------------------------------------------------------------------------------------------------- |
126
- | `hideHeader` | `false` | Hides the widget's own heading, for pages that already show one. |
127
- | `fullPage` | `false` | Gives the widget its own background and full viewport height. Leave it off to place the widget inside your page layout. |
128
-
129
- ## Theming
130
-
131
- Pass CSS custom properties in `config.theme` to match your brand. Anything you leave out
132
- keeps the default shown here.
133
-
134
- ```ts
135
- theme: {
136
- "--accent": "#0a7d3b",
137
- "--accent-light": "#d3eddd",
138
- "--accent-dark": "#075e2c",
139
- "--accent-hover": "#eef8f2",
140
- "--font-display": '"Your Font", system-ui, sans-serif',
141
- "--radius": "8px",
142
- }
143
- ```
144
-
145
- | Variable | Default | Used for |
146
- | --------------------------- | ------------------------------------ | ---------------------------------- |
147
- | `--accent` | `#e8622c` | Buttons, highlights, active states |
148
- | `--accent-light` | `#fbe3d5` | Tinted accent backgrounds |
149
- | `--accent-dark` | `#c94f1f` | Accent text and pressed states |
150
- | `--accent-hover` | `#fff3ec` | Hover backgrounds |
151
- | `--on-accent` | `#fff` | Text on accent buttons (use a dark colour for light accents) |
152
- | `--link` | same as `--accent` | Links |
153
- | `--navy` | `#10213f` | Dark hero panels |
154
- | `--navy-light` | `#182c52` | Raised areas on dark panels |
155
- | `--navy-border` | `rgba(255, 255, 255, 0.12)` | Borders on dark panels |
156
- | `--bg` | `#ffffff` | Cards and raised panels |
157
- | `--bg2` | `#f9fafb` | Widget background |
158
- | `--bg3` | `#f3f4f6` | Subtle fills |
159
- | `--border` | `#e5e7eb` | Borders |
160
- | `--border-dark` | `#d1d5db` | Stronger borders |
161
- | `--text` | `#111827` | Main text |
162
- | `--text-mid` | `#6b7280` | Secondary text |
163
- | `--text-dim` | `#9ca3af` | Hints and placeholders |
164
- | `--green` / `--green-light` | `#16a34a` / `#dcfce7` | Matched and success states |
165
- | `--red` / `--red-light` | `#dc2626` / `#fee2e2` | Errors |
166
- | `--amber` / `--amber-light` | `#d97706` / `#fef3c7` | Warnings and items to review |
167
- | `--font-display` | Inter, then the system font | All widget text |
168
- | `--font-mono` | IBM Plex Mono, then system monospace | Product codes |
169
- | `--radius` / `--radius-sm` | `0` | Corner rounding |
170
- | `--shadow` / `--shadow-md` | subtle drop shadows | Cards and popovers |
171
-
172
- The widgets don't load any fonts. Inter is used only if your page already loads it;
173
- otherwise they fall back to the system font. Set `--font-display` to match your site.
174
-
175
- ### How the styles interact with your page
176
-
177
- Every rule in `styles.css` is scoped to the widget, so it won't restyle anything else on
178
- your page. The scoping only works one way, though: a broad rule on your site, such as one
179
- that targets every `button` or `input`, can still reach elements inside the widget. If a
180
- widget looks wrong on your site, check for global element selectors first.
181
-
182
- ## Security
183
-
184
- The API key is included in your page's JavaScript, so treat it as public. Metron protects
185
- your account by only accepting the key from the web addresses registered to it. A copied
186
- key won't work from any other site.
187
-
188
- - Register each environment's address with Metron before going live. If an address isn't
189
- registered, the widgets load but their requests are rejected.
190
- - If you think a key has been misused, ask Metron to issue a new one.
191
- - The npm access token is different: it's a real secret. Keep it in your CI secrets and
192
- local environment, never in source code.
193
-
194
- ## Troubleshooting
195
-
196
- **The widget renders unstyled.** Import `@metron-labs/quote-react/styles.css`.
197
-
198
- **Requests fail with 401 or a CORS error in the browser console.** The page's address isn't
199
- registered for your API key. Send Metron the exact origin, including protocol and port
200
- (e.g. `https://staging.example.com`).
201
-
202
- **`npm install` fails with 401, 403 or 404.** Check that `.npmrc` is in your project root and
203
- that `NPM_TOKEN` is set wherever you install, including CI.
204
-
205
- **The build fails on a `.webp` import.** Your bundler needs to handle image imports. Vite,
206
- Next.js and Parcel do this automatically. In webpack 5, add an `asset/resource` rule for
207
- `.webp`.
208
-
209
- ## Versioning
210
-
211
- Releases follow semantic versioning. While the version is `0.x`, a minor release
212
- (`0.1` → `0.2`) may include breaking changes, so allow patch updates only
213
- (`"~0.1.0"`) and upgrade minor versions deliberately.
214
-
215
- ## Support
216
-
217
- Contact your Metron account manager.
218
-
219
- ## Licence
220
-
221
- Proprietary. You may use this package only under a current agreement with Metron Labs Ltd;
222
- see [LICENSE](LICENSE) for the terms.
1
+ # @metron-labs/quote-react
2
+
3
+ Metron's quoting tools as React components you place in your own website:
4
+
5
+ - **Quick Quote** — your customers upload a schedule, paste a materials list or photograph
6
+ a handwritten one. Metron matches each line to products in your catalogue, then shows
7
+ prices, quantities and a basket they can download as a spreadsheet.
8
+ - **Estimating Tools** — 12 calculators (fencing, decking, brick and block, paint, timber,
9
+ paving and more) that turn a customer's measurements into a priced list of your products.
10
+
11
+ Both widgets call Metron's API straight from the browser, so you don't need your own server.
12
+
13
+ ## Before you start
14
+
15
+ Your Metron account manager will give you:
16
+
17
+ | What | Example | Used for |
18
+ | ---------------- | ----------- | -------------------------------------------- |
19
+ | Client code | `ABCD-1234` | Identifies your catalogue. Not secret. |
20
+ | API key | `boq_...` | Authenticates the widgets with Metron's API. |
21
+ | npm access token | `npm_...` | Installing this package, which is private. |
22
+
23
+ Tell Metron every web address the widgets will run on: production, staging and local
24
+ development (for example `http://localhost:3000`). The API key only works from addresses
25
+ registered to your account (see [Security](#security)).
26
+
27
+ ## Requirements
28
+
29
+ - React 18.3 or later, or React 19.
30
+ - A bundler that can import images and CSS, such as Vite, Next.js, webpack 5 or Parcel.
31
+ The widget images ship inside the package, and your build serves them like your own.
32
+
33
+ Not using React? Ask Metron for the script-tag version, which works on any website.
34
+
35
+ ## Install
36
+
37
+ The package is private, so npm needs your access token. Add a `.npmrc` file to your
38
+ project root:
39
+
40
+ ```ini
41
+ @metron-labs:registry=https://registry.npmjs.org/
42
+ //registry.npmjs.org/:_authToken=${NPM_TOKEN}
43
+ ```
44
+
45
+ Set `NPM_TOKEN` in your shell and CI, then install:
46
+
47
+ ```sh
48
+ npm install @metron-labs/quote-react
49
+ ```
50
+
51
+ Don't commit the token itself; `${NPM_TOKEN}` reads it from the environment.
52
+
53
+ ## Quick start
54
+
55
+ ```tsx
56
+ import { QuoteProvider, QuickQuote } from "@metron-labs/quote-react";
57
+ import "@metron-labs/quote-react/styles.css";
58
+
59
+ const quoteConfig = {
60
+ apiBaseUrl: "https://api.metronlabs.ai",
61
+ client: "ABCD-1234",
62
+ apiKey: "boq_...",
63
+ theme: { "--accent": "#0a7d3b" },
64
+ };
65
+
66
+ export default function QuotePage() {
67
+ return (
68
+ <QuoteProvider config={quoteConfig}>
69
+ <QuickQuote />
70
+ </QuoteProvider>
71
+ );
72
+ }
73
+ ```
74
+
75
+ For the calculators, render `<EstimatingTools />` in the same way.
76
+
77
+ Import `styles.css` once, anywhere in your app. Without it the widgets render unstyled.
78
+
79
+ ### Both widgets on one site
80
+
81
+ Use one `QuoteProvider` per page, and put every widget on that page inside it:
82
+
83
+ ```tsx
84
+ <QuoteProvider config={quoteConfig}>{tab === "quote" ? <QuickQuote hideHeader /> : <EstimatingTools hideHeader />}</QuoteProvider>
85
+ ```
86
+
87
+ The widgets don't include site navigation. To offer both, link between them from your own
88
+ menu or tabs.
89
+
90
+ ### Next.js (App Router)
91
+
92
+ The widgets are client components and already marked `"use client"`, so you can render them
93
+ from a server component page. Import `styles.css` in your root `layout.tsx` or in the page
94
+ itself.
95
+
96
+ ### Keeping settings per environment
97
+
98
+ To use different API keys for staging and production, read the config from environment
99
+ variables at build time. For example, with Vite:
100
+
101
+ ```ts
102
+ const quoteConfig = {
103
+ apiBaseUrl: import.meta.env.VITE_METRON_API_URL,
104
+ client: import.meta.env.VITE_METRON_CLIENT,
105
+ apiKey: import.meta.env.VITE_METRON_API_KEY,
106
+ };
107
+ ```
108
+
109
+ ## Configuration
110
+
111
+ `QuoteProvider` takes a single `config` prop:
112
+
113
+ | Option | Type | Required | Description |
114
+ | ------------ | ------------------------ | -------- | ------------------------------------------------------- |
115
+ | `apiBaseUrl` | `string` | Yes | Metron's API address, e.g. `https://api.metronlabs.ai`. |
116
+ | `client` | `string` | Yes | Your client code. |
117
+ | `apiKey` | `string` | Yes | Your API key. |
118
+ | `theme` | `Record<string, string>` | No | Colour and style overrides; see [Theming](#theming). |
119
+
120
+ ## Widget props
121
+
122
+ `QuickQuote` and `EstimatingTools` take the same optional props:
123
+
124
+ | Prop | Default | Description |
125
+ | ------------ | ------- | ----------------------------------------------------------------------------------------------------------------------- |
126
+ | `hideHeader` | `false` | Hides the widget's own heading, for pages that already show one. |
127
+ | `fullPage` | `false` | Gives the widget its own background and full viewport height. Leave it off to place the widget inside your page layout. |
128
+
129
+ ## Theming
130
+
131
+ Pass CSS custom properties in `config.theme` to match your brand. Anything you leave out
132
+ keeps the default shown here.
133
+
134
+ ```ts
135
+ theme: {
136
+ "--accent": "#0a7d3b",
137
+ "--accent-light": "#d3eddd",
138
+ "--accent-dark": "#075e2c",
139
+ "--accent-hover": "#eef8f2",
140
+ "--font-display": '"Your Font", system-ui, sans-serif',
141
+ "--radius": "8px",
142
+ }
143
+ ```
144
+
145
+ | Variable | Default | Used for |
146
+ | --------------------------- | ------------------------------------ | ---------------------------------- |
147
+ | `--accent` | `#e8622c` | Buttons, highlights, active states |
148
+ | `--accent-light` | `#fbe3d5` | Tinted accent backgrounds |
149
+ | `--accent-dark` | `#c94f1f` | Accent text and pressed states |
150
+ | `--accent-hover` | `#fff3ec` | Hover backgrounds |
151
+ | `--on-accent` | `#fff` | Text on accent buttons (use a dark colour for light accents) |
152
+ | `--link` | same as `--accent` | Links |
153
+ | `--navy` | `#10213f` | Dark hero panels |
154
+ | `--navy-light` | `#182c52` | Raised areas on dark panels |
155
+ | `--navy-border` | `rgba(255, 255, 255, 0.12)` | Borders on dark panels |
156
+ | `--bg` | `#ffffff` | Cards and raised panels |
157
+ | `--bg2` | `#f9fafb` | Widget background |
158
+ | `--bg3` | `#f3f4f6` | Subtle fills |
159
+ | `--border` | `#e5e7eb` | Borders |
160
+ | `--border-dark` | `#d1d5db` | Stronger borders |
161
+ | `--text` | `#111827` | Main text |
162
+ | `--text-mid` | `#6b7280` | Secondary text |
163
+ | `--text-dim` | `#9ca3af` | Hints and placeholders |
164
+ | `--green` / `--green-light` | `#16a34a` / `#dcfce7` | Matched and success states |
165
+ | `--red` / `--red-light` | `#dc2626` / `#fee2e2` | Errors |
166
+ | `--amber` / `--amber-light` | `#d97706` / `#fef3c7` | Warnings and items to review |
167
+ | `--font-display` | Inter, then the system font | All widget text |
168
+ | `--font-mono` | IBM Plex Mono, then system monospace | Product codes |
169
+ | `--radius` / `--radius-sm` | `0` | Corner rounding |
170
+ | `--shadow` / `--shadow-md` | subtle drop shadows | Cards and popovers |
171
+
172
+ The widgets don't load any fonts. Inter is used only if your page already loads it;
173
+ otherwise they fall back to the system font. Set `--font-display` to match your site.
174
+
175
+ ### How the styles interact with your page
176
+
177
+ Every rule in `styles.css` is scoped to the widget, so it won't restyle anything else on
178
+ your page. The scoping only works one way, though: a broad rule on your site, such as one
179
+ that targets every `button` or `input`, can still reach elements inside the widget. If a
180
+ widget looks wrong on your site, check for global element selectors first.
181
+
182
+ ## Security
183
+
184
+ The API key is included in your page's JavaScript, so treat it as public. Metron protects
185
+ your account by only accepting the key from the web addresses registered to it. A copied
186
+ key won't work from any other site.
187
+
188
+ - Register each environment's address with Metron before going live. If an address isn't
189
+ registered, the widgets load but their requests are rejected.
190
+ - If you think a key has been misused, ask Metron to issue a new one.
191
+ - The npm access token is different: it's a real secret. Keep it in your CI secrets and
192
+ local environment, never in source code.
193
+
194
+ ## Troubleshooting
195
+
196
+ **The widget renders unstyled.** Import `@metron-labs/quote-react/styles.css`.
197
+
198
+ **Requests fail with 401 or a CORS error in the browser console.** The page's address isn't
199
+ registered for your API key. Send Metron the exact origin, including protocol and port
200
+ (e.g. `https://staging.example.com`).
201
+
202
+ **`npm install` fails with 401, 403 or 404.** Check that `.npmrc` is in your project root and
203
+ that `NPM_TOKEN` is set wherever you install, including CI.
204
+
205
+ **The build fails on a `.webp` import.** Your bundler needs to handle image imports. Vite,
206
+ Next.js and Parcel do this automatically. In webpack 5, add an `asset/resource` rule for
207
+ `.webp`.
208
+
209
+ ## Versioning
210
+
211
+ Releases follow semantic versioning. While the version is `0.x`, a minor release
212
+ (`0.1` → `0.2`) may include breaking changes, so allow patch updates only
213
+ (`"~0.1.0"`) and upgrade minor versions deliberately.
214
+
215
+ ## Support
216
+
217
+ Contact your Metron account manager.
218
+
219
+ ## Licence
220
+
221
+ Proprietary. You may use this package only under a current agreement with Metron Labs Ltd;
222
+ see [LICENSE](LICENSE) for the terms.
@@ -19,7 +19,7 @@ function EstimatingToolsInner({ fullPage = false, hideHeader = false, internal =
19
19
  const [activeEstimatorId, setActiveEstimatorId] = useState(null);
20
20
  const [review, setReview] = useState(null);
21
21
  const activeEstimator = ESTIMATOR_TOOLS.find((tool) => tool.id === activeEstimatorId);
22
- const { cart, updateCartQty, updateCartSellUnits, updateCartConversions, removeFromCart } = useCart();
22
+ const { cart, updateCartQty, updateCartSellUnits, updateCartConversions, removeFromCart, clearCart } = useCart();
23
23
  const { adding, basketError, addToBasket } = useFixedBasket(activeEstimator?.title ?? "calculator");
24
24
  const reviewOrBasket = (requirements) => {
25
25
  if (!activeEstimator)
@@ -30,5 +30,10 @@ function EstimatingToolsInner({ fullPage = false, hideHeader = false, internal =
30
30
  else
31
31
  setReview(requirements);
32
32
  };
33
- return (_jsx("div", { className: `app-wrapper${fullPage ? "" : " app-wrapper--embedded"}${hideHeader ? " app-wrapper--no-header" : ""}`, style: getQuoteConfig().theme, children: _jsx("main", { className: "main-content", children: showBasket ? (_jsx(BasketStage, { cart: cart, onUpdateQty: updateCartQty, onUpdateSellUnits: updateCartSellUnits, onResolveConversions: updateCartConversions, onRemove: removeFromCart, onBack: () => onShowBasket(false), internal: internal })) : adding && activeEstimator ? (_jsx(EstimatorBasketLoading, { toolTitle: activeEstimator.title })) : activeEstimator ? (_jsxs(_Fragment, { children: [_jsx("div", { hidden: review !== null, children: _jsx(activeEstimator.Component, { onBack: () => setActiveEstimatorId(null), onReview: reviewOrBasket }) }), basketError && _jsx("p", { className: "results-empty-hint estimator-products__error", children: basketError }), review !== null && (_jsx(EstimatorReviewStage, { toolId: activeEstimator.id, toolTitle: activeEstimator.title, requirements: review, onBack: () => setReview(null) }))] })) : (_jsx(_Fragment, { children: _jsxs("div", { className: "quick-order-card", children: [_jsxs("div", { className: "quick-order-hero", children: [_jsx("div", { className: "quick-order-eyebrow", children: "Construction Services" }), _jsx("h1", { children: "Estimating Tools" }), _jsx("p", { children: "Trade-specific calculators for trusses, fencing, decking, masonry and painting. Dial in your specs and get an instant material estimate." })] }), _jsxs("div", { className: "quick-order-alt", children: [_jsx("div", { className: "quick-order-alt-title", children: "Choose a Calculator" }), _jsx("div", { className: "estimator-tools-grid", children: ESTIMATOR_TOOLS.map((tool) => (_jsxs("button", { type: "button", className: "estimator-tool-card", onClick: () => { setActiveEstimatorId(tool.id); setReview(null); }, children: [_jsx("span", { className: "estimator-tool-card__image-wrap", children: _jsx("img", { className: "fill-img estimator-tool-card__image", src: assetUrl(tool.imageUrl), alt: "" }) }), _jsxs("span", { className: "estimator-tool-card__content", children: [_jsx("span", { className: "estimator-tool-card__title", children: tool.title }), _jsx("span", { className: "estimator-tool-card__desc", children: tool.desc })] })] }, tool.id))) })] })] }) })) }) }));
33
+ return (_jsx("div", { className: `app-wrapper${fullPage ? "" : " app-wrapper--embedded"}${hideHeader ? " app-wrapper--no-header" : ""}`, style: getQuoteConfig().theme, children: _jsx("main", { className: "main-content", children: showBasket ? (_jsx(BasketStage, { cart: cart, onUpdateQty: updateCartQty, onUpdateSellUnits: updateCartSellUnits, onResolveConversions: updateCartConversions, onRemove: removeFromCart, onBack: () => onShowBasket(false), onStartNewQuote: () => {
34
+ clearCart();
35
+ setActiveEstimatorId(null);
36
+ setReview(null);
37
+ onShowBasket(false);
38
+ }, internal: internal })) : adding && activeEstimator ? (_jsx(EstimatorBasketLoading, { toolTitle: activeEstimator.title })) : activeEstimator ? (_jsxs(_Fragment, { children: [_jsx("div", { hidden: review !== null, children: _jsx(activeEstimator.Component, { onBack: () => setActiveEstimatorId(null), onReview: reviewOrBasket }) }), basketError && _jsx("p", { className: "results-empty-hint estimator-products__error", children: basketError }), review !== null && (_jsx(EstimatorReviewStage, { toolId: activeEstimator.id, toolTitle: activeEstimator.title, requirements: review, onBack: () => setReview(null) }))] })) : (_jsx(_Fragment, { children: _jsxs("div", { className: "quick-order-card", children: [_jsxs("div", { className: "quick-order-hero", children: [_jsx("div", { className: "quick-order-eyebrow", children: "Construction Services" }), _jsx("h1", { children: "Estimating Tools" }), _jsx("p", { children: "Trade-specific calculators for trusses, fencing, decking, masonry and painting. Dial in your specs and get an instant material estimate." })] }), _jsxs("div", { className: "quick-order-alt", children: [_jsx("div", { className: "quick-order-alt-title", children: "Choose a Calculator" }), _jsx("div", { className: "estimator-tools-grid", children: ESTIMATOR_TOOLS.map((tool) => (_jsxs("button", { type: "button", className: "estimator-tool-card", onClick: () => { setActiveEstimatorId(tool.id); setReview(null); }, children: [_jsx("span", { className: "estimator-tool-card__image-wrap", children: _jsx("img", { className: "fill-img estimator-tool-card__image", src: assetUrl(tool.imageUrl), alt: "" }) }), _jsxs("span", { className: "estimator-tool-card__content", children: [_jsx("span", { className: "estimator-tool-card__title", children: tool.title }), _jsx("span", { className: "estimator-tool-card__desc", children: tool.desc })] })] }, tool.id))) })] })] }) })) }) }));
34
39
  }
@@ -173,5 +173,5 @@ function QuickQuoteInner({ fullPage = false, hideHeader = false, internal = fals
173
173
  setEntryMode("schedule");
174
174
  await runScheduleMatch(file);
175
175
  }, [runScheduleMatch]);
176
- return (_jsx("div", { className: `app-wrapper${fullPage ? "" : " app-wrapper--embedded"}${hideHeader ? " app-wrapper--no-header" : ""}${!fullPage && stage === "processing" ? " app-wrapper--loading" : ""}`, style: getQuoteConfig().theme, children: _jsxs("main", { className: "main-content", children: [error && (_jsxs("div", { className: "error-box", role: "alert", children: [_jsx("strong", { children: "Error:" }), " ", error] })), stage === "entry" && (_jsx(EntryStage, { onPasteSchedule: handlePasteSchedule, onUploadSchedule: handleUploadSchedule, onEntryModeChange: setEntryMode })), stage === "results" && entryMode === "schedule" && scheduleResult && (_jsx(ScheduleResultsStage, { scheduleResult: scheduleResult, onStartOver: resetToEntry, cart: cart, onAddToCart: addToCart, onRemoveFromCart: removeFromCart, onGoToBasket: goToBasket, cartSeeded: cartSeeded, onCartSeeded: () => setCartSeeded(true), reviewGateComplete: scheduleReviewGateComplete, onReviewGateComplete: () => setScheduleReviewGateComplete(true), internal: internal })), stage === "basket" && (_jsx(BasketStage, { cart: cart, onUpdateQty: updateCartQty, onUpdateSellUnits: updateCartSellUnits, onRemove: removeFromCart, onBack: backToResults, internal: internal }))] }) }));
176
+ return (_jsx("div", { className: `app-wrapper${fullPage ? "" : " app-wrapper--embedded"}${hideHeader ? " app-wrapper--no-header" : ""}${!fullPage && stage === "processing" ? " app-wrapper--loading" : ""}`, style: getQuoteConfig().theme, children: _jsxs("main", { className: "main-content", children: [error && (_jsxs("div", { className: "error-box", role: "alert", children: [_jsx("strong", { children: "Error:" }), " ", error] })), stage === "entry" && (_jsx(EntryStage, { onPasteSchedule: handlePasteSchedule, onUploadSchedule: handleUploadSchedule, onEntryModeChange: setEntryMode })), stage === "results" && entryMode === "schedule" && scheduleResult && (_jsx(ScheduleResultsStage, { scheduleResult: scheduleResult, onStartOver: resetToEntry, cart: cart, onAddToCart: addToCart, onRemoveFromCart: removeFromCart, onGoToBasket: goToBasket, cartSeeded: cartSeeded, onCartSeeded: () => setCartSeeded(true), reviewGateComplete: scheduleReviewGateComplete, onReviewGateComplete: () => setScheduleReviewGateComplete(true), internal: internal })), stage === "basket" && (_jsx(BasketStage, { cart: cart, onUpdateQty: updateCartQty, onUpdateSellUnits: updateCartSellUnits, onRemove: removeFromCart, onBack: backToResults, onStartNewQuote: resetToEntry, internal: internal }))] }) }));
177
177
  }
@@ -6,7 +6,9 @@ interface BasketStageProps {
6
6
  onResolveConversions?: (conversions: Record<string, Partial<CartItem>>) => void;
7
7
  onRemove: (lineId: string) => void;
8
8
  onBack: () => void;
9
+ /** Clears the basket and returns to the start, from the quote-request confirmation page. */
10
+ onStartNewQuote: () => void;
9
11
  internal?: boolean;
10
12
  }
11
- export default function BasketStage({ cart, onUpdateQty, onUpdateSellUnits, onResolveConversions, onRemove, onBack, internal }: BasketStageProps): import("react").JSX.Element;
13
+ export default function BasketStage({ cart, onUpdateQty, onUpdateSellUnits, onResolveConversions, onRemove, onBack, onStartNewQuote, internal }: BasketStageProps): import("react").JSX.Element;
12
14
  export {};
@@ -34,7 +34,7 @@ function TrashIcon() {
34
34
  function ChevronIcon() {
35
35
  return (_jsx("svg", { width: "8", height: "5", viewBox: "0 0 8 5", fill: "none", children: _jsx("path", { d: "M1 4L4 1L7 4", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round", strokeLinejoin: "round" }) }));
36
36
  }
37
- export default function BasketStage({ cart, onUpdateQty, onUpdateSellUnits, onResolveConversions, onRemove, onBack, internal = false }) {
37
+ export default function BasketStage({ cart, onUpdateQty, onUpdateSellUnits, onResolveConversions, onRemove, onBack, onStartNewQuote, internal = false }) {
38
38
  // Both widgets can render a basket on the same page, so element ids are made unique.
39
39
  const uid = useId();
40
40
  const qzaBasketLayout = !internal && getBoqClientId() === "QZA6-MRA4";
@@ -171,6 +171,7 @@ export default function BasketStage({ cart, onUpdateQty, onUpdateSellUnits, onRe
171
171
  return;
172
172
  }
173
173
  setQuoteRequestSent(true);
174
+ window.scrollTo(0, 0);
174
175
  }
175
176
  catch {
176
177
  setQuoteContactError("Your request couldn't be sent. Check your connection and try again.");
@@ -192,6 +193,9 @@ export default function BasketStage({ cart, onUpdateQty, onUpdateSellUnits, onRe
192
193
  const totalPages = Math.max(1, Math.ceil(pricedItems.length / PAGE_SIZE));
193
194
  const currentPage = Math.min(page, totalPages);
194
195
  const pagedItems = pricedItems.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE);
196
+ if (quoteRequestSent) {
197
+ return (_jsx("div", { className: "wizard-step entry-stage", children: _jsxs("div", { className: "quote-request-sent", role: "status", children: [_jsx("div", { className: "quote-request-sent-icon", "aria-hidden": "true", children: _jsx("svg", { width: "40", height: "40", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("polyline", { points: "5 12.5 10 17.5 19 7" }) }) }), _jsx("h1", { children: "Quote requested" }), _jsxs("p", { children: ["You're all done. We'll email your sharper price to ", _jsx("strong", { children: reference.trim() }), ", usually within one working day."] }), _jsx("button", { type: "button", className: "cta-btn", onClick: onStartNewQuote, children: _jsx("span", { children: "Start a new quote" }) }), _jsx("p", { className: "quote-request-sent-note", children: "Nothing is charged until you approve the quote." })] }) }));
198
+ }
195
199
  return (_jsx("div", { className: `wizard-step entry-stage${qzaBasketLayout ? " basket-layout--qza6-mra4" : ""}`, children: _jsxs("div", { className: `quick-order-card${internal ? "" : " quick-order-card--customer"}`, children: [_jsxs("div", { className: `quick-order-hero${internal ? "" : " quick-order-hero--customer"}`, children: [internal && _jsx("div", { className: "quick-order-eyebrow", children: "Construction Services" }), _jsxs("div", { className: "basket-hero-copy", children: [qzaBasketLayout && (_jsxs("button", { type: "button", className: "cta-btn cta-btn-secondary basket-hero-back", onClick: onBack, children: [_jsx(BackArrow, {}), _jsx("span", { children: "Back to results" })] })), _jsxs("div", { className: "quick-order-hero-title-row", children: [_jsx("h1", { children: internal ? "Your Selection" : qzaBasketLayout ? "Request a quote" : "Your Basket" }), internal && (_jsxs("span", { className: "summary-chip summary-chip--scope", children: [_jsx("strong", { children: items.length }), " ", items.length === 1 ? "item" : "items"] }))] }), _jsx("p", { children: internal
196
200
  ? "Review the items you've added before exporting them for review."
197
201
  : qzaBasketLayout
@@ -220,7 +224,7 @@ export default function BasketStage({ cart, onUpdateQty, onUpdateSellUnits, onRe
220
224
  setReference(e.target.value);
221
225
  if (qzaBasketLayout)
222
226
  setQuoteContactError("");
223
- }, placeholder: qzaBasketLayout ? "you@example.com" : "PO number or job reference" }), qzaBasketLayout && quoteContactError && (_jsx("p", { className: "basket-quote-request-error", role: "alert", children: quoteContactError })), qzaBasketLayout ? (quoteRequestSent ? (_jsxs("div", { className: "basket-quote-request-success", role: "status", children: [_jsx("strong", { children: "Better price requested" }), _jsx("span", { children: "We'll use this email address to prepare your better price." })] })) : (_jsx("button", { type: "button", className: "cta-btn", onClick: () => void submitQuoteRequest(), disabled: quoteRequestSending, "aria-busy": quoteRequestSending, children: _jsx("span", { children: quoteRequestSending ? "Sending…" : "Request our best quote" }) }))) : (_jsx("button", { className: "cta-btn", onClick: () => void exportBasketExcel(items), "aria-label": internal ? "Download selection" : "Download basket", children: _jsx("span", { children: internal ? "Download Quote" : "Buy Now" }) })), _jsxs("p", { className: "basket-sidebar-disclaimer", children: [_jsx("span", { "aria-hidden": "true", children: "\u26A0\uFE0F" }), qzaBasketLayout
227
+ }, placeholder: qzaBasketLayout ? "you@example.com" : "PO number or job reference" }), qzaBasketLayout && quoteContactError && (_jsx("p", { className: "basket-quote-request-error", role: "alert", children: quoteContactError })), qzaBasketLayout ? (_jsx("button", { type: "button", className: "cta-btn", onClick: () => void submitQuoteRequest(), disabled: quoteRequestSending, "aria-busy": quoteRequestSending, children: _jsx("span", { children: quoteRequestSending ? "Sending…" : "Request our best quote" }) })) : (_jsx("button", { className: "cta-btn", onClick: () => void exportBasketExcel(items), "aria-label": internal ? "Download selection" : "Download basket", children: _jsx("span", { children: internal ? "Download Quote" : "Buy Now" }) })), _jsxs("p", { className: "basket-sidebar-disclaimer", children: [_jsx("span", { "aria-hidden": "true", children: "\u26A0\uFE0F" }), qzaBasketLayout
224
228
  ? "This is a quote request, not an order. We'll confirm our best price, availability and delivery before you approve."
225
229
  : "Prices and availability are estimates and should be confirmed before procurement."] })] })] }), _jsx("div", { className: "btn-row results-actions", children: _jsxs("button", { className: "cta-btn cta-btn-secondary", style: { flex: "0 1 30%", gap: 10, justifyContent: "center" }, onClick: onBack, "aria-label": "Back to results", children: [_jsx(BackArrow, {}), _jsx("span", { children: "Back to results" })] }) })] })) })] }) }));
226
230
  }