@usethrottle/quotes 0.1.0 → 0.2.0

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/dist/index.d.cts CHANGED
@@ -108,13 +108,36 @@ interface RequestFormConfig {
108
108
  companyName?: boolean;
109
109
  phone?: boolean;
110
110
  buyerReference?: boolean;
111
+ lineItems?: boolean;
112
+ targetPrice?: boolean;
111
113
  requireTurnstile?: boolean;
112
114
  };
113
115
  branding?: QuoteBranding | null;
114
116
  }
117
+ /**
118
+ * A line the buyer wants quoted.
119
+ *
120
+ * `targetUnitPrice` (minor units) is what they are hoping to pay. It is
121
+ * recorded as part of the request and shown to the rep; it never becomes the
122
+ * quoted price. Nothing sent from a browser can set what you charge.
123
+ */
124
+ interface QuoteRequestItem {
125
+ name: string;
126
+ sku?: string | null;
127
+ /** Your own product id, echoed back on the quote line. */
128
+ referenceId?: string | null;
129
+ quantity?: number;
130
+ /** The buyer's spec for this line — size, finish, delivery date. */
131
+ note?: string | null;
132
+ targetUnitPrice?: number | null;
133
+ imageUrl?: string | null;
134
+ }
115
135
  interface SubmitQuoteRequestInput {
116
136
  email: string;
117
- message: string;
137
+ /** Free text. Optional when `items` carries at least one line. */
138
+ message?: string | null;
139
+ /** Lines to quote. Optional when `message` is written. */
140
+ items?: QuoteRequestItem[];
118
141
  name?: string | null;
119
142
  companyName?: string | null;
120
143
  phone?: string | null;
@@ -257,6 +280,61 @@ declare class ThrottleQuotesError extends ThrottleError {
257
280
  get currentRevisionId(): string | undefined;
258
281
  }
259
282
 
283
+ /**
284
+ * A quote cart: the buyer's running list of things to be quoted, built up
285
+ * while they browse.
286
+ *
287
+ * Throttle stores no catalog, so the storefront owns product data and hands
288
+ * lines in — the same arrangement as the shopping cart. Nothing here is sent
289
+ * until `read()` is submitted through `QuotesClient.submitRequest`, and the
290
+ * amounts a storefront attaches are the buyer's TARGET price, never a price
291
+ * we will honour.
292
+ *
293
+ * Lines are keyed by `sku ?? referenceId ?? name`, so adding the same product
294
+ * twice increases its quantity instead of stacking duplicate rows.
295
+ */
296
+ interface QuoteCartLine extends QuoteRequestItem {
297
+ /** Stable identity for updates and removals. */
298
+ key: string;
299
+ quantity: number;
300
+ }
301
+ interface QuoteCartStorage {
302
+ getItem(key: string): string | null;
303
+ setItem(key: string, value: string): void;
304
+ removeItem(key: string): void;
305
+ }
306
+ interface QuoteCartOptions {
307
+ /** Namespacing key, so two storefronts on one origin cannot collide. */
308
+ storageKey?: string;
309
+ /**
310
+ * Where the cart survives page navigation. Defaults to `localStorage` in a
311
+ * browser and to memory everywhere else (SSR, tests) — a cart that throws
312
+ * during server rendering would take the whole page down.
313
+ */
314
+ storage?: QuoteCartStorage | null;
315
+ /** Refuse to grow past this many distinct lines. The API caps at 100. */
316
+ maxLines?: number;
317
+ }
318
+ interface QuoteCart {
319
+ /** Current lines, oldest first. */
320
+ read(): QuoteCartLine[];
321
+ /** Total quantity across lines — what a header badge should show. */
322
+ count(): number;
323
+ /** Add a line, merging quantities into an existing one with the same key. */
324
+ add(item: QuoteRequestItem): QuoteCartLine[];
325
+ /** Add several at once (a "quote this whole cart" button). */
326
+ addMany(items: QuoteRequestItem[]): QuoteCartLine[];
327
+ /** Change any field of a line. A quantity below 1 removes it. */
328
+ update(key: string, patch: Partial<Omit<QuoteCartLine, 'key'>>): QuoteCartLine[];
329
+ remove(key: string): QuoteCartLine[];
330
+ clear(): void;
331
+ /** Subscribe to every change. Returns an unsubscribe function. */
332
+ subscribe(listener: (lines: QuoteCartLine[]) => void): () => void;
333
+ /** The `items` payload for `QuotesClient.submitRequest`. */
334
+ toRequestItems(): QuoteRequestItem[];
335
+ }
336
+ declare function createQuoteCart(options?: QuoteCartOptions): QuoteCart;
337
+
260
338
  interface UseQuoteResult {
261
339
  quote: BuyerQuote | null;
262
340
  loading: boolean;
@@ -302,6 +380,28 @@ interface UseQuoteRequestFormResult {
302
380
  /** Loads a hosted RFQ form's config and submits it, with bot fields handled. */
303
381
  declare function useQuoteRequestForm(formToken: string, options?: QuotesClientOptions): UseQuoteRequestFormResult;
304
382
 
383
+ interface UseQuoteCartResult {
384
+ lines: QuoteCartLine[];
385
+ /** Total quantity across lines — for a header badge. */
386
+ count: number;
387
+ add: (item: QuoteRequestItem) => void;
388
+ addMany: (items: QuoteRequestItem[]) => void;
389
+ update: (key: string, patch: Partial<Omit<QuoteCartLine, 'key'>>) => void;
390
+ remove: (key: string) => void;
391
+ clear: () => void;
392
+ /** The underlying cart, for use outside React (an Add to Quote button in plain JS). */
393
+ cart: QuoteCart;
394
+ }
395
+ /**
396
+ * A quote cart the buyer fills while browsing, then submits as one request.
397
+ *
398
+ * The cart persists across page views, so an Add to Quote button on a product
399
+ * page and a Request quote page elsewhere on the site see the same lines.
400
+ * Pass the result of `toRequestItems()` (or `lines`) to
401
+ * `useQuoteRequestForm().submit` as `items`.
402
+ */
403
+ declare function useQuoteCart(options?: QuoteCartOptions): UseQuoteCartResult;
404
+
305
405
  interface QuoteViewProps {
306
406
  /** The `qtl_…` token from the buyer's link. */
307
407
  token: string;
@@ -337,6 +437,12 @@ interface QuoteRequestFormProps {
337
437
  * API fails the submission closed.
338
438
  */
339
439
  turnstileToken?: string;
440
+ /**
441
+ * Lines to submit with the request — typically `useQuoteCart().lines` after
442
+ * the buyer has been adding products while browsing. When present, the
443
+ * written message is optional.
444
+ */
445
+ items?: QuoteRequestItem[];
340
446
  onSubmitted?: () => void;
341
447
  loadingFallback?: ReactNode;
342
448
  className?: string;
@@ -349,6 +455,6 @@ interface QuoteRequestFormProps {
349
455
  * a success-shaped response and creates nothing, so `onSubmitted` fires either
350
456
  * way — by design, a bot must not learn it was caught.
351
457
  */
352
- declare function QuoteRequestForm({ formToken, options, turnstileToken, onSubmitted, loadingFallback, className, }: QuoteRequestFormProps): react_jsx_runtime.JSX.Element;
458
+ declare function QuoteRequestForm({ formToken, options, turnstileToken, items, onSubmitted, loadingFallback, className, }: QuoteRequestFormProps): react_jsx_runtime.JSX.Element;
353
459
 
354
- export { type AcceptQuoteInput, type AcceptQuoteResult, type BuyerQuote, type BuyerQuoteStatus, type ItemSelection, type PaymentTerms, type QuoteBranding, type QuoteComment, type QuoteItem, QuoteRequestForm, type QuoteRequestFormProps, type QuoteRevision, QuoteView, type QuoteViewProps, QuotesClient, type QuotesClientOptions, type RequestFormConfig, type SubmitQuoteRequestInput, type ThrottleEnvelope, ThrottleQuotesError, type UseQuoteRequestFormResult, type UseQuoteResult, computeSelectionTotal, createQuotesClient, formatMoney, scaleLineCharges, useQuote, useQuoteRequestForm };
460
+ export { type AcceptQuoteInput, type AcceptQuoteResult, type BuyerQuote, type BuyerQuoteStatus, type ItemSelection, type PaymentTerms, type QuoteBranding, type QuoteCart, type QuoteCartLine, type QuoteCartOptions, type QuoteCartStorage, type QuoteComment, type QuoteItem, QuoteRequestForm, type QuoteRequestFormProps, type QuoteRequestItem, type QuoteRevision, QuoteView, type QuoteViewProps, QuotesClient, type QuotesClientOptions, type RequestFormConfig, type SubmitQuoteRequestInput, type ThrottleEnvelope, ThrottleQuotesError, type UseQuoteCartResult, type UseQuoteRequestFormResult, type UseQuoteResult, computeSelectionTotal, createQuoteCart, createQuotesClient, formatMoney, scaleLineCharges, useQuote, useQuoteCart, useQuoteRequestForm };
package/dist/index.d.ts CHANGED
@@ -108,13 +108,36 @@ interface RequestFormConfig {
108
108
  companyName?: boolean;
109
109
  phone?: boolean;
110
110
  buyerReference?: boolean;
111
+ lineItems?: boolean;
112
+ targetPrice?: boolean;
111
113
  requireTurnstile?: boolean;
112
114
  };
113
115
  branding?: QuoteBranding | null;
114
116
  }
117
+ /**
118
+ * A line the buyer wants quoted.
119
+ *
120
+ * `targetUnitPrice` (minor units) is what they are hoping to pay. It is
121
+ * recorded as part of the request and shown to the rep; it never becomes the
122
+ * quoted price. Nothing sent from a browser can set what you charge.
123
+ */
124
+ interface QuoteRequestItem {
125
+ name: string;
126
+ sku?: string | null;
127
+ /** Your own product id, echoed back on the quote line. */
128
+ referenceId?: string | null;
129
+ quantity?: number;
130
+ /** The buyer's spec for this line — size, finish, delivery date. */
131
+ note?: string | null;
132
+ targetUnitPrice?: number | null;
133
+ imageUrl?: string | null;
134
+ }
115
135
  interface SubmitQuoteRequestInput {
116
136
  email: string;
117
- message: string;
137
+ /** Free text. Optional when `items` carries at least one line. */
138
+ message?: string | null;
139
+ /** Lines to quote. Optional when `message` is written. */
140
+ items?: QuoteRequestItem[];
118
141
  name?: string | null;
119
142
  companyName?: string | null;
120
143
  phone?: string | null;
@@ -257,6 +280,61 @@ declare class ThrottleQuotesError extends ThrottleError {
257
280
  get currentRevisionId(): string | undefined;
258
281
  }
259
282
 
283
+ /**
284
+ * A quote cart: the buyer's running list of things to be quoted, built up
285
+ * while they browse.
286
+ *
287
+ * Throttle stores no catalog, so the storefront owns product data and hands
288
+ * lines in — the same arrangement as the shopping cart. Nothing here is sent
289
+ * until `read()` is submitted through `QuotesClient.submitRequest`, and the
290
+ * amounts a storefront attaches are the buyer's TARGET price, never a price
291
+ * we will honour.
292
+ *
293
+ * Lines are keyed by `sku ?? referenceId ?? name`, so adding the same product
294
+ * twice increases its quantity instead of stacking duplicate rows.
295
+ */
296
+ interface QuoteCartLine extends QuoteRequestItem {
297
+ /** Stable identity for updates and removals. */
298
+ key: string;
299
+ quantity: number;
300
+ }
301
+ interface QuoteCartStorage {
302
+ getItem(key: string): string | null;
303
+ setItem(key: string, value: string): void;
304
+ removeItem(key: string): void;
305
+ }
306
+ interface QuoteCartOptions {
307
+ /** Namespacing key, so two storefronts on one origin cannot collide. */
308
+ storageKey?: string;
309
+ /**
310
+ * Where the cart survives page navigation. Defaults to `localStorage` in a
311
+ * browser and to memory everywhere else (SSR, tests) — a cart that throws
312
+ * during server rendering would take the whole page down.
313
+ */
314
+ storage?: QuoteCartStorage | null;
315
+ /** Refuse to grow past this many distinct lines. The API caps at 100. */
316
+ maxLines?: number;
317
+ }
318
+ interface QuoteCart {
319
+ /** Current lines, oldest first. */
320
+ read(): QuoteCartLine[];
321
+ /** Total quantity across lines — what a header badge should show. */
322
+ count(): number;
323
+ /** Add a line, merging quantities into an existing one with the same key. */
324
+ add(item: QuoteRequestItem): QuoteCartLine[];
325
+ /** Add several at once (a "quote this whole cart" button). */
326
+ addMany(items: QuoteRequestItem[]): QuoteCartLine[];
327
+ /** Change any field of a line. A quantity below 1 removes it. */
328
+ update(key: string, patch: Partial<Omit<QuoteCartLine, 'key'>>): QuoteCartLine[];
329
+ remove(key: string): QuoteCartLine[];
330
+ clear(): void;
331
+ /** Subscribe to every change. Returns an unsubscribe function. */
332
+ subscribe(listener: (lines: QuoteCartLine[]) => void): () => void;
333
+ /** The `items` payload for `QuotesClient.submitRequest`. */
334
+ toRequestItems(): QuoteRequestItem[];
335
+ }
336
+ declare function createQuoteCart(options?: QuoteCartOptions): QuoteCart;
337
+
260
338
  interface UseQuoteResult {
261
339
  quote: BuyerQuote | null;
262
340
  loading: boolean;
@@ -302,6 +380,28 @@ interface UseQuoteRequestFormResult {
302
380
  /** Loads a hosted RFQ form's config and submits it, with bot fields handled. */
303
381
  declare function useQuoteRequestForm(formToken: string, options?: QuotesClientOptions): UseQuoteRequestFormResult;
304
382
 
383
+ interface UseQuoteCartResult {
384
+ lines: QuoteCartLine[];
385
+ /** Total quantity across lines — for a header badge. */
386
+ count: number;
387
+ add: (item: QuoteRequestItem) => void;
388
+ addMany: (items: QuoteRequestItem[]) => void;
389
+ update: (key: string, patch: Partial<Omit<QuoteCartLine, 'key'>>) => void;
390
+ remove: (key: string) => void;
391
+ clear: () => void;
392
+ /** The underlying cart, for use outside React (an Add to Quote button in plain JS). */
393
+ cart: QuoteCart;
394
+ }
395
+ /**
396
+ * A quote cart the buyer fills while browsing, then submits as one request.
397
+ *
398
+ * The cart persists across page views, so an Add to Quote button on a product
399
+ * page and a Request quote page elsewhere on the site see the same lines.
400
+ * Pass the result of `toRequestItems()` (or `lines`) to
401
+ * `useQuoteRequestForm().submit` as `items`.
402
+ */
403
+ declare function useQuoteCart(options?: QuoteCartOptions): UseQuoteCartResult;
404
+
305
405
  interface QuoteViewProps {
306
406
  /** The `qtl_…` token from the buyer's link. */
307
407
  token: string;
@@ -337,6 +437,12 @@ interface QuoteRequestFormProps {
337
437
  * API fails the submission closed.
338
438
  */
339
439
  turnstileToken?: string;
440
+ /**
441
+ * Lines to submit with the request — typically `useQuoteCart().lines` after
442
+ * the buyer has been adding products while browsing. When present, the
443
+ * written message is optional.
444
+ */
445
+ items?: QuoteRequestItem[];
340
446
  onSubmitted?: () => void;
341
447
  loadingFallback?: ReactNode;
342
448
  className?: string;
@@ -349,6 +455,6 @@ interface QuoteRequestFormProps {
349
455
  * a success-shaped response and creates nothing, so `onSubmitted` fires either
350
456
  * way — by design, a bot must not learn it was caught.
351
457
  */
352
- declare function QuoteRequestForm({ formToken, options, turnstileToken, onSubmitted, loadingFallback, className, }: QuoteRequestFormProps): react_jsx_runtime.JSX.Element;
458
+ declare function QuoteRequestForm({ formToken, options, turnstileToken, items, onSubmitted, loadingFallback, className, }: QuoteRequestFormProps): react_jsx_runtime.JSX.Element;
353
459
 
354
- export { type AcceptQuoteInput, type AcceptQuoteResult, type BuyerQuote, type BuyerQuoteStatus, type ItemSelection, type PaymentTerms, type QuoteBranding, type QuoteComment, type QuoteItem, QuoteRequestForm, type QuoteRequestFormProps, type QuoteRevision, QuoteView, type QuoteViewProps, QuotesClient, type QuotesClientOptions, type RequestFormConfig, type SubmitQuoteRequestInput, type ThrottleEnvelope, ThrottleQuotesError, type UseQuoteRequestFormResult, type UseQuoteResult, computeSelectionTotal, createQuotesClient, formatMoney, scaleLineCharges, useQuote, useQuoteRequestForm };
460
+ export { type AcceptQuoteInput, type AcceptQuoteResult, type BuyerQuote, type BuyerQuoteStatus, type ItemSelection, type PaymentTerms, type QuoteBranding, type QuoteCart, type QuoteCartLine, type QuoteCartOptions, type QuoteCartStorage, type QuoteComment, type QuoteItem, QuoteRequestForm, type QuoteRequestFormProps, type QuoteRequestItem, type QuoteRevision, QuoteView, type QuoteViewProps, QuotesClient, type QuotesClientOptions, type RequestFormConfig, type SubmitQuoteRequestInput, type ThrottleEnvelope, ThrottleQuotesError, type UseQuoteCartResult, type UseQuoteRequestFormResult, type UseQuoteResult, computeSelectionTotal, createQuoteCart, createQuotesClient, formatMoney, scaleLineCharges, useQuote, useQuoteCart, useQuoteRequestForm };
package/dist/index.js CHANGED
@@ -161,6 +161,125 @@ function computeSelectionTotal(items, selections, shippingTotal = 0) {
161
161
 
162
162
  // src/hooks.ts
163
163
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
164
+
165
+ // src/quote-cart.ts
166
+ var DEFAULT_KEY = "throttle:quote-cart";
167
+ var DEFAULT_MAX_LINES = 100;
168
+ function memoryStorage() {
169
+ const map = /* @__PURE__ */ new Map();
170
+ return {
171
+ getItem: (key) => map.get(key) ?? null,
172
+ setItem: (key, value) => void map.set(key, value),
173
+ removeItem: (key) => void map.delete(key)
174
+ };
175
+ }
176
+ function defaultStorage() {
177
+ try {
178
+ if (typeof globalThis !== "undefined" && globalThis.localStorage) {
179
+ return globalThis.localStorage;
180
+ }
181
+ } catch {
182
+ }
183
+ return memoryStorage();
184
+ }
185
+ function lineKey(item) {
186
+ const raw = item.sku?.trim() || item.referenceId?.trim() || item.name.trim();
187
+ return raw.toLowerCase();
188
+ }
189
+ function normalizeQuantity(value) {
190
+ const n = Math.floor(Number(value ?? 1));
191
+ return Number.isFinite(n) && n > 0 ? n : 1;
192
+ }
193
+ function createQuoteCart(options = {}) {
194
+ const storageKey = options.storageKey ?? DEFAULT_KEY;
195
+ const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
196
+ const storage = options.storage === null ? memoryStorage() : options.storage ?? defaultStorage();
197
+ const listeners = /* @__PURE__ */ new Set();
198
+ function load() {
199
+ try {
200
+ const raw = storage.getItem(storageKey);
201
+ if (!raw) return [];
202
+ const parsed = JSON.parse(raw);
203
+ if (!Array.isArray(parsed)) return [];
204
+ return parsed.filter((line) => line && typeof line.name === "string" && line.name.trim()).map((line) => ({
205
+ ...line,
206
+ key: typeof line.key === "string" && line.key ? line.key : lineKey(line),
207
+ quantity: normalizeQuantity(line.quantity)
208
+ })).slice(0, maxLines);
209
+ } catch {
210
+ return [];
211
+ }
212
+ }
213
+ function save(lines) {
214
+ try {
215
+ storage.setItem(storageKey, JSON.stringify(lines));
216
+ } catch {
217
+ }
218
+ for (const listener of listeners) listener(lines);
219
+ return lines;
220
+ }
221
+ return {
222
+ read: load,
223
+ count() {
224
+ return load().reduce((sum, line) => sum + line.quantity, 0);
225
+ },
226
+ add(item) {
227
+ return this.addMany([item]);
228
+ },
229
+ addMany(items) {
230
+ const lines = load();
231
+ for (const item of items) {
232
+ if (!item?.name?.trim()) continue;
233
+ const key = lineKey(item);
234
+ const existing = lines.find((line) => line.key === key);
235
+ if (existing) {
236
+ existing.quantity += normalizeQuantity(item.quantity);
237
+ if (item.note) existing.note = item.note;
238
+ if (item.targetUnitPrice != null) existing.targetUnitPrice = item.targetUnitPrice;
239
+ continue;
240
+ }
241
+ if (lines.length >= maxLines) break;
242
+ lines.push({ ...item, key, quantity: normalizeQuantity(item.quantity) });
243
+ }
244
+ return save(lines);
245
+ },
246
+ update(key, patch) {
247
+ const lines = load();
248
+ const index = lines.findIndex((line) => line.key === key);
249
+ if (index === -1) return lines;
250
+ if (patch.quantity != null && Math.floor(Number(patch.quantity)) < 1) {
251
+ lines.splice(index, 1);
252
+ return save(lines);
253
+ }
254
+ lines[index] = {
255
+ ...lines[index],
256
+ ...patch,
257
+ key,
258
+ quantity: normalizeQuantity(patch.quantity ?? lines[index].quantity)
259
+ };
260
+ return save(lines);
261
+ },
262
+ remove(key) {
263
+ return save(load().filter((line) => line.key !== key));
264
+ },
265
+ clear() {
266
+ try {
267
+ storage.removeItem(storageKey);
268
+ } catch {
269
+ }
270
+ for (const listener of listeners) listener([]);
271
+ },
272
+ subscribe(listener) {
273
+ listeners.add(listener);
274
+ return () => void listeners.delete(listener);
275
+ },
276
+ toRequestItems() {
277
+ return load().map(({ key: _key, ...item }) => item);
278
+ }
279
+ };
280
+ }
281
+
282
+ // src/hooks.ts
164
283
  function useQuote(token, options = {}) {
165
284
  const client = useMemo(
166
285
  () => new QuotesClient(options),
@@ -353,6 +472,31 @@ function useQuoteRequestForm(formToken, options = {}) {
353
472
  );
354
473
  return { config, loading, error, submitting, submitted, submit, formLoadedAt };
355
474
  }
475
+ function useQuoteCart(options = {}) {
476
+ const cart = useMemo(
477
+ () => createQuoteCart(options),
478
+ // eslint-disable-next-line react-hooks/exhaustive-deps
479
+ [options.storageKey, options.maxLines]
480
+ );
481
+ const [lines, setLines] = useState([]);
482
+ useEffect(() => {
483
+ setLines(cart.read());
484
+ return cart.subscribe(setLines);
485
+ }, [cart]);
486
+ return {
487
+ lines,
488
+ count: lines.reduce((sum, line) => sum + line.quantity, 0),
489
+ add: useCallback((item) => void cart.add(item), [cart]),
490
+ addMany: useCallback((items) => void cart.addMany(items), [cart]),
491
+ update: useCallback(
492
+ (key, patch) => void cart.update(key, patch),
493
+ [cart]
494
+ ),
495
+ remove: useCallback((key) => void cart.remove(key), [cart]),
496
+ clear: useCallback(() => cart.clear(), [cart]),
497
+ cart
498
+ };
499
+ }
356
500
 
357
501
  // src/QuoteView.tsx
358
502
  import { useState as useState2 } from "react";
@@ -595,11 +739,13 @@ function QuoteRequestForm({
595
739
  formToken,
596
740
  options,
597
741
  turnstileToken,
742
+ items,
598
743
  onSubmitted,
599
744
  loadingFallback,
600
745
  className
601
746
  }) {
602
747
  const f = useQuoteRequestForm(formToken, options ?? {});
748
+ const lines = items ?? [];
603
749
  const [values, setValues] = useState3({
604
750
  email: "",
605
751
  name: "",
@@ -629,7 +775,8 @@ function QuoteRequestForm({
629
775
  e.preventDefault();
630
776
  void f.submit({
631
777
  email: values.email,
632
- message: values.message,
778
+ message: values.message || null,
779
+ ...lines.length > 0 ? { items: lines } : {},
633
780
  name: values.name || null,
634
781
  companyName: fields.companyName ? values.companyName || null : null,
635
782
  phone: fields.phone ? values.phone || null : null,
@@ -663,9 +810,23 @@ function QuoteRequestForm({
663
810
  "Your reference #",
664
811
  /* @__PURE__ */ jsx2("input", { value: values.buyerReference, onChange: set("buyerReference") })
665
812
  ] }) : null,
813
+ lines.length > 0 ? /* @__PURE__ */ jsx2("ul", { "aria-label": "Items on this request", children: lines.map((line, index) => /* @__PURE__ */ jsxs2("li", { children: [
814
+ line.quantity ?? 1,
815
+ " \xD7 ",
816
+ line.name,
817
+ line.sku ? ` (${line.sku})` : "",
818
+ line.note ? ` \u2014 ${line.note}` : ""
819
+ ] }, line.sku ?? line.referenceId ?? `${line.name}-${index}`)) }) : null,
666
820
  /* @__PURE__ */ jsxs2("label", { children: [
667
- "What do you need?",
668
- /* @__PURE__ */ jsx2("textarea", { value: values.message, onChange: set("message"), required: true })
821
+ lines.length > 0 ? "Anything else we should know?" : "What do you need?",
822
+ /* @__PURE__ */ jsx2(
823
+ "textarea",
824
+ {
825
+ value: values.message,
826
+ onChange: set("message"),
827
+ required: lines.length === 0
828
+ }
829
+ )
669
830
  ] }),
670
831
  /* @__PURE__ */ jsx2("div", { style: honeypotStyle, "aria-hidden": "true", children: /* @__PURE__ */ jsxs2("label", { children: [
671
832
  "Website",
@@ -680,7 +841,14 @@ function QuoteRequestForm({
680
841
  }
681
842
  )
682
843
  ] }) }),
683
- /* @__PURE__ */ jsx2("button", { type: "submit", disabled: f.submitting || !values.email || !values.message, children: f.submitting ? "Sending\u2026" : "Request a quote" })
844
+ /* @__PURE__ */ jsx2(
845
+ "button",
846
+ {
847
+ type: "submit",
848
+ disabled: f.submitting || !values.email || !values.message && lines.length === 0,
849
+ children: f.submitting ? "Sending\u2026" : "Request a quote"
850
+ }
851
+ )
684
852
  ]
685
853
  }
686
854
  );
@@ -691,10 +859,12 @@ export {
691
859
  QuotesClient,
692
860
  ThrottleQuotesError,
693
861
  computeSelectionTotal,
862
+ createQuoteCart,
694
863
  createQuotesClient,
695
864
  formatMoney,
696
865
  scaleLineCharges,
697
866
  useQuote,
867
+ useQuoteCart,
698
868
  useQuoteRequestForm
699
869
  };
700
870
  //# sourceMappingURL=index.js.map