@rebuy/rebuy 3.10.0 → 3.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/index.cjs CHANGED
@@ -1058,6 +1058,20 @@ var SmartCartTier = import_zod.z.looseObject({
1058
1058
  productAmountRemainingLabel: import_zod.z.boolean().optional(),
1059
1059
  productAmountRemainingLabelText: import_zod.z.string().optional(),
1060
1060
  products: import_zod.z.array(SmartCartTierProduct).optional(),
1061
+ /**
1062
+ * onsite `product_tier_element_size`: the step-row icon's square size as a px STRING (`'18px'` …
1063
+ * `'36px'` — the admin's five-value Size control, default `'18px'`). React stamps it onto the icon
1064
+ * SVG's `width`/`height` (`getProgressStepIconSize`), so the milestone renders at the merchant's
1065
+ * chosen size; the converter turns it into `CABProgressTier.iconSizePx` for the CAB renderer.
1066
+ *
1067
+ * `.catch(undefined)` for the same blast-radius reason as `productTierImage` below: this key was
1068
+ * previously unknown-and-ignored by the `looseObject`, and `SmartCartConfig` is parsed
1069
+ * `.nullish().catch(undefined)` with no intermediate catch, so one drifted value (a bare number, a
1070
+ * `'2rem'`) would drop EVERY progress bar for the shop rather than just this setting. Failing open to
1071
+ * `undefined` lands on the documented 18px default — strictly safer than that blast radius. The
1072
+ * converter parses the number out and clamps it, so a junk-but-string value degrades the same way.
1073
+ */
1074
+ productTierElementSize: import_zod.z.string().optional().catch(void 0),
1061
1075
  /**
1062
1076
  * onsite `product_tier_image`: on a product (gift) tier, show the gift's product image in the step-row
1063
1077
  * slot instead of the generic bag icon (React `TierProgressIcon` swaps in the image when this is set and
@@ -2703,6 +2717,25 @@ var CABProgressTier = import_zod24.z.object({
2703
2717
  title: import_zod24.z.string().optional(),
2704
2718
  variantIds: import_zod24.z.array(import_zod24.z.number().int().positive()).default(() => [])
2705
2719
  }).optional(),
2720
+ /**
2721
+ * The step-row icon's square size in PIXELS (onsite `product_tier_element_size`, an admin px string the
2722
+ * converter parses): 18 (default) / 20 / 24 / 30 / 36. React sizes the milestone by stamping this onto
2723
+ * the icon SVG's `width`/`height` (`getProgressStepIconSize`), so a merchant who deliberately enlarged
2724
+ * their milestones keeps that at checkout instead of every icon collapsing to one hardcoded square.
2725
+ *
2726
+ * A NUMBER, not the raw `'18px'` string: the renderer needs it as a length for the icon slot's grid
2727
+ * track, and a px-suffixed string would push unit parsing into every consumer. Deliberately NOT named
2728
+ * `iconSize` — the `iconSize` on an `image` section is the `s-icon` token enum, and one name covering both a
2729
+ * token and a pixel count is the kind of ambiguity that gets one passed where the other is meant.
2730
+ *
2731
+ * Bounded (not merely positive) because it is a raw merchant-controlled length: the step row is a grid
2732
+ * of per-tier cells, and an out-of-domain value would stretch the row rather than fail loudly. The
2733
+ * converter clamps to this same ceiling, so drifted config lands on a big-but-sane icon and never on a
2734
+ * tier the strict parse drops. `.default(18)` holds React's default at parse time (schema-first — the
2735
+ * renderer never spells a fallback), which also makes a tree authored before this field render at
2736
+ * React's size rather than the old hardcoded 20.
2737
+ */
2738
+ iconSizePx: import_zod24.z.number().int().min(1).max(64).default(18),
2706
2739
  /**
2707
2740
  * The step-row label at this tier's slot (onsite `getTierLabel`): the merchant custom label, else the
2708
2741
  * type default — 'Free Shipping' or '{n}% Discount'. A PRODUCT tier gets NO converter label: its name is
package/dist/index.mjs CHANGED
@@ -772,6 +772,20 @@ var SmartCartTier = z.looseObject({
772
772
  productAmountRemainingLabel: z.boolean().optional(),
773
773
  productAmountRemainingLabelText: z.string().optional(),
774
774
  products: z.array(SmartCartTierProduct).optional(),
775
+ /**
776
+ * onsite `product_tier_element_size`: the step-row icon's square size as a px STRING (`'18px'` …
777
+ * `'36px'` — the admin's five-value Size control, default `'18px'`). React stamps it onto the icon
778
+ * SVG's `width`/`height` (`getProgressStepIconSize`), so the milestone renders at the merchant's
779
+ * chosen size; the converter turns it into `CABProgressTier.iconSizePx` for the CAB renderer.
780
+ *
781
+ * `.catch(undefined)` for the same blast-radius reason as `productTierImage` below: this key was
782
+ * previously unknown-and-ignored by the `looseObject`, and `SmartCartConfig` is parsed
783
+ * `.nullish().catch(undefined)` with no intermediate catch, so one drifted value (a bare number, a
784
+ * `'2rem'`) would drop EVERY progress bar for the shop rather than just this setting. Failing open to
785
+ * `undefined` lands on the documented 18px default — strictly safer than that blast radius. The
786
+ * converter parses the number out and clamps it, so a junk-but-string value degrades the same way.
787
+ */
788
+ productTierElementSize: z.string().optional().catch(void 0),
775
789
  /**
776
790
  * onsite `product_tier_image`: on a product (gift) tier, show the gift's product image in the step-row
777
791
  * slot instead of the generic bag icon (React `TierProgressIcon` swaps in the image when this is set and
@@ -2417,6 +2431,25 @@ var CABProgressTier = z24.object({
2417
2431
  title: z24.string().optional(),
2418
2432
  variantIds: z24.array(z24.number().int().positive()).default(() => [])
2419
2433
  }).optional(),
2434
+ /**
2435
+ * The step-row icon's square size in PIXELS (onsite `product_tier_element_size`, an admin px string the
2436
+ * converter parses): 18 (default) / 20 / 24 / 30 / 36. React sizes the milestone by stamping this onto
2437
+ * the icon SVG's `width`/`height` (`getProgressStepIconSize`), so a merchant who deliberately enlarged
2438
+ * their milestones keeps that at checkout instead of every icon collapsing to one hardcoded square.
2439
+ *
2440
+ * A NUMBER, not the raw `'18px'` string: the renderer needs it as a length for the icon slot's grid
2441
+ * track, and a px-suffixed string would push unit parsing into every consumer. Deliberately NOT named
2442
+ * `iconSize` — the `iconSize` on an `image` section is the `s-icon` token enum, and one name covering both a
2443
+ * token and a pixel count is the kind of ambiguity that gets one passed where the other is meant.
2444
+ *
2445
+ * Bounded (not merely positive) because it is a raw merchant-controlled length: the step row is a grid
2446
+ * of per-tier cells, and an out-of-domain value would stretch the row rather than fail loudly. The
2447
+ * converter clamps to this same ceiling, so drifted config lands on a big-but-sane icon and never on a
2448
+ * tier the strict parse drops. `.default(18)` holds React's default at parse time (schema-first — the
2449
+ * renderer never spells a fallback), which also makes a tree authored before this field render at
2450
+ * React's size rather than the old hardcoded 20.
2451
+ */
2452
+ iconSizePx: z24.number().int().min(1).max(64).default(18),
2420
2453
  /**
2421
2454
  * The step-row label at this tier's slot (onsite `getTierLabel`): the merchant custom label, else the
2422
2455
  * type default — 'Free Shipping' or '{n}% Discount'. A PRODUCT tier gets NO converter label: its name is
@@ -144,6 +144,7 @@ export declare const CabUserConfig: z.ZodObject<{
144
144
  title: z.ZodOptional<z.ZodString>;
145
145
  variantIds: z.ZodOptional<z.ZodArray<z.ZodCoercedNumber<unknown>>>;
146
146
  }, z.core.$loose>>>;
147
+ productTierElementSize: z.ZodCatch<z.ZodOptional<z.ZodString>>;
147
148
  productTierImage: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
148
149
  type: z.ZodOptional<z.ZodString>;
149
150
  unlockConditionType: z.ZodOptional<z.ZodString>;
@@ -1272,6 +1272,25 @@ var CABProgressTier = import_zod14.z.object({
1272
1272
  title: import_zod14.z.string().optional(),
1273
1273
  variantIds: import_zod14.z.array(import_zod14.z.number().int().positive()).default(() => [])
1274
1274
  }).optional(),
1275
+ /**
1276
+ * The step-row icon's square size in PIXELS (onsite `product_tier_element_size`, an admin px string the
1277
+ * converter parses): 18 (default) / 20 / 24 / 30 / 36. React sizes the milestone by stamping this onto
1278
+ * the icon SVG's `width`/`height` (`getProgressStepIconSize`), so a merchant who deliberately enlarged
1279
+ * their milestones keeps that at checkout instead of every icon collapsing to one hardcoded square.
1280
+ *
1281
+ * A NUMBER, not the raw `'18px'` string: the renderer needs it as a length for the icon slot's grid
1282
+ * track, and a px-suffixed string would push unit parsing into every consumer. Deliberately NOT named
1283
+ * `iconSize` — the `iconSize` on an `image` section is the `s-icon` token enum, and one name covering both a
1284
+ * token and a pixel count is the kind of ambiguity that gets one passed where the other is meant.
1285
+ *
1286
+ * Bounded (not merely positive) because it is a raw merchant-controlled length: the step row is a grid
1287
+ * of per-tier cells, and an out-of-domain value would stretch the row rather than fail loudly. The
1288
+ * converter clamps to this same ceiling, so drifted config lands on a big-but-sane icon and never on a
1289
+ * tier the strict parse drops. `.default(18)` holds React's default at parse time (schema-first — the
1290
+ * renderer never spells a fallback), which also makes a tree authored before this field render at
1291
+ * React's size rather than the old hardcoded 20.
1292
+ */
1293
+ iconSizePx: import_zod14.z.number().int().min(1).max(64).default(18),
1275
1294
  /**
1276
1295
  * The step-row label at this tier's slot (onsite `getTierLabel`): the merchant custom label, else the
1277
1296
  * type default — 'Free Shipping' or '{n}% Discount'. A PRODUCT tier gets NO converter label: its name is
@@ -1075,6 +1075,25 @@ var CABProgressTier = z14.object({
1075
1075
  title: z14.string().optional(),
1076
1076
  variantIds: z14.array(z14.number().int().positive()).default(() => [])
1077
1077
  }).optional(),
1078
+ /**
1079
+ * The step-row icon's square size in PIXELS (onsite `product_tier_element_size`, an admin px string the
1080
+ * converter parses): 18 (default) / 20 / 24 / 30 / 36. React sizes the milestone by stamping this onto
1081
+ * the icon SVG's `width`/`height` (`getProgressStepIconSize`), so a merchant who deliberately enlarged
1082
+ * their milestones keeps that at checkout instead of every icon collapsing to one hardcoded square.
1083
+ *
1084
+ * A NUMBER, not the raw `'18px'` string: the renderer needs it as a length for the icon slot's grid
1085
+ * track, and a px-suffixed string would push unit parsing into every consumer. Deliberately NOT named
1086
+ * `iconSize` — the `iconSize` on an `image` section is the `s-icon` token enum, and one name covering both a
1087
+ * token and a pixel count is the kind of ambiguity that gets one passed where the other is meant.
1088
+ *
1089
+ * Bounded (not merely positive) because it is a raw merchant-controlled length: the step row is a grid
1090
+ * of per-tier cells, and an out-of-domain value would stretch the row rather than fail loudly. The
1091
+ * converter clamps to this same ceiling, so drifted config lands on a big-but-sane icon and never on a
1092
+ * tier the strict parse drops. `.default(18)` holds React's default at parse time (schema-first — the
1093
+ * renderer never spells a fallback), which also makes a tree authored before this field render at
1094
+ * React's size rather than the old hardcoded 20.
1095
+ */
1096
+ iconSizePx: z14.number().int().min(1).max(64).default(18),
1078
1097
  /**
1079
1098
  * The step-row label at this tier's slot (onsite `getTierLabel`): the merchant custom label, else the
1080
1099
  * type default — 'Free Shipping' or '{n}% Discount'. A PRODUCT tier gets NO converter label: its name is
@@ -59,6 +59,7 @@ export declare const SmartCartTier: z.ZodObject<{
59
59
  title: z.ZodOptional<z.ZodString>;
60
60
  variantIds: z.ZodOptional<z.ZodArray<z.ZodCoercedNumber<unknown>>>;
61
61
  }, z.core.$loose>>>;
62
+ productTierElementSize: z.ZodCatch<z.ZodOptional<z.ZodString>>;
62
63
  productTierImage: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
63
64
  type: z.ZodOptional<z.ZodString>;
64
65
  unlockConditionType: z.ZodOptional<z.ZodString>;
@@ -120,6 +121,7 @@ export declare const SmartCartBar: z.ZodObject<{
120
121
  title: z.ZodOptional<z.ZodString>;
121
122
  variantIds: z.ZodOptional<z.ZodArray<z.ZodCoercedNumber<unknown>>>;
122
123
  }, z.core.$loose>>>;
124
+ productTierElementSize: z.ZodCatch<z.ZodOptional<z.ZodString>>;
123
125
  productTierImage: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
124
126
  type: z.ZodOptional<z.ZodString>;
125
127
  unlockConditionType: z.ZodOptional<z.ZodString>;
@@ -180,6 +182,7 @@ export declare const SmartCartComponent: z.ZodObject<{
180
182
  title: z.ZodOptional<z.ZodString>;
181
183
  variantIds: z.ZodOptional<z.ZodArray<z.ZodCoercedNumber<unknown>>>;
182
184
  }, z.core.$loose>>>;
185
+ productTierElementSize: z.ZodCatch<z.ZodOptional<z.ZodString>>;
183
186
  productTierImage: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
184
187
  type: z.ZodOptional<z.ZodString>;
185
188
  unlockConditionType: z.ZodOptional<z.ZodString>;
@@ -251,6 +254,7 @@ export declare const SmartCartConfig: z.ZodObject<{
251
254
  title: z.ZodOptional<z.ZodString>;
252
255
  variantIds: z.ZodOptional<z.ZodArray<z.ZodCoercedNumber<unknown>>>;
253
256
  }, z.core.$loose>>>;
257
+ productTierElementSize: z.ZodCatch<z.ZodOptional<z.ZodString>>;
254
258
  productTierImage: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
255
259
  type: z.ZodOptional<z.ZodString>;
256
260
  unlockConditionType: z.ZodOptional<z.ZodString>;
@@ -151,6 +151,20 @@ var SmartCartTier = import_zod2.z.looseObject({
151
151
  productAmountRemainingLabel: import_zod2.z.boolean().optional(),
152
152
  productAmountRemainingLabelText: import_zod2.z.string().optional(),
153
153
  products: import_zod2.z.array(SmartCartTierProduct).optional(),
154
+ /**
155
+ * onsite `product_tier_element_size`: the step-row icon's square size as a px STRING (`'18px'` …
156
+ * `'36px'` — the admin's five-value Size control, default `'18px'`). React stamps it onto the icon
157
+ * SVG's `width`/`height` (`getProgressStepIconSize`), so the milestone renders at the merchant's
158
+ * chosen size; the converter turns it into `CABProgressTier.iconSizePx` for the CAB renderer.
159
+ *
160
+ * `.catch(undefined)` for the same blast-radius reason as `productTierImage` below: this key was
161
+ * previously unknown-and-ignored by the `looseObject`, and `SmartCartConfig` is parsed
162
+ * `.nullish().catch(undefined)` with no intermediate catch, so one drifted value (a bare number, a
163
+ * `'2rem'`) would drop EVERY progress bar for the shop rather than just this setting. Failing open to
164
+ * `undefined` lands on the documented 18px default — strictly safer than that blast radius. The
165
+ * converter parses the number out and clamps it, so a junk-but-string value degrades the same way.
166
+ */
167
+ productTierElementSize: import_zod2.z.string().optional().catch(void 0),
154
168
  /**
155
169
  * onsite `product_tier_image`: on a product (gift) tier, show the gift's product image in the step-row
156
170
  * slot instead of the generic bag icon (React `TierProgressIcon` swaps in the image when this is set and
@@ -127,6 +127,20 @@ var SmartCartTier = z2.looseObject({
127
127
  productAmountRemainingLabel: z2.boolean().optional(),
128
128
  productAmountRemainingLabelText: z2.string().optional(),
129
129
  products: z2.array(SmartCartTierProduct).optional(),
130
+ /**
131
+ * onsite `product_tier_element_size`: the step-row icon's square size as a px STRING (`'18px'` …
132
+ * `'36px'` — the admin's five-value Size control, default `'18px'`). React stamps it onto the icon
133
+ * SVG's `width`/`height` (`getProgressStepIconSize`), so the milestone renders at the merchant's
134
+ * chosen size; the converter turns it into `CABProgressTier.iconSizePx` for the CAB renderer.
135
+ *
136
+ * `.catch(undefined)` for the same blast-radius reason as `productTierImage` below: this key was
137
+ * previously unknown-and-ignored by the `looseObject`, and `SmartCartConfig` is parsed
138
+ * `.nullish().catch(undefined)` with no intermediate catch, so one drifted value (a bare number, a
139
+ * `'2rem'`) would drop EVERY progress bar for the shop rather than just this setting. Failing open to
140
+ * `undefined` lands on the documented 18px default — strictly safer than that blast radius. The
141
+ * converter parses the number out and clamps it, so a junk-but-string value degrades the same way.
142
+ */
143
+ productTierElementSize: z2.string().optional().catch(void 0),
130
144
  /**
131
145
  * onsite `product_tier_image`: on a product (gift) tier, show the gift's product image in the step-row
132
146
  * slot instead of the generic bag icon (React `TierProgressIcon` swaps in the image when this is set and
@@ -133,6 +133,7 @@ export declare const UserConfig: z.ZodObject<{
133
133
  title: z.ZodOptional<z.ZodString>;
134
134
  variantIds: z.ZodOptional<z.ZodArray<z.ZodCoercedNumber<unknown>>>;
135
135
  }, z.core.$loose>>>;
136
+ productTierElementSize: z.ZodCatch<z.ZodOptional<z.ZodString>>;
136
137
  productTierImage: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
137
138
  type: z.ZodOptional<z.ZodString>;
138
139
  unlockConditionType: z.ZodOptional<z.ZodString>;
@@ -1113,6 +1113,25 @@ var CABProgressTier = import_zod17.z.object({
1113
1113
  title: import_zod17.z.string().optional(),
1114
1114
  variantIds: import_zod17.z.array(import_zod17.z.number().int().positive()).default(() => [])
1115
1115
  }).optional(),
1116
+ /**
1117
+ * The step-row icon's square size in PIXELS (onsite `product_tier_element_size`, an admin px string the
1118
+ * converter parses): 18 (default) / 20 / 24 / 30 / 36. React sizes the milestone by stamping this onto
1119
+ * the icon SVG's `width`/`height` (`getProgressStepIconSize`), so a merchant who deliberately enlarged
1120
+ * their milestones keeps that at checkout instead of every icon collapsing to one hardcoded square.
1121
+ *
1122
+ * A NUMBER, not the raw `'18px'` string: the renderer needs it as a length for the icon slot's grid
1123
+ * track, and a px-suffixed string would push unit parsing into every consumer. Deliberately NOT named
1124
+ * `iconSize` — the `iconSize` on an `image` section is the `s-icon` token enum, and one name covering both a
1125
+ * token and a pixel count is the kind of ambiguity that gets one passed where the other is meant.
1126
+ *
1127
+ * Bounded (not merely positive) because it is a raw merchant-controlled length: the step row is a grid
1128
+ * of per-tier cells, and an out-of-domain value would stretch the row rather than fail loudly. The
1129
+ * converter clamps to this same ceiling, so drifted config lands on a big-but-sane icon and never on a
1130
+ * tier the strict parse drops. `.default(18)` holds React's default at parse time (schema-first — the
1131
+ * renderer never spells a fallback), which also makes a tree authored before this field render at
1132
+ * React's size rather than the old hardcoded 20.
1133
+ */
1134
+ iconSizePx: import_zod17.z.number().int().min(1).max(64).default(18),
1116
1135
  /**
1117
1136
  * The step-row label at this tier's slot (onsite `getTierLabel`): the merchant custom label, else the
1118
1137
  * type default — 'Free Shipping' or '{n}% Discount'. A PRODUCT tier gets NO converter label: its name is
@@ -1088,6 +1088,25 @@ var CABProgressTier = z17.object({
1088
1088
  title: z17.string().optional(),
1089
1089
  variantIds: z17.array(z17.number().int().positive()).default(() => [])
1090
1090
  }).optional(),
1091
+ /**
1092
+ * The step-row icon's square size in PIXELS (onsite `product_tier_element_size`, an admin px string the
1093
+ * converter parses): 18 (default) / 20 / 24 / 30 / 36. React sizes the milestone by stamping this onto
1094
+ * the icon SVG's `width`/`height` (`getProgressStepIconSize`), so a merchant who deliberately enlarged
1095
+ * their milestones keeps that at checkout instead of every icon collapsing to one hardcoded square.
1096
+ *
1097
+ * A NUMBER, not the raw `'18px'` string: the renderer needs it as a length for the icon slot's grid
1098
+ * track, and a px-suffixed string would push unit parsing into every consumer. Deliberately NOT named
1099
+ * `iconSize` — the `iconSize` on an `image` section is the `s-icon` token enum, and one name covering both a
1100
+ * token and a pixel count is the kind of ambiguity that gets one passed where the other is meant.
1101
+ *
1102
+ * Bounded (not merely positive) because it is a raw merchant-controlled length: the step row is a grid
1103
+ * of per-tier cells, and an out-of-domain value would stretch the row rather than fail loudly. The
1104
+ * converter clamps to this same ceiling, so drifted config lands on a big-but-sane icon and never on a
1105
+ * tier the strict parse drops. `.default(18)` holds React's default at parse time (schema-first — the
1106
+ * renderer never spells a fallback), which also makes a tree authored before this field render at
1107
+ * React's size rather than the old hardcoded 20.
1108
+ */
1109
+ iconSizePx: z17.number().int().min(1).max(64).default(18),
1091
1110
  /**
1092
1111
  * The step-row label at this tier's slot (onsite `getTierLabel`): the merchant custom label, else the
1093
1112
  * type default — 'Free Shipping' or '{n}% Discount'. A PRODUCT tier gets NO converter label: its name is
@@ -15,6 +15,7 @@ export declare const CABProgressTier: z.ZodObject<{
15
15
  title: z.ZodOptional<z.ZodString>;
16
16
  variantIds: z.ZodDefault<z.ZodArray<z.ZodNumber>>;
17
17
  }, z.core.$strip>>;
18
+ iconSizePx: z.ZodDefault<z.ZodNumber>;
18
19
  label: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
19
20
  productTierImage: z.ZodDefault<z.ZodBoolean>;
20
21
  threshold: z.ZodNumber;
@@ -334,6 +335,7 @@ export declare const CABProgressBarSection: z.ZodLazy<z.ZodObject<{
334
335
  title: z.ZodOptional<z.ZodString>;
335
336
  variantIds: z.ZodDefault<z.ZodArray<z.ZodNumber>>;
336
337
  }, z.core.$strip>>;
338
+ iconSizePx: z.ZodDefault<z.ZodNumber>;
337
339
  label: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
338
340
  productTierImage: z.ZodDefault<z.ZodBoolean>;
339
341
  threshold: z.ZodNumber;
@@ -3,8 +3,8 @@ import { type AdsEngineOffer } from '../server/adsEngine';
3
3
  import { type MonetizeInput } from '../server/shared';
4
4
  /**
5
5
  * Compose one ads-engine offer into a renderable CAB card: a bordered `layout` whose `viewUrl` is the
6
- * impression pixel, holding the header chrome, the copy (headline + HTML body, coerced to Tiptap by
7
- * the text-content schema on parse) beside the optional image (75%/fill — the React desktop split),
6
+ * impression pixel, holding the header chrome, the copy (headline + HTML body, pre-converted to a
7
+ * small-sized Tiptap doc by {@link smallText}) beside the optional image (75%/fill — the React desktop split),
8
8
  * the two CTAs in a 50/50 row, and the right-aligned Privacy Policy footer. `acceptOffer` links out
9
9
  * to the advertiser (`destinationUrl` = `click_url`, which self-tracks); `declineOffer` fires the
10
10
  * decline pixel (`trackingUrl`) and advances. Parsing through the schema fills defaults and validates
@@ -478,6 +478,20 @@ var SmartCartTier = import_zod5.z.looseObject({
478
478
  productAmountRemainingLabel: import_zod5.z.boolean().optional(),
479
479
  productAmountRemainingLabelText: import_zod5.z.string().optional(),
480
480
  products: import_zod5.z.array(SmartCartTierProduct).optional(),
481
+ /**
482
+ * onsite `product_tier_element_size`: the step-row icon's square size as a px STRING (`'18px'` …
483
+ * `'36px'` — the admin's five-value Size control, default `'18px'`). React stamps it onto the icon
484
+ * SVG's `width`/`height` (`getProgressStepIconSize`), so the milestone renders at the merchant's
485
+ * chosen size; the converter turns it into `CABProgressTier.iconSizePx` for the CAB renderer.
486
+ *
487
+ * `.catch(undefined)` for the same blast-radius reason as `productTierImage` below: this key was
488
+ * previously unknown-and-ignored by the `looseObject`, and `SmartCartConfig` is parsed
489
+ * `.nullish().catch(undefined)` with no intermediate catch, so one drifted value (a bare number, a
490
+ * `'2rem'`) would drop EVERY progress bar for the shop rather than just this setting. Failing open to
491
+ * `undefined` lands on the documented 18px default — strictly safer than that blast radius. The
492
+ * converter parses the number out and clamps it, so a junk-but-string value degrades the same way.
493
+ */
494
+ productTierElementSize: import_zod5.z.string().optional().catch(void 0),
481
495
  /**
482
496
  * onsite `product_tier_image`: on a product (gift) tier, show the gift's product image in the step-row
483
497
  * slot instead of the generic bag icon (React `TierProgressIcon` swaps in the image when this is set and
@@ -1657,6 +1671,25 @@ var CABProgressTier = import_zod21.z.object({
1657
1671
  title: import_zod21.z.string().optional(),
1658
1672
  variantIds: import_zod21.z.array(import_zod21.z.number().int().positive()).default(() => [])
1659
1673
  }).optional(),
1674
+ /**
1675
+ * The step-row icon's square size in PIXELS (onsite `product_tier_element_size`, an admin px string the
1676
+ * converter parses): 18 (default) / 20 / 24 / 30 / 36. React sizes the milestone by stamping this onto
1677
+ * the icon SVG's `width`/`height` (`getProgressStepIconSize`), so a merchant who deliberately enlarged
1678
+ * their milestones keeps that at checkout instead of every icon collapsing to one hardcoded square.
1679
+ *
1680
+ * A NUMBER, not the raw `'18px'` string: the renderer needs it as a length for the icon slot's grid
1681
+ * track, and a px-suffixed string would push unit parsing into every consumer. Deliberately NOT named
1682
+ * `iconSize` — the `iconSize` on an `image` section is the `s-icon` token enum, and one name covering both a
1683
+ * token and a pixel count is the kind of ambiguity that gets one passed where the other is meant.
1684
+ *
1685
+ * Bounded (not merely positive) because it is a raw merchant-controlled length: the step row is a grid
1686
+ * of per-tier cells, and an out-of-domain value would stretch the row rather than fail loudly. The
1687
+ * converter clamps to this same ceiling, so drifted config lands on a big-but-sane icon and never on a
1688
+ * tier the strict parse drops. `.default(18)` holds React's default at parse time (schema-first — the
1689
+ * renderer never spells a fallback), which also makes a tree authored before this field render at
1690
+ * React's size rather than the old hardcoded 20.
1691
+ */
1692
+ iconSizePx: import_zod21.z.number().int().min(1).max(64).default(18),
1660
1693
  /**
1661
1694
  * The step-row label at this tier's slot (onsite `getTierLabel`): the merchant custom label, else the
1662
1695
  * type default — 'Free Shipping' or '{n}% Discount'. A PRODUCT tier gets NO converter label: its name is
@@ -2386,12 +2419,21 @@ var NAMED_ENTITIES = {
2386
2419
  rsquo: "\u2019",
2387
2420
  trade: "\u2122"
2388
2421
  };
2389
- var decodeEntities = (input = "") => input.replace(/&(#x[\da-f]+|#\d+|[a-z][a-z\d]*);/gi, (match, code) => {
2390
- if (code[0] !== "#") return NAMED_ENTITIES[code.toLowerCase()] ?? match;
2391
- const isHex = /^#x/i.test(code);
2392
- const cp = parseInt(code.slice(isHex ? 2 : 1), isHex ? 16 : 10);
2393
- return Number.isInteger(cp) && cp >= 0 && cp <= 1114111 ? String.fromCodePoint(cp) : match;
2422
+ var decodeEntities = (input = "", { keepAngles = false } = {}) => input.replace(/&(#x[\da-f]+|#\d+|[a-z][a-z\d]*);/gi, (match, code) => {
2423
+ let decoded;
2424
+ if (code[0] !== "#") {
2425
+ decoded = NAMED_ENTITIES[code.toLowerCase()] ?? match;
2426
+ } else {
2427
+ const isHex = /^#x/i.test(code);
2428
+ const cp = parseInt(code.slice(isHex ? 2 : 1), isHex ? 16 : 10);
2429
+ decoded = Number.isInteger(cp) && cp >= 0 && cp <= 1114111 ? String.fromCodePoint(cp) : match;
2430
+ }
2431
+ return keepAngles && (decoded === "<" || decoded === ">") ? match : decoded;
2394
2432
  });
2433
+ var decodeTextEntities = (input = "") => {
2434
+ const decoded = decodeEntities(input);
2435
+ return isHTML(decoded) ? decodeEntities(input, { keepAngles: true }) : decoded;
2436
+ };
2395
2437
 
2396
2438
  // src/transforms/htmlToTiptap/parseAttribs.ts
2397
2439
  var ATTRIB_RE = /([a-zA-Z_:][\w:.-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'`=<>]+)))?/g;
@@ -2446,7 +2488,7 @@ var openTag = (stack, name, attrs) => {
2446
2488
  // src/transforms/htmlToTiptap/pushText.ts
2447
2489
  var pushText = (state, rawText) => {
2448
2490
  if (!rawText || isSkipping(state.stack)) return;
2449
- decodeEntities(rawText).split("\n").forEach((segment, index) => {
2491
+ decodeTextEntities(rawText).split("\n").forEach((segment, index) => {
2450
2492
  if (index > 0) flushParagraph(state);
2451
2493
  if (!segment) return;
2452
2494
  const marks = state.stack.flatMap((entry) => entry.marks);
@@ -2718,11 +2760,11 @@ var isMonetizeV1 = isWidgetV1(WidgetType.ui_extension_ad);
2718
2760
  var HTML_TAG_RE2 = /<[a-zA-Z]/;
2719
2761
  var containsHtml = (text2) => HTML_TAG_RE2.test(text2);
2720
2762
  var isObject = (x) => x !== null && typeof x === "object" && !Array.isArray(x);
2721
- var normalizeLangValue = (value) => {
2722
- if (typeof value !== "string") return value;
2763
+ var normalizeTextValue = (value, options = {}) => {
2723
2764
  const tokenized = normalizeTokens(value);
2724
- return containsHtml(tokenized) ? htmlToTiptap(tokenized) : tokenized;
2765
+ return containsHtml(tokenized) || options.defaultTextStyle ? htmlToTiptap(tokenized, options) : decodeTextEntities(tokenized);
2725
2766
  };
2767
+ var normalizeLangValue = (value) => typeof value === "string" ? normalizeTextValue(value) : value;
2726
2768
  var normalizeTextContent = (node) => {
2727
2769
  if (Array.isArray(node)) return node.map(normalizeTextContent);
2728
2770
  if (!isObject(node)) return node;
@@ -3521,6 +3563,7 @@ var feedbackDoc = (raw, tokens) => ({
3521
3563
  ],
3522
3564
  type: "doc"
3523
3565
  });
3566
+ var MAX_ICON_SIZE_PX = 64;
3524
3567
  var IN_SCOPE_TIER_TYPES = new Set(progressTierTypes);
3525
3568
  var inScope = (tier) => IN_SCOPE_TIER_TYPES.has(tier.type ?? "");
3526
3569
  var thresholdOf = (tier) => tier.unlockConditionType === "item_quantity" ? Math.ceil(Number(tier.minimum)) : Math.round(Number(tier.minimum) * 100);
@@ -3539,6 +3582,10 @@ var discountOf = (tier) => {
3539
3582
  const amount = tier.discountAmountFromTotal ?? 0;
3540
3583
  return tier.discountType === "fixedAmount" ? { amount: Math.round(amount * 100), type: "fixedAmount" } : { amount, type: "percentage" };
3541
3584
  };
3585
+ var iconSizeOf = (tier) => {
3586
+ const px = Math.round(Number.parseFloat(String(tier.productTierElementSize ?? "")));
3587
+ return Number.isFinite(px) && px > 0 ? Math.min(px, MAX_ICON_SIZE_PX) : void 0;
3588
+ };
3542
3589
  var giftOf = (tier) => {
3543
3590
  if (tier.type !== "product") return void 0;
3544
3591
  const product = tier.products?.[0] ?? tier.allProducts?.[0];
@@ -3555,6 +3602,7 @@ var giftOf = (tier) => {
3555
3602
  var toTierMeta = (tier) => {
3556
3603
  const discount = discountOf(tier);
3557
3604
  const gift = giftOf(tier);
3605
+ const iconSizePx = iconSizeOf(tier);
3558
3606
  const isItemCount = tier.unlockConditionType === "item_quantity";
3559
3607
  const label2 = tierLabel(tier);
3560
3608
  const type = tier.type;
@@ -3563,6 +3611,8 @@ var toTierMeta = (tier) => {
3563
3611
  /** Carry the OOS-filter flag only on gift tiers that set it — the client drops the tier when its gift is fully out of stock. */
3564
3612
  ...gift && tier.filterOosVariants && { filterOosVariants: true },
3565
3613
  ...gift && { gift },
3614
+ /** Carry the merchant's milestone icon size only when set — absent ⇒ the schema's 18px (React) default. */
3615
+ ...iconSizePx !== void 0 && { iconSizePx },
3566
3616
  ...label2 && { label: { en: label2 } },
3567
3617
  /** Carry the image-swap flag only on gift tiers that set it — the client shows the gift's product image in the step slot instead of the bag icon. */
3568
3618
  ...gift && tier.productTierImage && { productTierImage: true },
@@ -3990,6 +4040,10 @@ var label = (text2) => ({
3990
4040
  sectionType: SectionType.text
3991
4041
  });
3992
4042
  var text = (content) => ({ content: { en: content }, sectionType: SectionType.text });
4043
+ var smallText = (raw) => ({
4044
+ content: { en: normalizeTextValue(raw, { defaultTextStyle: { fontSize: "small" } }) },
4045
+ sectionType: SectionType.text
4046
+ });
3993
4047
  var styled = (value, { align, bold, color, fontSize, href }) => ({
3994
4048
  content: {
3995
4049
  en: {
@@ -4019,8 +4073,8 @@ var shopName = (shop) => shop.replace(/\.myshopify\.com$/, "") || "store";
4019
4073
  var headerSections = (input) => [
4020
4074
  {
4021
4075
  sections: [
4022
- styled("Congratulations!", { bold: true, fontSize: "large" }),
4023
- styled(`Your ${shopName(input.shop)} purchase unlocked a bonus:`, { bold: true, fontSize: "large" })
4076
+ styled("Congratulations!", { bold: true, fontSize: "heading" }),
4077
+ styled(`Your ${shopName(input.shop)} purchase unlocked a bonus:`, { bold: true, fontSize: "heading" })
4024
4078
  ],
4025
4079
  sectionType: SectionType.layout,
4026
4080
  spacing: Spacing.none
@@ -4029,7 +4083,7 @@ var headerSections = (input) => [
4029
4083
  ];
4030
4084
  var composeOfferCard = (offer, input) => {
4031
4085
  const copy = {
4032
- sections: [text(offer.headline), text(offer.description)],
4086
+ sections: [text(offer.headline), smallText(offer.description)],
4033
4087
  sectionType: SectionType.layout,
4034
4088
  spacing: Spacing.tight
4035
4089
  };
@@ -430,6 +430,20 @@ var SmartCartTier = z5.looseObject({
430
430
  productAmountRemainingLabel: z5.boolean().optional(),
431
431
  productAmountRemainingLabelText: z5.string().optional(),
432
432
  products: z5.array(SmartCartTierProduct).optional(),
433
+ /**
434
+ * onsite `product_tier_element_size`: the step-row icon's square size as a px STRING (`'18px'` …
435
+ * `'36px'` — the admin's five-value Size control, default `'18px'`). React stamps it onto the icon
436
+ * SVG's `width`/`height` (`getProgressStepIconSize`), so the milestone renders at the merchant's
437
+ * chosen size; the converter turns it into `CABProgressTier.iconSizePx` for the CAB renderer.
438
+ *
439
+ * `.catch(undefined)` for the same blast-radius reason as `productTierImage` below: this key was
440
+ * previously unknown-and-ignored by the `looseObject`, and `SmartCartConfig` is parsed
441
+ * `.nullish().catch(undefined)` with no intermediate catch, so one drifted value (a bare number, a
442
+ * `'2rem'`) would drop EVERY progress bar for the shop rather than just this setting. Failing open to
443
+ * `undefined` lands on the documented 18px default — strictly safer than that blast radius. The
444
+ * converter parses the number out and clamps it, so a junk-but-string value degrades the same way.
445
+ */
446
+ productTierElementSize: z5.string().optional().catch(void 0),
433
447
  /**
434
448
  * onsite `product_tier_image`: on a product (gift) tier, show the gift's product image in the step-row
435
449
  * slot instead of the generic bag icon (React `TierProgressIcon` swaps in the image when this is set and
@@ -1609,6 +1623,25 @@ var CABProgressTier = z21.object({
1609
1623
  title: z21.string().optional(),
1610
1624
  variantIds: z21.array(z21.number().int().positive()).default(() => [])
1611
1625
  }).optional(),
1626
+ /**
1627
+ * The step-row icon's square size in PIXELS (onsite `product_tier_element_size`, an admin px string the
1628
+ * converter parses): 18 (default) / 20 / 24 / 30 / 36. React sizes the milestone by stamping this onto
1629
+ * the icon SVG's `width`/`height` (`getProgressStepIconSize`), so a merchant who deliberately enlarged
1630
+ * their milestones keeps that at checkout instead of every icon collapsing to one hardcoded square.
1631
+ *
1632
+ * A NUMBER, not the raw `'18px'` string: the renderer needs it as a length for the icon slot's grid
1633
+ * track, and a px-suffixed string would push unit parsing into every consumer. Deliberately NOT named
1634
+ * `iconSize` — the `iconSize` on an `image` section is the `s-icon` token enum, and one name covering both a
1635
+ * token and a pixel count is the kind of ambiguity that gets one passed where the other is meant.
1636
+ *
1637
+ * Bounded (not merely positive) because it is a raw merchant-controlled length: the step row is a grid
1638
+ * of per-tier cells, and an out-of-domain value would stretch the row rather than fail loudly. The
1639
+ * converter clamps to this same ceiling, so drifted config lands on a big-but-sane icon and never on a
1640
+ * tier the strict parse drops. `.default(18)` holds React's default at parse time (schema-first — the
1641
+ * renderer never spells a fallback), which also makes a tree authored before this field render at
1642
+ * React's size rather than the old hardcoded 20.
1643
+ */
1644
+ iconSizePx: z21.number().int().min(1).max(64).default(18),
1612
1645
  /**
1613
1646
  * The step-row label at this tier's slot (onsite `getTierLabel`): the merchant custom label, else the
1614
1647
  * type default — 'Free Shipping' or '{n}% Discount'. A PRODUCT tier gets NO converter label: its name is
@@ -2338,12 +2371,21 @@ var NAMED_ENTITIES = {
2338
2371
  rsquo: "\u2019",
2339
2372
  trade: "\u2122"
2340
2373
  };
2341
- var decodeEntities = (input = "") => input.replace(/&(#x[\da-f]+|#\d+|[a-z][a-z\d]*);/gi, (match, code) => {
2342
- if (code[0] !== "#") return NAMED_ENTITIES[code.toLowerCase()] ?? match;
2343
- const isHex = /^#x/i.test(code);
2344
- const cp = parseInt(code.slice(isHex ? 2 : 1), isHex ? 16 : 10);
2345
- return Number.isInteger(cp) && cp >= 0 && cp <= 1114111 ? String.fromCodePoint(cp) : match;
2374
+ var decodeEntities = (input = "", { keepAngles = false } = {}) => input.replace(/&(#x[\da-f]+|#\d+|[a-z][a-z\d]*);/gi, (match, code) => {
2375
+ let decoded;
2376
+ if (code[0] !== "#") {
2377
+ decoded = NAMED_ENTITIES[code.toLowerCase()] ?? match;
2378
+ } else {
2379
+ const isHex = /^#x/i.test(code);
2380
+ const cp = parseInt(code.slice(isHex ? 2 : 1), isHex ? 16 : 10);
2381
+ decoded = Number.isInteger(cp) && cp >= 0 && cp <= 1114111 ? String.fromCodePoint(cp) : match;
2382
+ }
2383
+ return keepAngles && (decoded === "<" || decoded === ">") ? match : decoded;
2346
2384
  });
2385
+ var decodeTextEntities = (input = "") => {
2386
+ const decoded = decodeEntities(input);
2387
+ return isHTML(decoded) ? decodeEntities(input, { keepAngles: true }) : decoded;
2388
+ };
2347
2389
 
2348
2390
  // src/transforms/htmlToTiptap/parseAttribs.ts
2349
2391
  var ATTRIB_RE = /([a-zA-Z_:][\w:.-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'`=<>]+)))?/g;
@@ -2398,7 +2440,7 @@ var openTag = (stack, name, attrs) => {
2398
2440
  // src/transforms/htmlToTiptap/pushText.ts
2399
2441
  var pushText = (state, rawText) => {
2400
2442
  if (!rawText || isSkipping(state.stack)) return;
2401
- decodeEntities(rawText).split("\n").forEach((segment, index) => {
2443
+ decodeTextEntities(rawText).split("\n").forEach((segment, index) => {
2402
2444
  if (index > 0) flushParagraph(state);
2403
2445
  if (!segment) return;
2404
2446
  const marks = state.stack.flatMap((entry) => entry.marks);
@@ -2670,11 +2712,11 @@ var isMonetizeV1 = isWidgetV1(WidgetType.ui_extension_ad);
2670
2712
  var HTML_TAG_RE2 = /<[a-zA-Z]/;
2671
2713
  var containsHtml = (text2) => HTML_TAG_RE2.test(text2);
2672
2714
  var isObject = (x) => x !== null && typeof x === "object" && !Array.isArray(x);
2673
- var normalizeLangValue = (value) => {
2674
- if (typeof value !== "string") return value;
2715
+ var normalizeTextValue = (value, options = {}) => {
2675
2716
  const tokenized = normalizeTokens(value);
2676
- return containsHtml(tokenized) ? htmlToTiptap(tokenized) : tokenized;
2717
+ return containsHtml(tokenized) || options.defaultTextStyle ? htmlToTiptap(tokenized, options) : decodeTextEntities(tokenized);
2677
2718
  };
2719
+ var normalizeLangValue = (value) => typeof value === "string" ? normalizeTextValue(value) : value;
2678
2720
  var normalizeTextContent = (node) => {
2679
2721
  if (Array.isArray(node)) return node.map(normalizeTextContent);
2680
2722
  if (!isObject(node)) return node;
@@ -3473,6 +3515,7 @@ var feedbackDoc = (raw, tokens) => ({
3473
3515
  ],
3474
3516
  type: "doc"
3475
3517
  });
3518
+ var MAX_ICON_SIZE_PX = 64;
3476
3519
  var IN_SCOPE_TIER_TYPES = new Set(progressTierTypes);
3477
3520
  var inScope = (tier) => IN_SCOPE_TIER_TYPES.has(tier.type ?? "");
3478
3521
  var thresholdOf = (tier) => tier.unlockConditionType === "item_quantity" ? Math.ceil(Number(tier.minimum)) : Math.round(Number(tier.minimum) * 100);
@@ -3491,6 +3534,10 @@ var discountOf = (tier) => {
3491
3534
  const amount = tier.discountAmountFromTotal ?? 0;
3492
3535
  return tier.discountType === "fixedAmount" ? { amount: Math.round(amount * 100), type: "fixedAmount" } : { amount, type: "percentage" };
3493
3536
  };
3537
+ var iconSizeOf = (tier) => {
3538
+ const px = Math.round(Number.parseFloat(String(tier.productTierElementSize ?? "")));
3539
+ return Number.isFinite(px) && px > 0 ? Math.min(px, MAX_ICON_SIZE_PX) : void 0;
3540
+ };
3494
3541
  var giftOf = (tier) => {
3495
3542
  if (tier.type !== "product") return void 0;
3496
3543
  const product = tier.products?.[0] ?? tier.allProducts?.[0];
@@ -3507,6 +3554,7 @@ var giftOf = (tier) => {
3507
3554
  var toTierMeta = (tier) => {
3508
3555
  const discount = discountOf(tier);
3509
3556
  const gift = giftOf(tier);
3557
+ const iconSizePx = iconSizeOf(tier);
3510
3558
  const isItemCount = tier.unlockConditionType === "item_quantity";
3511
3559
  const label2 = tierLabel(tier);
3512
3560
  const type = tier.type;
@@ -3515,6 +3563,8 @@ var toTierMeta = (tier) => {
3515
3563
  /** Carry the OOS-filter flag only on gift tiers that set it — the client drops the tier when its gift is fully out of stock. */
3516
3564
  ...gift && tier.filterOosVariants && { filterOosVariants: true },
3517
3565
  ...gift && { gift },
3566
+ /** Carry the merchant's milestone icon size only when set — absent ⇒ the schema's 18px (React) default. */
3567
+ ...iconSizePx !== void 0 && { iconSizePx },
3518
3568
  ...label2 && { label: { en: label2 } },
3519
3569
  /** Carry the image-swap flag only on gift tiers that set it — the client shows the gift's product image in the step slot instead of the bag icon. */
3520
3570
  ...gift && tier.productTierImage && { productTierImage: true },
@@ -3942,6 +3992,10 @@ var label = (text2) => ({
3942
3992
  sectionType: SectionType.text
3943
3993
  });
3944
3994
  var text = (content) => ({ content: { en: content }, sectionType: SectionType.text });
3995
+ var smallText = (raw) => ({
3996
+ content: { en: normalizeTextValue(raw, { defaultTextStyle: { fontSize: "small" } }) },
3997
+ sectionType: SectionType.text
3998
+ });
3945
3999
  var styled = (value, { align, bold, color, fontSize, href }) => ({
3946
4000
  content: {
3947
4001
  en: {
@@ -3971,8 +4025,8 @@ var shopName = (shop) => shop.replace(/\.myshopify\.com$/, "") || "store";
3971
4025
  var headerSections = (input) => [
3972
4026
  {
3973
4027
  sections: [
3974
- styled("Congratulations!", { bold: true, fontSize: "large" }),
3975
- styled(`Your ${shopName(input.shop)} purchase unlocked a bonus:`, { bold: true, fontSize: "large" })
4028
+ styled("Congratulations!", { bold: true, fontSize: "heading" }),
4029
+ styled(`Your ${shopName(input.shop)} purchase unlocked a bonus:`, { bold: true, fontSize: "heading" })
3976
4030
  ],
3977
4031
  sectionType: SectionType.layout,
3978
4032
  spacing: Spacing.none
@@ -3981,7 +4035,7 @@ var headerSections = (input) => [
3981
4035
  ];
3982
4036
  var composeOfferCard = (offer, input) => {
3983
4037
  const copy = {
3984
- sections: [text(offer.headline), text(offer.description)],
4038
+ sections: [text(offer.headline), smallText(offer.description)],
3985
4039
  sectionType: SectionType.layout,
3986
4040
  spacing: Spacing.tight
3987
4041
  };
@@ -1 +1,19 @@
1
- export declare const decodeEntities: (input?: string) => string;
1
+ export type DecodeEntitiesOptions = {
2
+ /**
3
+ * Leave every entity that would decode to `<` or `>` (named, decimal or hex, any case) as its
4
+ * literal source text. Decided on the DECODED character, not the spelling, so `&lt;`, `&#60;`
5
+ * and `&#x3C;` are one rule — there is no second list of angle spellings to keep in sync.
6
+ */
7
+ keepAngles?: boolean;
8
+ };
9
+ export declare const decodeEntities: (input?: string, { keepAngles }?: DecodeEntitiesOptions) => string;
10
+ /**
11
+ * The entity decode for DISPLAY text — every text run and plain-string content value the CAB
12
+ * pipeline produces. Fully decodes ordinary entities (`&amp;` → `&`, `5 &lt; 10` → `5 < 10`), but
13
+ * when the fully-decoded result is tag-shaped (`&lt;b&gt;` → `<b>`) it re-decodes holding the
14
+ * angle entities, because `CABTextSection.content` carries a `NO_HTML` refine that rejects
15
+ * tag-shaped text and would otherwise drop the whole section — silent content loss, which is worse
16
+ * than showing the escaped literal. Uses the refine's own `isHTML` predicate so guard and gate can't
17
+ * drift. Single-pass by construction: a double-escaped `&amp;lt;` resolves to literal `&lt;` and stops.
18
+ */
19
+ export declare const decodeTextEntities: (input?: string) => string;
@@ -1108,6 +1108,25 @@ var CABProgressTier = import_zod15.z.object({
1108
1108
  title: import_zod15.z.string().optional(),
1109
1109
  variantIds: import_zod15.z.array(import_zod15.z.number().int().positive()).default(() => [])
1110
1110
  }).optional(),
1111
+ /**
1112
+ * The step-row icon's square size in PIXELS (onsite `product_tier_element_size`, an admin px string the
1113
+ * converter parses): 18 (default) / 20 / 24 / 30 / 36. React sizes the milestone by stamping this onto
1114
+ * the icon SVG's `width`/`height` (`getProgressStepIconSize`), so a merchant who deliberately enlarged
1115
+ * their milestones keeps that at checkout instead of every icon collapsing to one hardcoded square.
1116
+ *
1117
+ * A NUMBER, not the raw `'18px'` string: the renderer needs it as a length for the icon slot's grid
1118
+ * track, and a px-suffixed string would push unit parsing into every consumer. Deliberately NOT named
1119
+ * `iconSize` — the `iconSize` on an `image` section is the `s-icon` token enum, and one name covering both a
1120
+ * token and a pixel count is the kind of ambiguity that gets one passed where the other is meant.
1121
+ *
1122
+ * Bounded (not merely positive) because it is a raw merchant-controlled length: the step row is a grid
1123
+ * of per-tier cells, and an out-of-domain value would stretch the row rather than fail loudly. The
1124
+ * converter clamps to this same ceiling, so drifted config lands on a big-but-sane icon and never on a
1125
+ * tier the strict parse drops. `.default(18)` holds React's default at parse time (schema-first — the
1126
+ * renderer never spells a fallback), which also makes a tree authored before this field render at
1127
+ * React's size rather than the old hardcoded 20.
1128
+ */
1129
+ iconSizePx: import_zod15.z.number().int().min(1).max(64).default(18),
1111
1130
  /**
1112
1131
  * The step-row label at this tier's slot (onsite `getTierLabel`): the merchant custom label, else the
1113
1132
  * type default — 'Free Shipping' or '{n}% Discount'. A PRODUCT tier gets NO converter label: its name is
@@ -1837,12 +1856,21 @@ var NAMED_ENTITIES = {
1837
1856
  rsquo: "\u2019",
1838
1857
  trade: "\u2122"
1839
1858
  };
1840
- var decodeEntities = (input = "") => input.replace(/&(#x[\da-f]+|#\d+|[a-z][a-z\d]*);/gi, (match, code) => {
1841
- if (code[0] !== "#") return NAMED_ENTITIES[code.toLowerCase()] ?? match;
1842
- const isHex = /^#x/i.test(code);
1843
- const cp = parseInt(code.slice(isHex ? 2 : 1), isHex ? 16 : 10);
1844
- return Number.isInteger(cp) && cp >= 0 && cp <= 1114111 ? String.fromCodePoint(cp) : match;
1859
+ var decodeEntities = (input = "", { keepAngles = false } = {}) => input.replace(/&(#x[\da-f]+|#\d+|[a-z][a-z\d]*);/gi, (match, code) => {
1860
+ let decoded;
1861
+ if (code[0] !== "#") {
1862
+ decoded = NAMED_ENTITIES[code.toLowerCase()] ?? match;
1863
+ } else {
1864
+ const isHex = /^#x/i.test(code);
1865
+ const cp = parseInt(code.slice(isHex ? 2 : 1), isHex ? 16 : 10);
1866
+ decoded = Number.isInteger(cp) && cp >= 0 && cp <= 1114111 ? String.fromCodePoint(cp) : match;
1867
+ }
1868
+ return keepAngles && (decoded === "<" || decoded === ">") ? match : decoded;
1845
1869
  });
1870
+ var decodeTextEntities = (input = "") => {
1871
+ const decoded = decodeEntities(input);
1872
+ return isHTML(decoded) ? decodeEntities(input, { keepAngles: true }) : decoded;
1873
+ };
1846
1874
 
1847
1875
  // src/transforms/htmlToTiptap/parseAttribs.ts
1848
1876
  var ATTRIB_RE = /([a-zA-Z_:][\w:.-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'`=<>]+)))?/g;
@@ -1897,7 +1925,7 @@ var openTag = (stack, name, attrs) => {
1897
1925
  // src/transforms/htmlToTiptap/pushText.ts
1898
1926
  var pushText = (state, rawText) => {
1899
1927
  if (!rawText || isSkipping(state.stack)) return;
1900
- decodeEntities(rawText).split("\n").forEach((segment, index) => {
1928
+ decodeTextEntities(rawText).split("\n").forEach((segment, index) => {
1901
1929
  if (index > 0) flushParagraph(state);
1902
1930
  if (!segment) return;
1903
1931
  const marks = state.stack.flatMap((entry) => entry.marks);
@@ -2169,11 +2197,11 @@ var isMonetizeV1 = isWidgetV1(WidgetType.ui_extension_ad);
2169
2197
  var HTML_TAG_RE2 = /<[a-zA-Z]/;
2170
2198
  var containsHtml = (text) => HTML_TAG_RE2.test(text);
2171
2199
  var isObject = (x) => x !== null && typeof x === "object" && !Array.isArray(x);
2172
- var normalizeLangValue = (value) => {
2173
- if (typeof value !== "string") return value;
2200
+ var normalizeTextValue = (value, options = {}) => {
2174
2201
  const tokenized = normalizeTokens(value);
2175
- return containsHtml(tokenized) ? htmlToTiptap(tokenized) : tokenized;
2202
+ return containsHtml(tokenized) || options.defaultTextStyle ? htmlToTiptap(tokenized, options) : decodeTextEntities(tokenized);
2176
2203
  };
2204
+ var normalizeLangValue = (value) => typeof value === "string" ? normalizeTextValue(value) : value;
2177
2205
  var normalizeTextContent = (node) => {
2178
2206
  if (Array.isArray(node)) return node.map(normalizeTextContent);
2179
2207
  if (!isObject(node)) return node;
@@ -3403,6 +3431,7 @@ var feedbackDoc = (raw, tokens) => ({
3403
3431
  ],
3404
3432
  type: "doc"
3405
3433
  });
3434
+ var MAX_ICON_SIZE_PX = 64;
3406
3435
  var IN_SCOPE_TIER_TYPES = new Set(progressTierTypes);
3407
3436
  var inScope = (tier) => IN_SCOPE_TIER_TYPES.has(tier.type ?? "");
3408
3437
  var thresholdOf = (tier) => tier.unlockConditionType === "item_quantity" ? Math.ceil(Number(tier.minimum)) : Math.round(Number(tier.minimum) * 100);
@@ -3421,6 +3450,10 @@ var discountOf = (tier) => {
3421
3450
  const amount = tier.discountAmountFromTotal ?? 0;
3422
3451
  return tier.discountType === "fixedAmount" ? { amount: Math.round(amount * 100), type: "fixedAmount" } : { amount, type: "percentage" };
3423
3452
  };
3453
+ var iconSizeOf = (tier) => {
3454
+ const px = Math.round(Number.parseFloat(String(tier.productTierElementSize ?? "")));
3455
+ return Number.isFinite(px) && px > 0 ? Math.min(px, MAX_ICON_SIZE_PX) : void 0;
3456
+ };
3424
3457
  var giftOf = (tier) => {
3425
3458
  if (tier.type !== "product") return void 0;
3426
3459
  const product = tier.products?.[0] ?? tier.allProducts?.[0];
@@ -3437,6 +3470,7 @@ var giftOf = (tier) => {
3437
3470
  var toTierMeta = (tier) => {
3438
3471
  const discount = discountOf(tier);
3439
3472
  const gift = giftOf(tier);
3473
+ const iconSizePx = iconSizeOf(tier);
3440
3474
  const isItemCount = tier.unlockConditionType === "item_quantity";
3441
3475
  const label = tierLabel(tier);
3442
3476
  const type = tier.type;
@@ -3445,6 +3479,8 @@ var toTierMeta = (tier) => {
3445
3479
  /** Carry the OOS-filter flag only on gift tiers that set it — the client drops the tier when its gift is fully out of stock. */
3446
3480
  ...gift && tier.filterOosVariants && { filterOosVariants: true },
3447
3481
  ...gift && { gift },
3482
+ /** Carry the merchant's milestone icon size only when set — absent ⇒ the schema's 18px (React) default. */
3483
+ ...iconSizePx !== void 0 && { iconSizePx },
3448
3484
  ...label && { label: { en: label } },
3449
3485
  /** Carry the image-swap flag only on gift tiers that set it — the client shows the gift's product image in the step slot instead of the bag icon. */
3450
3486
  ...gift && tier.productTierImage && { productTierImage: true },
@@ -1051,6 +1051,25 @@ var CABProgressTier = z15.object({
1051
1051
  title: z15.string().optional(),
1052
1052
  variantIds: z15.array(z15.number().int().positive()).default(() => [])
1053
1053
  }).optional(),
1054
+ /**
1055
+ * The step-row icon's square size in PIXELS (onsite `product_tier_element_size`, an admin px string the
1056
+ * converter parses): 18 (default) / 20 / 24 / 30 / 36. React sizes the milestone by stamping this onto
1057
+ * the icon SVG's `width`/`height` (`getProgressStepIconSize`), so a merchant who deliberately enlarged
1058
+ * their milestones keeps that at checkout instead of every icon collapsing to one hardcoded square.
1059
+ *
1060
+ * A NUMBER, not the raw `'18px'` string: the renderer needs it as a length for the icon slot's grid
1061
+ * track, and a px-suffixed string would push unit parsing into every consumer. Deliberately NOT named
1062
+ * `iconSize` — the `iconSize` on an `image` section is the `s-icon` token enum, and one name covering both a
1063
+ * token and a pixel count is the kind of ambiguity that gets one passed where the other is meant.
1064
+ *
1065
+ * Bounded (not merely positive) because it is a raw merchant-controlled length: the step row is a grid
1066
+ * of per-tier cells, and an out-of-domain value would stretch the row rather than fail loudly. The
1067
+ * converter clamps to this same ceiling, so drifted config lands on a big-but-sane icon and never on a
1068
+ * tier the strict parse drops. `.default(18)` holds React's default at parse time (schema-first — the
1069
+ * renderer never spells a fallback), which also makes a tree authored before this field render at
1070
+ * React's size rather than the old hardcoded 20.
1071
+ */
1072
+ iconSizePx: z15.number().int().min(1).max(64).default(18),
1054
1073
  /**
1055
1074
  * The step-row label at this tier's slot (onsite `getTierLabel`): the merchant custom label, else the
1056
1075
  * type default — 'Free Shipping' or '{n}% Discount'. A PRODUCT tier gets NO converter label: its name is
@@ -1780,12 +1799,21 @@ var NAMED_ENTITIES = {
1780
1799
  rsquo: "\u2019",
1781
1800
  trade: "\u2122"
1782
1801
  };
1783
- var decodeEntities = (input = "") => input.replace(/&(#x[\da-f]+|#\d+|[a-z][a-z\d]*);/gi, (match, code) => {
1784
- if (code[0] !== "#") return NAMED_ENTITIES[code.toLowerCase()] ?? match;
1785
- const isHex = /^#x/i.test(code);
1786
- const cp = parseInt(code.slice(isHex ? 2 : 1), isHex ? 16 : 10);
1787
- return Number.isInteger(cp) && cp >= 0 && cp <= 1114111 ? String.fromCodePoint(cp) : match;
1802
+ var decodeEntities = (input = "", { keepAngles = false } = {}) => input.replace(/&(#x[\da-f]+|#\d+|[a-z][a-z\d]*);/gi, (match, code) => {
1803
+ let decoded;
1804
+ if (code[0] !== "#") {
1805
+ decoded = NAMED_ENTITIES[code.toLowerCase()] ?? match;
1806
+ } else {
1807
+ const isHex = /^#x/i.test(code);
1808
+ const cp = parseInt(code.slice(isHex ? 2 : 1), isHex ? 16 : 10);
1809
+ decoded = Number.isInteger(cp) && cp >= 0 && cp <= 1114111 ? String.fromCodePoint(cp) : match;
1810
+ }
1811
+ return keepAngles && (decoded === "<" || decoded === ">") ? match : decoded;
1788
1812
  });
1813
+ var decodeTextEntities = (input = "") => {
1814
+ const decoded = decodeEntities(input);
1815
+ return isHTML(decoded) ? decodeEntities(input, { keepAngles: true }) : decoded;
1816
+ };
1789
1817
 
1790
1818
  // src/transforms/htmlToTiptap/parseAttribs.ts
1791
1819
  var ATTRIB_RE = /([a-zA-Z_:][\w:.-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'`=<>]+)))?/g;
@@ -1840,7 +1868,7 @@ var openTag = (stack, name, attrs) => {
1840
1868
  // src/transforms/htmlToTiptap/pushText.ts
1841
1869
  var pushText = (state, rawText) => {
1842
1870
  if (!rawText || isSkipping(state.stack)) return;
1843
- decodeEntities(rawText).split("\n").forEach((segment, index) => {
1871
+ decodeTextEntities(rawText).split("\n").forEach((segment, index) => {
1844
1872
  if (index > 0) flushParagraph(state);
1845
1873
  if (!segment) return;
1846
1874
  const marks = state.stack.flatMap((entry) => entry.marks);
@@ -2112,11 +2140,11 @@ var isMonetizeV1 = isWidgetV1(WidgetType.ui_extension_ad);
2112
2140
  var HTML_TAG_RE2 = /<[a-zA-Z]/;
2113
2141
  var containsHtml = (text) => HTML_TAG_RE2.test(text);
2114
2142
  var isObject = (x) => x !== null && typeof x === "object" && !Array.isArray(x);
2115
- var normalizeLangValue = (value) => {
2116
- if (typeof value !== "string") return value;
2143
+ var normalizeTextValue = (value, options = {}) => {
2117
2144
  const tokenized = normalizeTokens(value);
2118
- return containsHtml(tokenized) ? htmlToTiptap(tokenized) : tokenized;
2145
+ return containsHtml(tokenized) || options.defaultTextStyle ? htmlToTiptap(tokenized, options) : decodeTextEntities(tokenized);
2119
2146
  };
2147
+ var normalizeLangValue = (value) => typeof value === "string" ? normalizeTextValue(value) : value;
2120
2148
  var normalizeTextContent = (node) => {
2121
2149
  if (Array.isArray(node)) return node.map(normalizeTextContent);
2122
2150
  if (!isObject(node)) return node;
@@ -3346,6 +3374,7 @@ var feedbackDoc = (raw, tokens) => ({
3346
3374
  ],
3347
3375
  type: "doc"
3348
3376
  });
3377
+ var MAX_ICON_SIZE_PX = 64;
3349
3378
  var IN_SCOPE_TIER_TYPES = new Set(progressTierTypes);
3350
3379
  var inScope = (tier) => IN_SCOPE_TIER_TYPES.has(tier.type ?? "");
3351
3380
  var thresholdOf = (tier) => tier.unlockConditionType === "item_quantity" ? Math.ceil(Number(tier.minimum)) : Math.round(Number(tier.minimum) * 100);
@@ -3364,6 +3393,10 @@ var discountOf = (tier) => {
3364
3393
  const amount = tier.discountAmountFromTotal ?? 0;
3365
3394
  return tier.discountType === "fixedAmount" ? { amount: Math.round(amount * 100), type: "fixedAmount" } : { amount, type: "percentage" };
3366
3395
  };
3396
+ var iconSizeOf = (tier) => {
3397
+ const px = Math.round(Number.parseFloat(String(tier.productTierElementSize ?? "")));
3398
+ return Number.isFinite(px) && px > 0 ? Math.min(px, MAX_ICON_SIZE_PX) : void 0;
3399
+ };
3367
3400
  var giftOf = (tier) => {
3368
3401
  if (tier.type !== "product") return void 0;
3369
3402
  const product = tier.products?.[0] ?? tier.allProducts?.[0];
@@ -3380,6 +3413,7 @@ var giftOf = (tier) => {
3380
3413
  var toTierMeta = (tier) => {
3381
3414
  const discount = discountOf(tier);
3382
3415
  const gift = giftOf(tier);
3416
+ const iconSizePx = iconSizeOf(tier);
3383
3417
  const isItemCount = tier.unlockConditionType === "item_quantity";
3384
3418
  const label = tierLabel(tier);
3385
3419
  const type = tier.type;
@@ -3388,6 +3422,8 @@ var toTierMeta = (tier) => {
3388
3422
  /** Carry the OOS-filter flag only on gift tiers that set it — the client drops the tier when its gift is fully out of stock. */
3389
3423
  ...gift && tier.filterOosVariants && { filterOosVariants: true },
3390
3424
  ...gift && { gift },
3425
+ /** Carry the merchant's milestone icon size only when set — absent ⇒ the schema's 18px (React) default. */
3426
+ ...iconSizePx !== void 0 && { iconSizePx },
3391
3427
  ...label && { label: { en: label } },
3392
3428
  /** Carry the image-swap flag only on gift tiers that set it — the client shows the gift's product image in the step slot instead of the bag icon. */
3393
3429
  ...gift && tier.productTierImage && { productTierImage: true },
@@ -1,11 +1,38 @@
1
+ import { type TiptapDocument } from '../schema/widgets/checkout-and-beyond/text';
2
+ import { type HtmlToTiptapOptions } from '../transforms/htmlToTiptap/htmlToTiptap';
3
+ export type NormalizeTextOptions = Pick<HtmlToTiptapOptions, 'defaultTextStyle'>;
1
4
  /**
2
- * Walks an untyped CABRootSection-shaped object (post deepCamelCase, pre schema
3
- * parse) and rewrites `text` sections' `content[lang]` strings so that:
5
+ * THE text pipeline for one content value the single path every text a CAB tree renders takes,
6
+ * whether it is merchant-authored, converted from a legacy widget, or third-party ad creative:
7
+ *
8
+ * 1. `{firstName}` (single brace) becomes `{{firstName}}` (canonical).
9
+ * 2. HTML becomes a fully-formed `TiptapDocument` (entities decoded per run by `htmlToTiptap`).
10
+ * 3. Plain text is entity-decoded in place and stays a string, so `Sale &amp; Save` never renders
11
+ * its `&amp;` literally and URL-bearing values (`buttonField: destinationUrl`) keep the string
12
+ * shape their validation expects.
13
+ *
14
+ * Both decodes run through `decodeTextEntities`, which holds tag-shaped angle entities escaped so a
15
+ * decode can never manufacture the text the section's `NO_HTML` refine rejects.
16
+ *
17
+ * `defaultTextStyle` is the server-side analogue of React's `defaultTextStyle` prop: the doc's runs
18
+ * carry that `textStyle` unless the markup set their own. A default forces plain text through the
19
+ * converter too, since only a doc can carry a mark.
4
20
  *
5
- * - `{firstName}` (single brace) becomes `{{firstName}}` (canonical), and
6
- * - HTML-laden strings become a fully-formed `TiptapDocument`.
21
+ * NOT idempotent for entity-bearing strings (`&amp;amp;` `&amp;` `&`), by nature of decoding.
22
+ * That is safe because this runs at SERVE time on the stored original and its output is returned,
23
+ * never written back — rebuy-api's `/cab/*` routes are read-only and admin-nextjs never invokes the
24
+ * walk. Keep it that way: a save path that persisted this output would degrade one entity level per
25
+ * cycle. (Docs are immune — the walk leaves them untouched.)
26
+ */
27
+ export declare const normalizeTextValue: (value: string, options?: NormalizeTextOptions) => string | TiptapDocument;
28
+ /**
29
+ * Walks an untyped CABRootSection-shaped object (post deepCamelCase, pre schema
30
+ * parse) and runs every `text` section's `content[lang]` string through
31
+ * {@link normalizeTextValue}. Already-converted Tiptap docs pass through untouched,
32
+ * so a caller that needed {@link normalizeTextValue}'s options can convert first
33
+ * and hand the result to this walk without double-conversion.
7
34
  *
8
- * Plain strings pass through unchanged. Non-text sections only recurse into
9
- * their `sections` array; Tiptap docs and other nested objects are not walked.
35
+ * Non-text sections only recurse into their `sections` array; Tiptap docs and
36
+ * other nested objects are not walked.
10
37
  */
11
38
  export declare const normalizeTextContent: (node: unknown) => unknown;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebuy/rebuy",
3
- "version": "3.10.0",
3
+ "version": "3.12.0",
4
4
  "description": "Shared zod schemas, legacy-to-CAB widget transforms, and server orchestrators for Rebuy's Shopify consumers (rebuy-api, admin-nextjs, rebuy-shopify-extensions)",
5
5
  "license": "MIT",
6
6
  "author": "Rebuy, Inc.",