@asteby/storefront-react 0.11.1 → 0.12.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/checkout.css CHANGED
@@ -7,6 +7,158 @@
7
7
  * --sf-delivery-badge-bg / --sf-delivery-badge-fg
8
8
  * --sf-delivery-muted secondary text (default currentColor at 70%)
9
9
  */
10
+ /* Checkout fields (`CheckoutField`, `AddressFields`, `ContactFields`): label, hint and the error right under the field. */
11
+ [data-sf-field] {
12
+ display: grid;
13
+ gap: 0.25rem;
14
+ align-content: start;
15
+ min-width: 0;
16
+ }
17
+ [data-sf-field-optional],
18
+ [data-sf-field-hint] {
19
+ font-weight: 400;
20
+ color: var(--sf-delivery-muted, color-mix(in srgb, currentColor 72%, transparent));
21
+ }
22
+ [data-sf-field-hint] {
23
+ margin: 0;
24
+ font-size: 0.8125rem;
25
+ }
26
+ [data-sf-field-control] {
27
+ min-height: 44px;
28
+ width: 100%;
29
+ }
30
+ [data-sf-field-error] {
31
+ display: flex;
32
+ gap: 0.375rem;
33
+ align-items: flex-start;
34
+ margin: 0;
35
+ font-size: 0.875rem;
36
+ font-weight: 600;
37
+ color: var(--sf-field-error, #b42318);
38
+ }
39
+ [data-sf-field-error]::before {
40
+ content: '!';
41
+ display: inline-grid;
42
+ place-items: center;
43
+ flex: none;
44
+ width: 1.1em;
45
+ height: 1.1em;
46
+ margin-top: 0.15em;
47
+ border-radius: 999px;
48
+ background: var(--sf-field-error, #b42318);
49
+ color: var(--sf-field-error-contrast, #fff);
50
+ font-size: 0.8em;
51
+ line-height: 1;
52
+ }
53
+ [data-sf-error-summary] {
54
+ outline-offset: 2px;
55
+ }
56
+ [data-sf-error-summary] ul {
57
+ margin: 0.5rem 0 0;
58
+ padding-inline-start: 1.25rem;
59
+ }
60
+ [data-sf-error-summary] a {
61
+ color: inherit;
62
+ text-decoration: underline;
63
+ }
64
+ /* Order summary (`OrderSummary`, `OrderSummaryDisclosure`). */
65
+ [data-sf-summary-lines] {
66
+ display: grid;
67
+ gap: 0.75rem;
68
+ margin: 0;
69
+ padding: 0;
70
+ list-style: none;
71
+ }
72
+ [data-sf-summary-line] {
73
+ display: flex;
74
+ align-items: center;
75
+ gap: 0.75rem;
76
+ }
77
+ [data-sf-summary-media] {
78
+ position: relative;
79
+ flex: none;
80
+ width: 3.5rem;
81
+ height: 3.5rem;
82
+ }
83
+ [data-sf-summary-qty] {
84
+ position: absolute;
85
+ top: -0.4rem;
86
+ right: -0.4rem;
87
+ min-width: 1.35rem;
88
+ height: 1.35rem;
89
+ padding: 0 0.3rem;
90
+ border-radius: 999px;
91
+ display: inline-grid;
92
+ place-items: center;
93
+ font-size: 0.75rem;
94
+ font-weight: 700;
95
+ background: var(--sf-summary-qty-bg, #4a5878);
96
+ color: var(--sf-summary-qty-fg, #fff);
97
+ }
98
+ [data-sf-summary-line-body] {
99
+ display: grid;
100
+ flex: 1 1 auto;
101
+ min-width: 0;
102
+ overflow-wrap: anywhere;
103
+ }
104
+ [data-sf-summary-variant] {
105
+ font-size: 0.875em;
106
+ color: var(--sf-delivery-muted, color-mix(in srgb, currentColor 72%, transparent));
107
+ }
108
+ [data-sf-summary-line-price],
109
+ [data-sf-summary-totals] dd {
110
+ font-variant-numeric: tabular-nums;
111
+ text-align: right;
112
+ }
113
+ [data-sf-summary-totals] {
114
+ display: grid;
115
+ gap: 0.5rem;
116
+ margin: 1rem 0 0;
117
+ }
118
+ [data-sf-summary-totals] > div {
119
+ display: flex;
120
+ justify-content: space-between;
121
+ gap: 1rem;
122
+ }
123
+ [data-sf-summary-totals] dd {
124
+ margin: 0;
125
+ }
126
+ [data-sf-summary-totals] dd[data-updating] {
127
+ opacity: 0.6;
128
+ }
129
+ [data-sf-summary-toggle] {
130
+ display: flex;
131
+ align-items: center;
132
+ justify-content: space-between;
133
+ gap: 1rem;
134
+ min-height: 48px;
135
+ cursor: pointer;
136
+ list-style: none;
137
+ }
138
+ [data-sf-summary-toggle]::-webkit-details-marker {
139
+ display: none;
140
+ }
141
+ [data-sf-summary-toggle-label]::after {
142
+ content: '';
143
+ display: inline-block;
144
+ width: 0.45rem;
145
+ height: 0.45rem;
146
+ margin-inline-start: 0.5rem;
147
+ border-right: 2px solid currentColor;
148
+ border-bottom: 2px solid currentColor;
149
+ transform: translateY(-0.15rem) rotate(45deg);
150
+ }
151
+ [data-sf-summary-disclosure][open] [data-sf-summary-toggle-label]::after {
152
+ transform: translateY(0.1rem) rotate(-135deg);
153
+ }
154
+ [data-sf-summary-disclosure]:not([open]) [data-sf-summary-hide],
155
+ [data-sf-summary-disclosure][open] [data-sf-summary-show] {
156
+ display: none;
157
+ }
158
+ [data-sf-summary-toggle-total] {
159
+ font-weight: 700;
160
+ font-variant-numeric: tabular-nums;
161
+ }
10
162
  [data-sf-delivery] {
11
163
  display: grid;
12
164
  gap: 0.75rem;
@@ -18,6 +170,12 @@
18
170
  min-height: 44px;
19
171
  cursor: pointer;
20
172
  }
173
+ [data-sf-delivery-mark] {
174
+ display: flex;
175
+ flex: none;
176
+ justify-content: center;
177
+ width: 5.5rem;
178
+ }
21
179
  [data-sf-delivery-option][data-loading] {
22
180
  opacity: 0.6;
23
181
  transition: opacity 150ms;
@@ -98,3 +256,75 @@
98
256
  display: inline-block;
99
257
  vertical-align: middle;
100
258
  }
259
+ /* layout="recommended": the other rates under one toggle, grouped by carrier (the carrier name lives in the group title). */
260
+ [data-sf-delivery-more] > summary {
261
+ display: flex;
262
+ align-items: center;
263
+ gap: 0.5rem;
264
+ min-height: 44px;
265
+ cursor: pointer;
266
+ font-weight: 600;
267
+ list-style: none;
268
+ }
269
+ [data-sf-delivery-more] > summary::-webkit-details-marker {
270
+ display: none;
271
+ }
272
+ [data-sf-delivery-more] > summary::before {
273
+ content: '';
274
+ width: 0.5rem;
275
+ height: 0.5rem;
276
+ border-right: 2px solid currentColor;
277
+ border-bottom: 2px solid currentColor;
278
+ transform: rotate(-45deg);
279
+ transition: transform 150ms;
280
+ flex: none;
281
+ }
282
+ [data-sf-delivery-more][open] > summary::before {
283
+ transform: rotate(45deg);
284
+ }
285
+ [data-sf-delivery-groups] {
286
+ display: grid;
287
+ gap: 1rem;
288
+ margin-top: 0.5rem;
289
+ }
290
+ [data-sf-delivery-group] {
291
+ display: grid;
292
+ gap: 0.5rem;
293
+ }
294
+ [data-sf-delivery-group-title] {
295
+ display: flex;
296
+ align-items: center;
297
+ gap: 0.5rem;
298
+ margin: 0;
299
+ font-size: 0.875em;
300
+ font-weight: 600;
301
+ }
302
+ [data-sf-delivery-error] {
303
+ display: grid;
304
+ gap: 0.5rem;
305
+ justify-items: start;
306
+ }
307
+ [data-sf-delivery-error] p {
308
+ margin: 0;
309
+ }
310
+ [data-sf-delivery-retry] {
311
+ min-height: 44px;
312
+ cursor: pointer;
313
+ }
314
+ @media (prefers-reduced-motion: reduce) {
315
+ [data-sf-delivery-more] > summary::before {
316
+ transition: none;
317
+ }
318
+ }
319
+ @media (max-width: 30rem) {
320
+ /* Phones: a lower carrier mark leaves the service name room on one or two lines. */
321
+ [data-sf-delivery-option] {
322
+ gap: 0.625rem;
323
+ }
324
+ [data-sf-delivery-mark] {
325
+ width: 4.25rem;
326
+ }
327
+ [data-sf-delivery-mark] > [data-sf-brand-mark] {
328
+ zoom: 0.78;
329
+ }
330
+ }
@@ -1,6 +1,250 @@
1
1
  import * as react from 'react';
2
- import { ReactNode } from 'react';
3
- import { ShippingRate, PickupOption, Money } from '@asteby/storefront';
2
+ import { ReactNode, InputHTMLAttributes, SelectHTMLAttributes, RefObject } from 'react';
3
+ import { Address, CartLine, CartCost, Money, ShippingRate, PickupOption } from '@asteby/storefront';
4
+
5
+ type FieldMessages = {
6
+ valueMissing: string;
7
+ /** Required radio group (delivery, payment). */
8
+ choiceMissing: string;
9
+ email: string;
10
+ tel: string;
11
+ pattern: string;
12
+ /** `{n}` = minimum length. */
13
+ tooShort: string;
14
+ /** A field the server rejected. */
15
+ server: string;
16
+ /** Error summary title, `{n}` = number of fields. */
17
+ summaryOne: string;
18
+ summaryMany: string;
19
+ optional: string;
20
+ };
21
+ /** Messages for a locale (`es*` default, `en*`), with optional overrides. */
22
+ declare function fieldMessages(locale?: string | null, overrides?: Partial<FieldMessages> | null): FieldMessages;
23
+ /** Field name -> error text. */
24
+ type FieldErrors = Record<string, string>;
25
+ type ErrorSummaryItem = {
26
+ name: string;
27
+ id: string;
28
+ label: string;
29
+ message: string;
30
+ };
31
+ type Control = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
32
+ /**
33
+ * The error text for a control, from its validity. A control can bring its own text with `data-sf-error` (any
34
+ * failure) or `title` (pattern mismatch, the HTML convention).
35
+ */
36
+ declare function validationMessage(el: Control, m?: FieldMessages): string | null;
37
+ type UseFieldErrorsOptions = {
38
+ locale?: string;
39
+ messages?: Partial<FieldMessages>;
40
+ /** Fields the server rejected (e.g. from the action), shown with `serverMessages[name]` or the generic text. */
41
+ serverFields?: Iterable<string> | null;
42
+ serverMessages?: Record<string, string> | null;
43
+ };
44
+ type FieldErrorsState = {
45
+ errors: FieldErrors;
46
+ /** Non-null after a submit with problems (or with server errors): render it in `<ErrorSummary>`. */
47
+ summary: ErrorSummaryItem[] | null;
48
+ /** Changes every time a new summary appears, so `<ErrorSummary>` focuses it again. */
49
+ summaryKey: number;
50
+ clear: () => void;
51
+ };
52
+ /**
53
+ * Inline validation for the page `<form>`. Listens to the form (no wrapping component): leaving a field with a value
54
+ * checks it; typing in a field with an error clears it when fixed; a submit or `reportValidity()` with problems
55
+ * collects them (the browser's bubbles are replaced by the inline texts) for the summary.
56
+ */
57
+ declare function useFieldErrors(formRef: RefObject<HTMLFormElement | null>, options?: UseFieldErrorsOptions): FieldErrorsState;
58
+ type ErrorSummaryProps = {
59
+ items: readonly ErrorSummaryItem[] | null;
60
+ /** Changes when a new summary must take focus (`useFieldErrors().summaryKey`). */
61
+ focusKey?: number;
62
+ locale?: string;
63
+ messages?: Partial<FieldMessages>;
64
+ title?: ReactNode;
65
+ className?: string;
66
+ classNames?: Partial<Record<'title' | 'list' | 'link', string>>;
67
+ };
68
+ /**
69
+ * The list of problems after a submit, above the form: `role="alert"`, takes focus, one link per field (moves the
70
+ * focus to it). Renders nothing without items.
71
+ */
72
+ declare function ErrorSummary({ items, focusKey, locale, messages, title, className, classNames: cx }: ErrorSummaryProps): react.JSX.Element | null;
73
+ type CheckoutFieldClassNames = Partial<Record<'root' | 'label' | 'optional' | 'input' | 'hint' | 'error', string>>;
74
+ type BaseFieldProps = {
75
+ name: string;
76
+ label: string;
77
+ /** Field id. Default: the name. */
78
+ id?: string;
79
+ /** Error text (from `useFieldErrors().errors[name]` or the server). */
80
+ error?: string | null;
81
+ /** Help under the label ("we only use it for the delivery"). */
82
+ hint?: ReactNode;
83
+ /** Marks the label "(optional)" and drops `required`. */
84
+ optional?: boolean;
85
+ classNames?: CheckoutFieldClassNames;
86
+ locale?: string;
87
+ messages?: Partial<FieldMessages>;
88
+ };
89
+ type CheckoutFieldProps = BaseFieldProps & Omit<InputHTMLAttributes<HTMLInputElement>, 'name' | 'id'> & {
90
+ as?: 'input';
91
+ };
92
+ type CheckoutSelectProps = BaseFieldProps & Omit<SelectHTMLAttributes<HTMLSelectElement>, 'name' | 'id'> & {
93
+ as: 'select';
94
+ children: ReactNode;
95
+ };
96
+ /**
97
+ * One labelled field: label (with "(optional)"), hint, the control and its error text, wired with `aria-describedby`
98
+ * and `aria-invalid`. Required unless `optional`. `as="select"` renders a `<select>` with the given options.
99
+ */
100
+ declare function CheckoutField(props: CheckoutFieldProps | CheckoutSelectProps): react.JSX.Element;
101
+
102
+ type AddressFieldKey = 'zip' | 'province' | 'city' | 'neighborhood' | 'address1' | 'address2' | 'reference';
103
+ type ContactFieldKey = 'email' | 'phone' | 'first_name' | 'last_name';
104
+ type AddressFieldsMessages = Record<AddressFieldKey | ContactFieldKey, string> & {
105
+ provincePlaceholder: string;
106
+ address1Hint: string;
107
+ referenceHint: string;
108
+ emailHint: string;
109
+ phoneHint: string;
110
+ lookupFilled: string;
111
+ };
112
+ declare function addressFieldsMessages(locale?: string | null, overrides?: Partial<AddressFieldsMessages> | null): AddressFieldsMessages;
113
+ type FieldLayoutClassNames = CheckoutFieldClassNames & {
114
+ /** Extra class of a field that should span the whole row in a two-column grid. */
115
+ wide?: string;
116
+ /** Extra class of a field that fits half a row. */
117
+ half?: string;
118
+ };
119
+ type Common = {
120
+ /** Inline errors by field name (`useFieldErrors().errors`). */
121
+ errors?: FieldErrors;
122
+ locale?: string;
123
+ messages?: Partial<AddressFieldsMessages>;
124
+ fieldMessages?: Partial<FieldMessages>;
125
+ classNames?: FieldLayoutClassNames;
126
+ };
127
+ type ContactFieldsProps = Common & {
128
+ defaults?: Partial<Record<ContactFieldKey, string | null>>;
129
+ /** Form names. Default: the keys. */
130
+ names?: Partial<Record<ContactFieldKey, string>>;
131
+ /** Phone format from the shop or region (e.g. `{ pattern: '[0-9 +()-]{10,}', title: '10 dígitos' }`). */
132
+ phone?: {
133
+ pattern?: string;
134
+ title?: string;
135
+ minLength?: number;
136
+ };
137
+ /** Explain why e-mail and phone are needed (default true). */
138
+ hints?: boolean;
139
+ };
140
+ /** E-mail, phone, given and family name with their autofill tokens and the right mobile keyboards. */
141
+ declare function ContactFields({ defaults, names, errors, phone, hints, locale, messages, fieldMessages: fm, classNames: cx }: ContactFieldsProps): react.JSX.Element;
142
+ /** What a postal-code lookup answers. Every field optional: only what the source knows. */
143
+ type PostalCodeInfo = {
144
+ province?: string | null;
145
+ city?: string | null;
146
+ neighborhoods?: readonly string[] | null;
147
+ };
148
+ type AddressFieldsProps = Common & {
149
+ defaults?: Partial<Pick<Address, 'zip' | 'province' | 'city' | 'neighborhood' | 'address1' | 'address2' | 'reference'>> | null;
150
+ /** Form names. Default: the keys (the `AddressInput` fields of the Storefront API). */
151
+ names?: Partial<Record<AddressFieldKey, string>>;
152
+ /** ISO country of the address (hidden field `country_code`). */
153
+ countryCode: string;
154
+ /** States of the country (from the region config). With them, "State" is a `<select>`; the value sent is `name`. */
155
+ provinces?: ReadonlyArray<{
156
+ code?: string | null;
157
+ name: string;
158
+ }> | null;
159
+ /** Postal-code format of the country (from the region config), e.g. `{ pattern: '[0-9]{5}', maxLength: 5, inputMode: 'numeric', title: '5 dígitos' }`. */
160
+ postalCode?: {
161
+ pattern?: string;
162
+ maxLength?: number;
163
+ inputMode?: 'numeric' | 'text';
164
+ title?: string;
165
+ };
166
+ /**
167
+ * Fills state and city (and suggests colonias) from the postal code, when the shop has a source (a same-origin
168
+ * route over a postal-code catalog). Called once the code matches `postalCode.pattern` (or on leaving the field).
169
+ * Never overwrites what the buyer typed.
170
+ */
171
+ lookupPostalCode?: (zip: string) => Promise<PostalCodeInfo | null>;
172
+ /** Show the colonia field: `true` (default, optional), `'required'`, or `false`. */
173
+ neighborhood?: boolean | 'required';
174
+ /** Show the delivery references field (default true, optional). */
175
+ reference?: boolean;
176
+ /** Rendered after the fields (e.g. "save this address"). */
177
+ children?: ReactNode;
178
+ };
179
+ /**
180
+ * Shipping address in the order buyers fill it: postal code, state, city, colonia, street and number, interior and
181
+ * references. Autofill tokens: `postal-code`, `address-level1/2/3`, `address-line1/2`. The postal code uses the
182
+ * numeric keyboard when the country format is numeric.
183
+ */
184
+ declare function AddressFields(props: AddressFieldsProps): react.JSX.Element;
185
+
186
+ type OrderSummaryMessages = {
187
+ title: string;
188
+ subtotal: string;
189
+ discount: string;
190
+ shipping: string;
191
+ /** Shipping not chosen yet. */
192
+ shippingPending: string;
193
+ /** Shipping being saved / re-read. */
194
+ updating: string;
195
+ free: string;
196
+ taxes: string;
197
+ /** Taxes already inside the prices, `{amount}`. */
198
+ taxesIncluded: string;
199
+ total: string;
200
+ /** `{n}` = quantity. */
201
+ quantity: string;
202
+ show: string;
203
+ hide: string;
204
+ };
205
+ declare function orderSummaryMessages(locale?: string | null, overrides?: Partial<OrderSummaryMessages> | null): OrderSummaryMessages;
206
+ type OrderSummaryClassNames = Partial<Record<'root' | 'title' | 'lines' | 'line' | 'media' | 'qty' | 'lineTitle' | 'variant' | 'linePrice' | 'totals' | 'row' | 'discount' | 'muted' | 'free' | 'total' | 'note', string>>;
207
+ type OrderSummaryProps = {
208
+ lines: readonly CartLine[];
209
+ /** `cart.cost` as the server sends it. */
210
+ cost: CartCost;
211
+ /** The delivery is being saved and the cart re-read: shipping and total say so (`aria-busy`). */
212
+ updating?: boolean;
213
+ /** Picture of a line (e.g. `<ProductImage>`), 48-64 px. The quantity badge sits on it. */
214
+ renderLineMedia?: (line: CartLine) => ReactNode;
215
+ renderAmount?: (amount: Money) => ReactNode;
216
+ /** Heading. `false` hides it (the disclosure already names the block). */
217
+ title?: ReactNode | false;
218
+ /** Heading level. Default 2. */
219
+ headingLevel?: 2 | 3;
220
+ locale?: string;
221
+ messages?: Partial<OrderSummaryMessages>;
222
+ classNames?: OrderSummaryClassNames;
223
+ /** Rendered between the lines and the totals (e.g. a discount code form). */
224
+ children?: ReactNode;
225
+ };
226
+ /** Lines and totals of the order. The total row is `aria-live`, so a new total after choosing delivery is announced. */
227
+ declare function OrderSummary(props: OrderSummaryProps): react.JSX.Element;
228
+ type OrderSummaryDisclosureProps = {
229
+ /** Total shown on the collapsed bar (`cart.cost.total`). */
230
+ total: Money;
231
+ updating?: boolean;
232
+ renderAmount?: (amount: Money) => ReactNode;
233
+ locale?: string;
234
+ messages?: Partial<OrderSummaryMessages>;
235
+ /** Start open (default closed). */
236
+ defaultOpen?: boolean;
237
+ className?: string;
238
+ classNames?: Partial<Record<'summary' | 'label' | 'total' | 'panel', string>>;
239
+ /** Usually an `<OrderSummary title={false} … />`. */
240
+ children: ReactNode;
241
+ };
242
+ /**
243
+ * Phones: the order summary as a collapsed bar at the top ("Show order summary · $1,234.00"). Native `<details>`:
244
+ * works without JavaScript, the server HTML is already the final layout (no shift on hydration). Show it only on
245
+ * narrow screens and the full summary on wide ones (the shop's CSS).
246
+ */
247
+ declare function OrderSummaryDisclosure({ total, updating, renderAmount, locale: loc, messages, defaultOpen, className, classNames: cx, children }: OrderSummaryDisclosureProps): react.JSX.Element;
4
248
 
5
249
  type BrandMarkProps = {
6
250
  /** Provider identifier (`fedex`, `oxxo`, `mercadopago`, `7eleven`…). Unknown keys get a neutral monogram. */
@@ -107,6 +351,15 @@ type DeliveryMessages = {
107
351
  loading: string;
108
352
  /** The quote expired or changed and the rates were reloaded. */
109
353
  requoted: string;
354
+ /** Tag of the recommended rate that balances price and arrival time (`layout="recommended"`). */
355
+ bestValue: string;
356
+ /** Toggle of the other rates, `{n}` = how many. */
357
+ moreOptions: string;
358
+ /** Title of the group of rates without a carrier. */
359
+ otherCarriers: string;
360
+ /** The rates could not be quoted (`error`). */
361
+ error: string;
362
+ retry: string;
110
363
  };
111
364
  /** Messages for a locale (`es*` default, `en*`), with optional overrides. */
112
365
  declare function deliveryMessages(locale?: string | null, overrides?: Partial<DeliveryMessages> | null): DeliveryMessages;
@@ -159,7 +412,7 @@ declare function isQuoteChange(err: {
159
412
  */
160
413
  declare function matchRate<R extends Pick<ShippingRate, 'id' | 'carrier' | 'service_code' | 'title'>>(previous: Pick<ShippingRate, 'carrier' | 'service_code' | 'title'> | null | undefined, rates: readonly R[]): R | null;
161
414
 
162
- type DeliveryOptionsClassNames = Partial<Record<'root' | 'option' | 'radio' | 'mark' | 'body' | 'title' | 'carrier' | 'service' | 'eta' | 'badges' | 'badge' | 'price' | 'free' | 'note' | 'partial' | 'skeleton', string>>;
415
+ type DeliveryOptionsClassNames = Partial<Record<'root' | 'option' | 'radio' | 'mark' | 'body' | 'title' | 'carrier' | 'service' | 'eta' | 'badges' | 'badge' | 'price' | 'free' | 'note' | 'partial' | 'skeleton' | 'more' | 'moreSummary' | 'group' | 'groupTitle' | 'error' | 'retry', string>>;
163
416
  type DeliveryOptionsProps = {
164
417
  rates: readonly ShippingRate[];
165
418
  pickup?: readonly PickupOption[];
@@ -193,6 +446,21 @@ type DeliveryOptionsProps = {
193
446
  messages?: Partial<DeliveryMessages>;
194
447
  renderAmount?: (amount: Money) => ReactNode;
195
448
  classNames?: DeliveryOptionsClassNames;
449
+ /**
450
+ * `list` (default): every rate, as the server orders them. `recommended`: up to `recommendedCount` rates (cheapest,
451
+ * fastest and the best balance, tagged) and the rest under "Show more options (N)", grouped by carrier so the carrier
452
+ * name is not repeated on every row. Repeated services are dropped (the cheapest stays). Works without JavaScript
453
+ * (`<details>`); it opens by itself when the chosen rate is inside.
454
+ */
455
+ layout?: 'list' | 'recommended';
456
+ /** Rates recommended in `layout="recommended"`. Default 3. */
457
+ recommendedCount?: number;
458
+ /**
459
+ * The rates could not be quoted (provider down, timeout): one friendly alert instead of the list, with a retry button
460
+ * when `onRetry` is set. `true` uses the default text; a string replaces it.
461
+ */
462
+ error?: boolean | string;
463
+ onRetry?: () => void;
196
464
  /** `data-testid` of every radio. */
197
465
  testId?: string;
198
466
  /** Rows the shop renders before the rates (e.g. "home delivery, rates after the address"). */
@@ -209,4 +477,55 @@ type DeliveryOptionsProps = {
209
477
  */
210
478
  declare function DeliveryOptions(props: DeliveryOptionsProps): react.JSX.Element;
211
479
 
212
- export { type Brand, type BrandKind, BrandMark, type BrandMarkProps, type BrandMarkSource, type DeliveryMessages, DeliveryOptions, type DeliveryOptionsClassNames, type DeliveryOptionsProps, type QuoteNote, type RateBadge, brandKey, brandLogo, brandName, deliveryMessages, findBrand, isQuoteChange, listBrands, matchRate, quoteDeadline, quoteNote, rateBadges, rateCarrier, rateEta, rateService };
480
+ /**
481
+ * Which carrier rates to show first. Pure: no DOM, no amounts computed (costs are only compared, as the server sends
482
+ * them). A long quote (10+ services, several per carrier) becomes up to three recommended rows (the cheapest, the
483
+ * fastest and the best balance of both) plus the rest grouped by carrier, so the buyer compares three things instead of
484
+ * scanning a flat list.
485
+ */
486
+
487
+ /** Why a rate is recommended. `best_value` balances price and arrival time. */
488
+ type RateTag = 'cheapest' | 'fastest' | 'best_value';
489
+ type RecommendedRate<R extends ShippingRate = ShippingRate> = {
490
+ rate: R;
491
+ tags: RateTag[];
492
+ };
493
+ type RateGroup<R extends ShippingRate = ShippingRate> = {
494
+ /** Brand key of the carrier (`fedex`), or `''` for rates without one. */
495
+ key: string;
496
+ /** Carrier as the server sends it (first rate of the group), for `BrandMark` and the group name. */
497
+ carrier: string | null;
498
+ rates: R[];
499
+ };
500
+ type RecommendRatesOptions = {
501
+ /** How many rates to recommend. Default 3. */
502
+ count?: number;
503
+ /**
504
+ * With this many rates or fewer every rate is recommended (no "more options"): hiding one or two rates behind a
505
+ * toggle costs more than it saves. Default `count + 1`.
506
+ */
507
+ collapseAfter?: number;
508
+ /** Drop repeated services (same carrier and service, e.g. quoted by two providers), keeping the cheapest. Default true. */
509
+ dedupe?: boolean;
510
+ };
511
+ type RecommendedRates<R extends ShippingRate = ShippingRate> = {
512
+ recommended: RecommendedRate<R>[];
513
+ /** Everything else, grouped by carrier; groups ordered by their cheapest rate, rates by cost. */
514
+ more: RateGroup<R>[];
515
+ /** Number of rates in `more`. */
516
+ moreCount: number;
517
+ };
518
+ /** Repeated services (same carrier, same service code or title, same arrival): only the cheapest stays. Order kept. */
519
+ declare function dedupeRates<R extends ShippingRate>(rates: readonly R[]): R[];
520
+ /**
521
+ * Up to `count` recommended rates and the rest by carrier.
522
+ * - **cheapest**: lowest cost (only when every rate shares the currency); a tie goes to the faster one.
523
+ * - **fastest**: fewest business days; a tie goes to the cheaper one. Only when some rate is strictly faster than the
524
+ * cheapest, so the tag always tells the buyer something.
525
+ * - **best_value**: the remaining rate with the best combined position in price and in time (sum of ranks), a tie goes
526
+ * to the cheaper one. Fills the remaining slots.
527
+ * Recommended rows come back cheapest first. Pickup is not a rate and is never hidden (the component shows it apart).
528
+ */
529
+ declare function recommendRates<R extends ShippingRate>(input: readonly R[], options?: RecommendRatesOptions): RecommendedRates<R>;
530
+
531
+ export { type AddressFieldKey, AddressFields, type AddressFieldsMessages, type AddressFieldsProps, type Brand, type BrandKind, BrandMark, type BrandMarkProps, type BrandMarkSource, CheckoutField, type CheckoutFieldClassNames, type CheckoutFieldProps, type CheckoutSelectProps, type ContactFieldKey, ContactFields, type ContactFieldsProps, type DeliveryMessages, DeliveryOptions, type DeliveryOptionsClassNames, type DeliveryOptionsProps, ErrorSummary, type ErrorSummaryItem, type ErrorSummaryProps, type FieldErrors, type FieldErrorsState, type FieldLayoutClassNames, type FieldMessages, OrderSummary, type OrderSummaryClassNames, OrderSummaryDisclosure, type OrderSummaryDisclosureProps, type OrderSummaryMessages, type OrderSummaryProps, type PostalCodeInfo, type QuoteNote, type RateBadge, type RateGroup, type RateTag, type RecommendRatesOptions, type RecommendedRate, type RecommendedRates, type UseFieldErrorsOptions, addressFieldsMessages, brandKey, brandLogo, brandName, dedupeRates, deliveryMessages, fieldMessages, findBrand, isQuoteChange, listBrands, matchRate, orderSummaryMessages, quoteDeadline, quoteNote, rateBadges, rateCarrier, rateEta, rateService, recommendRates, useFieldErrors, validationMessage };