@gemboss/gem-framework 0.15.0 → 0.18.11

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.
@@ -1,3 +1,38 @@
1
+ // ../../packages/gem-framework/src/section-dividers.ts
2
+ var spikes = (() => {
3
+ const teeth = [];
4
+ for (let x = 95; x > 0; x -= 5) teeth.push(`L${x} ${teeth.length % 2 === 0 ? 10 : 3}`);
5
+ return `M0 0 H100 V3 ${teeth.join(" ")} L0 ${teeth.length % 2 === 0 ? 10 : 3} Z`;
6
+ })();
7
+ var GEM_SECTION_DIVIDER_PATHS = Object.freeze({
8
+ wave: "M0 0 H100 V3 C80 9 70 -1 50 4 C30 9 20 1 0 4 Z",
9
+ slope: "M0 0 H100 V10 Z",
10
+ spikes,
11
+ arch: "M0 0 H100 V2 Q50 12 0 2 Z",
12
+ blob: "M0 0 H100 V4 C85 11 72 0 58 5 C44 10 30 2 16 6 C10 8 5 5 0 6 Z"
13
+ });
14
+ var NUMBER = "[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[+-]?\\d+)?";
15
+ var CHANNEL = new RegExp(`^${NUMBER}%?$`, "i");
16
+ var PERCENT = new RegExp(`^${NUMBER}%$`, "i");
17
+ var ANGLE = new RegExp(`^${NUMBER}(?:deg|grad|rad|turn)?$`, "i");
18
+ var GEM_ELEMENT_SECTION_DIVIDER_CSS = [
19
+ ".gem-el-section[data-gem-element-dividers] { position: relative; }",
20
+ // A configured edge must escape the band's parallax clip. The media keeps its own clipped
21
+ // wrapper so releasing the root cannot expose the deliberately oversized moving background.
22
+ ".gem-el-section[data-gem-parallax][data-gem-element-dividers] { overflow: visible; isolation: auto; }",
23
+ ".gem-el-section[data-gem-element-dividers] > .gem-el-section__parallax-clip { position: absolute; inset: 0; overflow: hidden; border-radius: inherit; pointer-events: none; z-index: 0; }",
24
+ // Edge paint may extend into a neighbour with little padding. Only content, never its background,
25
+ // stacks above it. Guard on the current document's opted-in markup: a retained CSS resource after
26
+ // navigation must not change a page with no native dividers.
27
+ // An ordinary neighbouring parallax band also isolates its content by default. Release that
28
+ // stacking context on divider documents, retaining its existing media clip and direct markup.
29
+ "body:has(.gem-el-section[data-gem-element-dividers]) .gem-el-section[data-gem-parallax] { isolation: auto; }",
30
+ "body:has(.gem-el-section[data-gem-element-dividers]) .gem-el-section > .gem-el-section__inner { position: relative; z-index: 2; }",
31
+ ".gem-el-section > .gem-el-section__divider { position: absolute; display: block; inset-inline: 0; width: 100%; max-width: none; height: var(--gem-element-divider-height, 48px); color: var(--gem-element-divider-color, var(--gem-color-background)); overflow: hidden; pointer-events: none; z-index: 1; transform: scaleX(var(--gem-element-divider-scale-x, 1)); }",
32
+ ".gem-el-section > .gem-el-section__divider--bottom { top: 100%; }",
33
+ ".gem-el-section > .gem-el-section__divider--top { bottom: 100%; transform: scaleX(var(--gem-element-divider-scale-x, 1)) scaleY(-1); }"
34
+ ].join("\n");
35
+
1
36
  // ../../packages/gem-framework/src/form-set.ts
2
37
  var GEM_FORM_TOKEN_KEYS = [
3
38
  "radius",
@@ -51,7 +86,7 @@ var GEM_FORM_DEFAULTS = {
51
86
  checkboxAccent: "var(--gem-color-accent)",
52
87
  checkboxSize: "24px"
53
88
  };
54
- var GEM_FORM_VAR_SCOPE = "[data-gem-form-set], [data-gem-form-set] :is(input, select, textarea, button, form)";
89
+ var GEM_FORM_VAR_SCOPE = "[data-gem-form-set], [data-gem-form-set] :is(input, select, textarea, button, form, output)";
55
90
  function gemFormCssBlock(form) {
56
91
  const vars = gemFormCssVars(form);
57
92
  return vars.length ? `${GEM_FORM_VAR_SCOPE} {
@@ -573,12 +608,12 @@ function gemMobileTokenCssVars(tokens, phone = readPhoneTokens(tokens)) {
573
608
  var rampKey = (k) => k.toLowerCase().replace(/[^a-z0-9]/g, "");
574
609
  var isRampKey = (k) => GEM_TYPE_RAMP_KEYS.some((r) => rampKey(r) === rampKey(k));
575
610
  var LENGTH = /^\d*\.?\d+(px|rem|em)$/;
576
- var NUMBER = /^\d*\.?\d+$/;
611
+ var NUMBER2 = /^\d*\.?\d+$/;
577
612
  var typeRampValue = (typography, key) => {
578
613
  const hit = Object.entries(typography ?? {}).find(([k]) => rampKey(k) === rampKey(key));
579
614
  const v = hit == null || hit[1] == null ? "" : stripImportant(hit[1]).trim();
580
615
  if (!v) return null;
581
- return (key === "scale" ? NUMBER.test(v) && Number(v) > 0 : LENGTH.test(v)) ? v : null;
616
+ return (key === "scale" ? NUMBER2.test(v) && Number(v) > 0 : LENGTH.test(v)) ? v : null;
582
617
  };
583
618
  var GEM_TYPE_MOBILE_MAX = "749px";
584
619
  function gemTypeRampCssVars(typography) {
@@ -653,6 +688,14 @@ function slugify(value) {
653
688
  var EDITION_2_CSS = [
654
689
  `:where(&) { ${GEM_HEADING_SIZE_CSS_VARS.filter((decl) => !decl.startsWith("--gem-section-heading-size-mobile") && !decl.startsWith("--gem-hero-heading-size-mobile")).join(" ")} }`
655
690
  ];
691
+ var EDITION_3_ELEMENTS = [
692
+ `:where(&) .gem-product-card-grid--quick-view-grid .gem-product-card-grid__card { border: 0; border-radius: 0; box-shadow: none; background: transparent; padding: 0; }`,
693
+ `:where(&) .gem-product-card-grid--quick-view-grid .gem-product-card__media > .gem-product-card__badges { display: flex; }`,
694
+ `@media (max-width: 989px) { :where(&) .gem-product-card-grid--quick-view-grid .gem-product-card-grid__grid[data-gem-product-grid] { grid-template-columns: repeat(var(--gem-product-card-grid-mobile-columns, 2), minmax(0, 1fr)); } }`,
695
+ `:where(&) .gem-hero.gem-hero-banner--compact-collection .gem-hero__overlay:not(.has-media) { position: relative; inset: auto; z-index: auto; opacity: 1; width: min(var(--gem-layout-page, 1200px), calc(100% - var(--gem-layout-gutter, 32px))); margin: clamp(24px, 5vw, 48px) auto 0; background: none; color: var(--gem-color-text); border-radius: 0; }`,
696
+ `:where(&) .gem-hero.gem-hero-banner--compact-collection .gem-hero__overlay:not(.has-media) .gem-hero__heading { color: var(--gem-color-text); }`,
697
+ `:where(&) .gem-section.gem-hero.gem-hero-banner--compact-collection:not(:has(> .gem-hero__bg)) { padding-block-end: 0; }`
698
+ ];
656
699
  var GEM_FRAMEWORK_EDITIONS = [
657
700
  { edition: 1, since: "0.1.0", released: "2026-07-04", changes: [], css: [] },
658
701
  {
@@ -661,12 +704,27 @@ var GEM_FRAMEWORK_EDITIONS = [
661
704
  released: "2026-10-10",
662
705
  changes: ["Section headings and hero titles follow your type scale on tablets and computers. Phones keep their current size."],
663
706
  css: EDITION_2_CSS
707
+ },
708
+ {
709
+ // Edition 3. Collection pages at Dawn's tiers (12/10/2026, measured on a "Start from scratch" theme): EDITION_3_ELEMENTS.
710
+ edition: 3,
711
+ since: "0.17.0",
712
+ released: "2026-10-12",
713
+ changes: [
714
+ "Collection grids: each product photo fills its column, phones and tablets show your mobile column count, and Sale and Sold out badges appear.",
715
+ "The collection title sits on the page, lined up with the products, in your text colour."
716
+ ],
717
+ css: [],
718
+ elements: EDITION_3_ELEMENTS
664
719
  }
665
720
  ];
666
721
  var LATEST_GEM_FRAMEWORK_EDITION = GEM_FRAMEWORK_EDITIONS[GEM_FRAMEWORK_EDITIONS.length - 1].edition;
667
722
  function gemFrameworkEditionScope(edition) {
668
723
  return GEM_FRAMEWORK_EDITIONS.filter((e) => e.edition >= edition).map((e) => `[data-gem-edition='${e.edition}']`).join(", ");
669
724
  }
725
+ function gemFrameworkEditionElementCss() {
726
+ return GEM_FRAMEWORK_EDITIONS.flatMap((e) => (e.elements ?? []).map((rule) => rule.replaceAll("&", gemFrameworkEditionScope(e.edition)))).join("\n");
727
+ }
670
728
  function gemFrameworkEditionCss() {
671
729
  return GEM_FRAMEWORK_EDITIONS.flatMap((e) => e.css.map((rule) => rule.replaceAll("&", gemFrameworkEditionScope(e.edition)))).join("\n");
672
730
  }
@@ -1086,18 +1144,8 @@ var GEM_POPUP_SHELL_CSS = GEM_POPUP_SHELL_CSS_LINES.join("\n");
1086
1144
 
1087
1145
  // ../../packages/gem-framework/src/primitives.ts
1088
1146
  var dividerMask = (path) => `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 10' preserveAspectRatio='none'%3E%3Cpath fill='%23fff' d='${path}'/%3E%3C/svg%3E")`;
1089
- var spikePath = (() => {
1090
- const teeth = [];
1091
- for (let x = 95; x > 0; x -= 5) teeth.push(`L${x} ${teeth.length % 2 === 0 ? 10 : 3}`);
1092
- return `M0 0 H100 V3 ${teeth.join(" ")} L0 ${teeth.length % 2 === 0 ? 10 : 3} Z`;
1093
- })();
1094
- var DIVIDER_SHAPES = {
1095
- wave: "M0 0 H100 V3 C80 9 70 -1 50 4 C30 9 20 1 0 4 Z",
1096
- slope: "M0 0 H100 V10 Z",
1097
- spikes: spikePath,
1098
- arch: "M0 0 H100 V2 Q50 12 0 2 Z",
1099
- blob: "M0 0 H100 V4 C85 11 72 0 58 5 C44 10 30 2 16 6 C10 8 5 5 0 6 Z"
1100
- };
1147
+ var knownValues = (attr, values) => `:where(${values.map((v) => `[${attr}='${v}']`).join(", ")})`;
1148
+ var DIVIDER_SHAPES = Object.keys(GEM_SECTION_DIVIDER_PATHS);
1101
1149
  var GEM_DIVIDER_CSS_LINES = [
1102
1150
  // The clip is WIDENED, not removed. `.gem-section` sets `overflow: clip` and the wedge is drawn
1103
1151
  // at `top: 100%` / `bottom: 100%` — entirely outside the box — so the parent deleted every
@@ -1108,7 +1156,8 @@ var GEM_DIVIDER_CSS_LINES = [
1108
1156
  // it entirely would paint square corners over that radius. A browser without
1109
1157
  // `overflow-clip-margin` keeps plain `clip` — the divider stays hidden, which is today's
1110
1158
  // behaviour rather than a broken layout.
1111
- ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-divider-top], :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-divider-bottom] { position: relative; overflow: clip; overflow-clip-margin: var(--gem-divider-height, 48px); }",
1159
+ // Only for a shape that has a mask (knownValues): a "none" edge used to widen the clip by 48px for nothing.
1160
+ `:is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-divider-top]${knownValues("data-gem-divider-top", DIVIDER_SHAPES)}, :is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-divider-bottom]${knownValues("data-gem-divider-bottom", DIVIDER_SHAPES)} { position: relative; overflow: clip; overflow-clip-margin: var(--gem-divider-height, 48px); }`,
1112
1161
  // Shared geometry. `top: 100%` / `bottom: 100%` put it outside the box; z-index 1 keeps it above the
1113
1162
  // neighbouring band's background. pointer-events: none so it never eats a click near the seam.
1114
1163
  // Qualified by SHAPE. Unqualified, any value paints a solid 48px slab — including the literal
@@ -1118,7 +1167,7 @@ var GEM_DIVIDER_CSS_LINES = [
1118
1167
  ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-divider-bottom]::after { top: 100%; }",
1119
1168
  // The top edge reuses the same masks, mirrored, so five shapes cover both edges.
1120
1169
  ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-divider-top]::before { bottom: 100%; transform: scaleY(-1); }",
1121
- ...Object.entries(DIVIDER_SHAPES).flatMap(([name, path]) => {
1170
+ ...Object.entries(GEM_SECTION_DIVIDER_PATHS).flatMap(([name, path]) => {
1122
1171
  const mask = dividerMask(path);
1123
1172
  return [
1124
1173
  `:is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-divider-bottom='${name}']::after { -webkit-mask-image: ${mask}; mask-image: ${mask}; }`,
@@ -1127,8 +1176,10 @@ var GEM_DIVIDER_CSS_LINES = [
1127
1176
  })
1128
1177
  ];
1129
1178
  var GEM_DIVIDER_CSS = GEM_DIVIDER_CSS_LINES.join("\n");
1179
+ var GEM_CARD_STYLES = ["outline", "shadow", "filled"];
1130
1180
  var GEM_CARD_CSS_LINES = [
1131
- ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-card] :is(.gem-section__inner, [class*='__inner']) { border-radius: var(--gem-radius-md); padding: var(--gem-space-xl); }",
1181
+ // Only for a style that draws (knownValues): "none", the knob's revert value, arrives here as an attribute too.
1182
+ `:is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-card]${knownValues("data-gem-card", GEM_CARD_STYLES)} :is(.gem-section__inner, [class*='__inner']) { border-radius: var(--gem-radius-md); padding: var(--gem-space-xl); }`,
1132
1183
  ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-card='outline'] :is(.gem-section__inner, [class*='__inner']) { border: 1px solid var(--gem-color-border); }",
1133
1184
  ":is(.gem-section, .gem-hero, .gem-hero-split, .gem-hero-collage, .gem-hero-triptych, .gem-slideshow)[data-gem-card='shadow'] :is(.gem-section__inner, [class*='__inner']) { background: var(--gem-color-surface); box-shadow: var(--gem-shadow-md); }",
1134
1185
  // `filled` tints the section's own ground rather than reaching for a fixed colour, so it stays
@@ -1415,7 +1466,7 @@ function createGemBaseCssParts(profile) {
1415
1466
  ].join("\n");
1416
1467
  return {
1417
1468
  // Edition rules close the primitives (editions.ts): custom properties on <html>, scoped :where([data-gem-edition]).
1418
- primitives: [reset, "", createPrimitiveCss(), "", gemFrameworkEditionCss(), "", ""].join("\n"),
1469
+ primitives: [reset, "", createPrimitiveCss(), "", gemFrameworkEditionElementCss(), "", gemFrameworkEditionCss(), "", ""].join("\n"),
1419
1470
  sections: [createShellCss(), "", createSectionCss(), "", createThemeProfileCss(profile)].join("\n")
1420
1471
  };
1421
1472
  }
@@ -1425,7 +1476,11 @@ function createThemeProfileCss(profile = "dawn-parity") {
1425
1476
  function createMorphProfileCss() {
1426
1477
  return [
1427
1478
  "/* Gem morph profile: style packs and section presets provide visual transformation above core primitives. */",
1428
- ":root { --gem-profile-mode: morph; }"
1479
+ ":root { --gem-profile-mode: morph; }",
1480
+ // A morph theme's quick-view card is a box drawn from its card tokens (the .gem-card primitive). Edition 3 unboxes this
1481
+ // card for Dawn's collection page; restated here one class higher, so a morph theme keeps its box and its card knobs on
1482
+ // every edition (review of #764). The same values as .gem-card: nothing changes for a morph store on edition 1 or 2.
1483
+ ".gem-product-card-grid--quick-view-grid .gem-card.gem-product-card-grid__card { border: var(--gem-border-subtle, 1px solid var(--gem-color-border)); border-radius: var(--gem-card-radius, var(--gem-radius-md)); background: var(--gem-card-background, var(--gem-color-surface)); padding: var(--gem-component-card-padding, var(--gem-space-md)); box-shadow: var(--gem-shadow-sm, none); }"
1429
1484
  ].join("\n");
1430
1485
  }
1431
1486
  function createPrimitiveCss() {
@@ -1871,17 +1926,25 @@ function createPrimitiveCss() {
1871
1926
  // .gem-rating above is a type rule for a text label; every actual star row in the theme was a
1872
1927
  // hardcoded five-glyph literal, so a captured 4-of-5 (or a half star) had nowhere to land. The
1873
1928
  // fill is a percentage-width overlay, so fractional values render exactly instead of rounding.
1929
+ // SVG stars (GemFactory gap #45, 0.18.11): real review widgets draw each star as an SVG (solid + outline), and the
1930
+ // eight product-reviews sections rebuilt in GemFactory put those SVGs in __base / __fill. Two things broke the fill
1931
+ // (measured 12/10/2026): the base `svg { max-width: 100% }` shrank the SVGs in the clipped __fill to the fill's
1932
+ // width (4.2 drew as five squeezed stars), and a .gem-stars that is a stretched grid / flex item computed its
1933
+ // percentage against the stretched width (3 of 5 drew as 5). The row is as wide as its stars, always, and a star
1934
+ // keeps its own size inside the clip.
1874
1935
  ".gem-stars {",
1875
1936
  " --gem-stars-total: 5;",
1876
1937
  " --gem-stars-value: 5;",
1877
1938
  " position: relative;",
1878
1939
  " display: inline-block;",
1940
+ " width: max-content;",
1879
1941
  " line-height: 1;",
1880
1942
  " font-size: var(--gem-text-sm, 0.8125rem);",
1881
1943
  " letter-spacing: 0.1em;",
1882
1944
  " color: var(--gem-color-border);",
1883
1945
  "}",
1884
1946
  ".gem-stars__base, .gem-stars__fill { display: block; white-space: nowrap; }",
1947
+ ".gem-stars :is(svg, img) { max-width: none; flex: none; }",
1885
1948
  ".gem-stars__fill {",
1886
1949
  " position: absolute;",
1887
1950
  " inset: 0;",
@@ -3387,7 +3450,7 @@ function createDawnParityCss() {
3387
3450
  ".gem-card, .gem-product-card { border: 0; border-radius: 0; box-shadow: none; background: transparent; padding: 0; }",
3388
3451
  ".gem-product-card__media, .gem-media { border: 0; border-radius: 0; box-shadow: none; background: rgb(244 244 244); }",
3389
3452
  ".gem-product-card__media { display: block; position: relative; width: 100%; height: 0; min-block-size: 0; padding-bottom: 100%; aspect-ratio: auto; overflow: hidden; }",
3390
- ".gem-featured-collection--editorial-grid .gem-product-card__media, .gem-product-card-grid--quick-view-grid .gem-product-card__media, .gem-main-search .gem-product-card__media, .gem-cart-recommendations--dawn-empty .gem-product-card__media { min-block-size: 0 !important; }",
3453
+ ".gem-featured-collection--editorial-grid .gem-product-card__media, .gem-product-card-grid--quick-view-grid .gem-product-card__media, .gem-main-search .gem-product-card__media, .gem-cart-recommendations--dawn-empty .gem-product-card__media, .gem-recommended-products--product-rail .gem-product-card__media, .gem-cart-recommendations--product-rail .gem-product-card__media { min-block-size: 0 !important; }",
3391
3454
  ".gem-product-card__media > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }",
3392
3455
  // Home product grids show generated/lifestyle imagery (not catalog product-on-white shots), so
3393
3456
  // `contain` letterboxes them inside the square card (looks misaligned / floating off the box).
@@ -3404,7 +3467,7 @@ function createDawnParityCss() {
3404
3467
  ".gem-cart-recommendations--dawn-empty .gem-product-card__quick-actions { display: none; }",
3405
3468
  ".gem-featured-collection--editorial-grid > .gem-section__inner > .gem-section__heading, .gem-featured-collection--editorial-grid > .gem-section__inner > .gem-section__body { display: none; }",
3406
3469
  ".gem-featured-collection--editorial-grid { padding-block: 28px 36px; }",
3407
- ".gem-featured-collection--editorial-grid .gem-section__grid, .gem-product-card-grid--quick-view-grid .gem-section__grid, .gem-main-search .gem-section__grid { grid-template-columns: repeat(var(--gem-product-card-grid-columns, 4), minmax(0, 1fr)); gap: 8px; }",
3470
+ ".gem-featured-collection--editorial-grid .gem-section__grid, .gem-product-card-grid--quick-view-grid .gem-section__grid, .gem-main-search .gem-section__grid, .gem-recommended-products--product-rail .gem-section__grid, .gem-cart-recommendations--product-rail .gem-section__grid { grid-template-columns: repeat(var(--gem-product-card-grid-columns, 4), minmax(0, 1fr)); gap: 8px; }",
3408
3471
  ".gem-product-card-grid--quick-view-grid { padding-top: 0; }",
3409
3472
  ".gem-product-card-grid--quick-view-grid .gem-section__heading:empty { display: none; }",
3410
3473
  // Hide a compare-at only when the price ISN'T actually on sale. The rule used to hide it
@@ -3574,14 +3637,16 @@ function createDawnParityMobileCss() {
3574
3637
  " .gem-hero-banner--compact-collection { padding-block: 25px; }",
3575
3638
  " .gem-hero-banner--compact-collection .gem-section__heading { font-size: 30px; line-height: 39px; }",
3576
3639
  " .gem-product-card-grid--quick-view-grid > .gem-section__inner, .gem-main-search > .gem-section__inner { width: calc(100% - 30px); }",
3577
- " .gem-product-card-grid--quick-view-grid .gem-product-card-grid__grid { gap: 4px !important; }",
3640
+ // 8px on a phone too, as the featured collection and the two product rails: the cards are Dawn's flat cards, and with
3641
+ // no padding of their own a 4px gap left two columns of titles 4px apart at 320 (review of gemsite #951).
3642
+ " .gem-product-card-grid--quick-view-grid .gem-product-card-grid__grid { gap: 8px !important; }",
3578
3643
  " .gem-product-card__content { padding-top: 8px; gap: 4px; }",
3579
3644
  " .gem-product-card__title { font-size: 15px; line-height: 24px; }",
3580
3645
  " .gem-product-card__price, .gem-product-card__price .gem-price { font-size: 16px; line-height: 24px; }",
3581
3646
  // Fallback = the profile's own phone heading, declared once as --gem-heading-phone-cap on the :root above.
3582
3647
  " .gem-section__heading { font-size: var(--gem-section-heading-size-mobile, var(--gem-heading-phone-cap)); }",
3583
3648
  " .gem-hero-banner h1, .gem-hero-banner h2 { font-size: var(--gem-hero-heading-size-mobile, var(--gem-heading-phone-cap)); }",
3584
- " .gem-featured-collection--editorial-grid .gem-section__grid, .gem-product-card-grid--quick-view-grid .gem-section__grid, .gem-main-search .gem-section__grid { grid-template-columns: repeat(var(--gem-product-card-grid-mobile-columns, 2), minmax(0, 1fr)); }",
3649
+ " .gem-featured-collection--editorial-grid .gem-section__grid, .gem-product-card-grid--quick-view-grid .gem-section__grid, .gem-main-search .gem-section__grid, .gem-recommended-products--product-rail .gem-section__grid, .gem-cart-recommendations--product-rail .gem-section__grid { grid-template-columns: repeat(var(--gem-product-card-grid-mobile-columns, 2), minmax(0, 1fr)); }",
3585
3650
  " .gem-product-main[data-gem-product-main-layout='dawn-main'] > .gem-section__inner { width: calc(100% - 30px); }",
3586
3651
  " .gem-product-main[data-gem-product-main-layout='dawn-main'] .gem-product-media-gallery__frame { display: flex; gap: 0; overflow-x: auto; scroll-snap-type: x mandatory; }",
3587
3652
  " .gem-product-main[data-gem-product-main-layout='dawn-main'] .gem-product-media-gallery__item { flex: 0 0 100%; scroll-snap-align: start; aspect-ratio: 1 / 1; }",
@@ -4813,14 +4878,15 @@ function layoutKnobCheck(tsx, knobExempt = /* @__PURE__ */ new Set()) {
4813
4878
  }
4814
4879
  function slotsReadBy(raw) {
4815
4880
  const source = raw.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/(^|[^:])\/\/.*$/gm, "$1 ");
4816
- const names = /* @__PURE__ */ new Set(["slots"]);
4817
- for (const m of source.matchAll(/(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:section\.)?content_slots\b/g)) names.add(m[1]);
4818
- const alt = [...names].map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
4881
+ const aliases = /* @__PURE__ */ new Set(["slots"]);
4882
+ for (const m of source.matchAll(/(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=;]+)?=\s*(?:[A-Za-z_$][\w$]*\??\.)*content_slots\b/g)) aliases.add(m[1]);
4883
+ const esc = (n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4884
+ const bag = `(?:(?<![\\w$.\\-/"'\`])(?:${[...aliases].map(esc).join("|")})|(?<![\\w$\\-/"'\`])content_slots)`;
4819
4885
  const keys = /* @__PURE__ */ new Set();
4820
- for (const m of source.matchAll(new RegExp(`(?:${alt})\\.([A-Za-z_][A-Za-z0-9_]*)`, "g"))) keys.add(m[1]);
4821
- for (const m of source.matchAll(new RegExp(`(?:${alt})\\[["']([^"']+)["']\\]`, "g"))) keys.add(m[1]);
4886
+ for (const m of source.matchAll(new RegExp(`${bag}\\??\\.([A-Za-z_][A-Za-z0-9_]*)`, "g"))) keys.add(m[1]);
4887
+ for (const m of source.matchAll(new RegExp(`${bag}\\??\\.?\\[["']([^"']+)["']\\]`, "g"))) keys.add(m[1]);
4822
4888
  const prefixes = [];
4823
- for (const m of source.matchAll(new RegExp("(?:" + alt + ")\\[`([^`]*)`\\]", "g"))) {
4889
+ for (const m of source.matchAll(new RegExp(`${bag}\\??\\.?\\[\`([^\`]*)\`\\]`, "g"))) {
4824
4890
  const literal = m[1].split("${")[0];
4825
4891
  if (literal.length >= 3) prefixes.push(literal);
4826
4892
  }
@@ -5060,6 +5126,11 @@ var claimNamed = (key) => CLAIM_KEY.test(key) && !DISPLAY_QUANTITY.test(key);
5060
5126
  function stringLiteralsIn(line) {
5061
5127
  return [...line.matchAll(/(["'`])((?:\\.|(?!\1)[^\\])*)\1/g)].map((m) => m[2]);
5062
5128
  }
5129
+ var VERIFIED_FLAG = String.raw`\b(?:is_?)?verified(?:_?(?:buyer|purchase|purchaser|customer|review))?\b`;
5130
+ var VERIFIED_BY_DEFAULT = new RegExp(
5131
+ `(?:${VERIFIED_FLAG}\\s*(?:!==?\\s*false\\b|(?:\\?\\?|\\|\\|)=?\\s*(?:true|!0)\\b|===?\\s*false\\s*\\?\\s*(?:null|undefined|false|""|'')\\s*:)|(?<=[{[,(]\\s*)${VERIFIED_FLAG}\\s*=(?![=>])\\s*(?:true|!0)\\b)`,
5132
+ "i"
5133
+ );
5063
5134
  function fabricatedProofInSource(raw) {
5064
5135
  const src = blankComments(raw);
5065
5136
  const out = [];
@@ -5107,6 +5178,11 @@ function fabricatedProofInSource(raw) {
5107
5178
  out.push({ line: at, why: `${kind} ${JSON.stringify(text)}`, text: line.trim() });
5108
5179
  }
5109
5180
  });
5181
+ const code = src.replace(/(["'])((?:\\.|(?!\1)[^\\\n])*)\1/g, (_m, q, body) => q + body.replace(/[^\n]/g, " ") + q);
5182
+ for (const m of code.matchAll(new RegExp(VERIFIED_BY_DEFAULT.source, "gi"))) {
5183
+ const at = code.slice(0, m.index).split("\n").length;
5184
+ out.push({ line: at, why: `verified by default (${m[0].replace(/\s+/g, " ").trim()}); badge only on verified === true`, text: (lines[at - 1] ?? "").trim() });
5185
+ }
5110
5186
  for (const r of demoReads) {
5111
5187
  const onDemoLine = caught.has(`${r.entry.line}\0key\0${r.key}`) || stringLiteralsIn(r.entry.value).some((text) => caught.has(`${r.entry.line}\0str\0${text}`));
5112
5188
  if (!onDemoLine) out.push({ line: r.at, why: r.why, text: r.text });
@@ -5271,6 +5347,7 @@ export {
5271
5347
  SHARED_FRAMEWORK_PREFIXES,
5272
5348
  TESTIMONY_NAME,
5273
5349
  TESTIMONY_PROPERTY,
5350
+ VERIFIED_BY_DEFAULT,
5274
5351
  claimIn,
5275
5352
  classesStyledBy,
5276
5353
  cssPairingOrphans,
package/dist/sections.css CHANGED
@@ -859,7 +859,7 @@ main > .gem-section:first-child { padding-top: 0; }
859
859
  .gem-card, .gem-product-card { border: 0; border-radius: 0; box-shadow: none; background: transparent; padding: 0; }
860
860
  .gem-product-card__media, .gem-media { border: 0; border-radius: 0; box-shadow: none; background: rgb(244 244 244); }
861
861
  .gem-product-card__media { display: block; position: relative; width: 100%; height: 0; min-block-size: 0; padding-bottom: 100%; aspect-ratio: auto; overflow: hidden; }
862
- .gem-featured-collection--editorial-grid .gem-product-card__media, .gem-product-card-grid--quick-view-grid .gem-product-card__media, .gem-main-search .gem-product-card__media, .gem-cart-recommendations--dawn-empty .gem-product-card__media { min-block-size: 0 !important; }
862
+ .gem-featured-collection--editorial-grid .gem-product-card__media, .gem-product-card-grid--quick-view-grid .gem-product-card__media, .gem-main-search .gem-product-card__media, .gem-cart-recommendations--dawn-empty .gem-product-card__media, .gem-recommended-products--product-rail .gem-product-card__media, .gem-cart-recommendations--product-rail .gem-product-card__media { min-block-size: 0 !important; }
863
863
  .gem-product-card__media > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
864
864
  .gem-featured-collection .gem-product-card__media > img, .gem-product-card-grid .gem-product-card__media > img { object-fit: cover; }
865
865
  .gem-product-card-grid--quick-view-grid .gem-product-card__media { display: block; min-height: 0; }
@@ -873,7 +873,7 @@ main > .gem-section:first-child { padding-top: 0; }
873
873
  .gem-cart-recommendations--dawn-empty .gem-product-card__quick-actions { display: none; }
874
874
  .gem-featured-collection--editorial-grid > .gem-section__inner > .gem-section__heading, .gem-featured-collection--editorial-grid > .gem-section__inner > .gem-section__body { display: none; }
875
875
  .gem-featured-collection--editorial-grid { padding-block: 28px 36px; }
876
- .gem-featured-collection--editorial-grid .gem-section__grid, .gem-product-card-grid--quick-view-grid .gem-section__grid, .gem-main-search .gem-section__grid { grid-template-columns: repeat(var(--gem-product-card-grid-columns, 4), minmax(0, 1fr)); gap: 8px; }
876
+ .gem-featured-collection--editorial-grid .gem-section__grid, .gem-product-card-grid--quick-view-grid .gem-section__grid, .gem-main-search .gem-section__grid, .gem-recommended-products--product-rail .gem-section__grid, .gem-cart-recommendations--product-rail .gem-section__grid { grid-template-columns: repeat(var(--gem-product-card-grid-columns, 4), minmax(0, 1fr)); gap: 8px; }
877
877
  .gem-product-card-grid--quick-view-grid { padding-top: 0; }
878
878
  .gem-product-card-grid--quick-view-grid .gem-section__heading:empty { display: none; }
879
879
  .gem-product-card-grid--quick-view-grid .gem-price:not(.gem-price--on-sale) .gem-price__compare { display: none; }
@@ -1027,13 +1027,13 @@ main > .gem-section:first-child { padding-top: 0; }
1027
1027
  .gem-hero-banner--compact-collection { padding-block: 25px; }
1028
1028
  .gem-hero-banner--compact-collection .gem-section__heading { font-size: 30px; line-height: 39px; }
1029
1029
  .gem-product-card-grid--quick-view-grid > .gem-section__inner, .gem-main-search > .gem-section__inner { width: calc(100% - 30px); }
1030
- .gem-product-card-grid--quick-view-grid .gem-product-card-grid__grid { gap: 4px !important; }
1030
+ .gem-product-card-grid--quick-view-grid .gem-product-card-grid__grid { gap: 8px !important; }
1031
1031
  .gem-product-card__content { padding-top: 8px; gap: 4px; }
1032
1032
  .gem-product-card__title { font-size: 15px; line-height: 24px; }
1033
1033
  .gem-product-card__price, .gem-product-card__price .gem-price { font-size: 16px; line-height: 24px; }
1034
1034
  .gem-section__heading { font-size: var(--gem-section-heading-size-mobile, var(--gem-heading-phone-cap)); }
1035
1035
  .gem-hero-banner h1, .gem-hero-banner h2 { font-size: var(--gem-hero-heading-size-mobile, var(--gem-heading-phone-cap)); }
1036
- .gem-featured-collection--editorial-grid .gem-section__grid, .gem-product-card-grid--quick-view-grid .gem-section__grid, .gem-main-search .gem-section__grid { grid-template-columns: repeat(var(--gem-product-card-grid-mobile-columns, 2), minmax(0, 1fr)); }
1036
+ .gem-featured-collection--editorial-grid .gem-section__grid, .gem-product-card-grid--quick-view-grid .gem-section__grid, .gem-main-search .gem-section__grid, .gem-recommended-products--product-rail .gem-section__grid, .gem-cart-recommendations--product-rail .gem-section__grid { grid-template-columns: repeat(var(--gem-product-card-grid-mobile-columns, 2), minmax(0, 1fr)); }
1037
1037
  .gem-product-main[data-gem-product-main-layout='dawn-main'] > .gem-section__inner { width: calc(100% - 30px); }
1038
1038
  .gem-product-main[data-gem-product-main-layout='dawn-main'] .gem-product-media-gallery__frame { display: flex; gap: 0; overflow-x: auto; scroll-snap-type: x mandatory; }
1039
1039
  .gem-product-main[data-gem-product-main-layout='dawn-main'] .gem-product-media-gallery__item { flex: 0 0 100%; scroll-snap-align: start; aspect-ratio: 1 / 1; }
package/dist/sections.js CHANGED
@@ -82,8 +82,8 @@ function subscriptionPlans(allocations, basePrice) {
82
82
  if (!id || seen.has(id)) continue;
83
83
  const adjustments = Array.isArray(row?.priceAdjustments) ? row.priceAdjustments.filter(Boolean) : [];
84
84
  const first = adjustments[0];
85
- const money = first?.perDeliveryPrice ?? first?.price ?? null;
86
- const amount = adjustments.length ? num(money?.amount) : base > 0 ? base : null;
85
+ const money2 = first?.perDeliveryPrice ?? first?.price ?? null;
86
+ const amount = adjustments.length ? num(money2?.amount) : base > 0 ? base : null;
87
87
  if (amount === null) continue;
88
88
  seen.add(id);
89
89
  const cheaper = base > 0 && amount < base;
@@ -92,7 +92,7 @@ function subscriptionPlans(allocations, basePrice) {
92
92
  id,
93
93
  name: typeof row?.sellingPlan?.name === "string" && row.sellingPlan.name.trim() || "Subscription",
94
94
  amount,
95
- currencyCode: typeof money?.currencyCode === "string" && money.currencyCode || void 0,
95
+ currencyCode: typeof money2?.currencyCode === "string" && money2.currencyCode || void 0,
96
96
  savingPct: Math.round(discountPct),
97
97
  discountPct,
98
98
  introOnly: adjustments.length > 1
@@ -153,10 +153,10 @@ function merge(runs) {
153
153
  }
154
154
  return out.length ? out : [{ text: "", marks: [] }];
155
155
  }
156
- function place(text, phrases) {
156
+ function place(text2, phrases) {
157
157
  const taken = [];
158
158
  phrases.forEach((phrase, index) => {
159
- for (let at = text.indexOf(phrase); at >= 0; at = text.indexOf(phrase, at + 1)) {
159
+ for (let at = text2.indexOf(phrase); at >= 0; at = text2.indexOf(phrase, at + 1)) {
160
160
  const end = at + phrase.length;
161
161
  if (!taken.some(([s, e]) => at < e && s < end)) {
162
162
  taken.push([at, end, index]);
@@ -166,8 +166,8 @@ function place(text, phrases) {
166
166
  });
167
167
  return taken;
168
168
  }
169
- function phraseMarkRuns(text, lists = {}) {
170
- const copy = String(text ?? "");
169
+ function phraseMarkRuns(text2, lists = {}) {
170
+ const copy = String(text2 ?? "");
171
171
  const n = copy.length;
172
172
  const marks = Array.from({ length: n });
173
173
  const hrefs = Array.from({ length: n });
@@ -207,10 +207,10 @@ var OPEN = [["***", ["bold", "italic"]], ["**", ["bold"]], ["==", ["highlight"]]
207
207
  var ESCAPABLE = "*_=!^[]()\\";
208
208
  var STAND_IN = 57344;
209
209
  var escapeMarkers = (src) => src.replace(/\\([*_=!^[\]()\\])/g, (_, c) => String.fromCharCode(STAND_IN + ESCAPABLE.indexOf(c)));
210
- var unescapeMarkers = (text) => text.replace(/[-]/g, (c) => ESCAPABLE[c.charCodeAt(0) - STAND_IN]);
210
+ var unescapeMarkers = (text2) => text2.replace(/[-]/g, (c) => ESCAPABLE[c.charCodeAt(0) - STAND_IN]);
211
211
  function read(src, inherited, href, depth) {
212
212
  const out = [];
213
- const at = (text, marks, h = href) => ({ text, marks: ordered(marks), ...h ? { href: h } : {} });
213
+ const at = (text2, marks, h = href) => ({ text: text2, marks: ordered(marks), ...h ? { href: h } : {} });
214
214
  let last = 0;
215
215
  for (const m of src.matchAll(INLINE)) {
216
216
  const i = m.index ?? 0;
@@ -235,10 +235,10 @@ function read(src, inherited, href, depth) {
235
235
  return out;
236
236
  }
237
237
  function inlineMarkRuns(src) {
238
- const text = String(src ?? "");
239
- const escapes = !/[\ue000-\ue009]/.test(text);
238
+ const text2 = String(src ?? "");
239
+ const escapes = !/[\ue000-\ue009]/.test(text2);
240
240
  const runs = [];
241
- text.split("\n").forEach((line, i) => {
241
+ text2.split("\n").forEach((line, i) => {
242
242
  if (i) runs.push({ text: "\n", marks: [] });
243
243
  if (line.length > MAX_INLINE) return void runs.push({ text: line, marks: [] });
244
244
  const read1 = read(escapes ? escapeMarkers(line) : line, [], void 0, 0);
@@ -253,8 +253,118 @@ function markClassName(marks) {
253
253
  function inlineMarkText(src) {
254
254
  return inlineMarkRuns(src).map((r) => r.text).join("");
255
255
  }
256
+
257
+ // ../../packages/gem-sections/src/card-price.ts
258
+ function priceAmount(m) {
259
+ const a = m?.amount;
260
+ if (a == null || typeof a === "string" && !a.trim()) return NaN;
261
+ return typeof a === "number" || typeof a === "string" ? Number(a) : NaN;
262
+ }
263
+ var text = (v) => typeof v === "string" && v.trim() ? v : void 0;
264
+ var finite = (n) => typeof n === "number" && Number.isFinite(n) ? n : void 0;
265
+ var MONEY = /* @__PURE__ */ new Map();
266
+ function money(n, currency) {
267
+ if (!Number.isFinite(n) || !currency) return void 0;
268
+ let format = MONEY.get(currency);
269
+ if (format === void 0) {
270
+ try {
271
+ format = new Intl.NumberFormat(void 0, { style: "currency", currency });
272
+ } catch {
273
+ format = null;
274
+ }
275
+ if (MONEY.size < 64) MONEY.set(currency, format);
276
+ }
277
+ return format ? format.format(n) : `${n.toFixed(2)} ${currency}`;
278
+ }
279
+ var WHOLE;
280
+ var CENTS;
281
+ function plain(n, cents = !Number.isInteger(n)) {
282
+ if (!Number.isFinite(n)) return void 0;
283
+ if (!cents) {
284
+ if (!WHOLE) WHOLE = new Intl.NumberFormat(void 0, { maximumFractionDigits: 0 });
285
+ return WHOLE.format(n);
286
+ }
287
+ if (!CENTS) CENTS = new Intl.NumberFormat(void 0, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
288
+ return CENTS.format(n);
289
+ }
290
+ var SEP = "[,'\u2019 \\u00a0\\u2009\\u202f]";
291
+ var SEPS = new RegExp(SEP, "g");
292
+ var ONE_NUMBER = new RegExp(`^(\\D*)(\\d{1,3}(?:(?:${SEP}\\d{2})*(?:${SEP}\\d{3})+)?|\\d+)(\\.\\d{1,2})?(\\D*)$`);
293
+ var MULTIPLIER = /^\s*(?:k|tr|triệu|nghìn|ngàn|m|mn|mio|b|bn)(?!\p{L})/iu;
294
+ function priceTextAmount(value) {
295
+ const m = value ? ONE_NUMBER.exec(value.trim()) : null;
296
+ if (!m || /[.,]$/.test(m[1]) || new Set(m[2].replace(/\d/g, "")).size > 1 || MULTIPLIER.test(m[4])) return void 0;
297
+ return Number(m[2].replace(SEPS, "") + (m[3] ?? ""));
298
+ }
299
+ function startsWithFrom(value, label) {
300
+ if (typeof value !== "string") return false;
301
+ const head = value.trim().normalize("NFC").toLowerCase();
302
+ return [label, "from", "t\u1EEB"].some((w) => {
303
+ const t = String(w ?? "").trim().normalize("NFC").toLowerCase();
304
+ return Boolean(t) && head.startsWith(t) && !/^\p{L}/u.test(head.slice(t.length));
305
+ });
306
+ }
307
+ var NUMBER_RUN = /\d(?:[\d.,'’    ]*\d)?/g;
308
+ function readAlike(value) {
309
+ const runs = value.trim().match(NUMBER_RUN);
310
+ if (!runs || runs.length !== 1) return void 0;
311
+ const run = runs[0];
312
+ const tail = /^(.*)[.,](\d{1,2})$/.exec(run);
313
+ const n = Number(tail ? `${tail[1].replace(/\D/g, "")}.${tail[2]}` : run.replace(/\D/g, ""));
314
+ return Number.isFinite(n) ? { n, frame: value.trim().replace(run, "#") } : void 0;
315
+ }
316
+ function struckAbove(price, struck) {
317
+ const a = readAlike(price), b = readAlike(struck);
318
+ return a != null && b != null && a.frame === b.frame && b.n > a.n;
319
+ }
320
+ function cardPrice(product, fromLabel = "From") {
321
+ const p = product ?? {};
322
+ const seller = text(p.price);
323
+ const nodes = (Array.isArray(p.variants?.nodes) ? p.variants.nodes : []).filter((v) => v != null && typeof v === "object");
324
+ const sel = p.selectedVariant != null && typeof p.selectedVariant === "object" ? p.selectedVariant : void 0;
325
+ const at = sel ? nodes.findIndex((v) => sel.id != null && v.id === sel.id || nodes.length === 1 && (v.id == null || sel.id == null)) : -1;
326
+ const variants = at >= 0 ? nodes.map((v, i) => i === at ? { ...sel, ...v } : v) : sel ? [sel, ...nodes] : nodes;
327
+ const priced = variants.filter((v) => Number.isFinite(priceAmount(v.price)));
328
+ const amounts = priced.map((v) => priceAmount(v.price));
329
+ const min = p.priceRange?.minVariantPrice;
330
+ const minAmount = priceAmount(min);
331
+ const maxAmount = priceAmount(p.priceRange?.maxVariantPrice);
332
+ const lowest = Number.isFinite(minAmount) ? minAmount : amounts.length ? Math.min(...amounts) : finite(p.price) ?? NaN;
333
+ const code = (v) => text(v)?.trim();
334
+ const currency = code(min?.currencyCode) ?? priced.map((v) => code(v.price?.currencyCode)).find(Boolean);
335
+ const varies = p.priceVaries === true || Number.isFinite(lowest) && (amounts.some((a) => a !== lowest) || Number.isFinite(maxAmount) && maxAmount !== lowest);
336
+ const amount = finite(lowest) ?? priceTextAmount(seller);
337
+ const soldOut = p.soldOut === true || p.availableForSale === false || p.availableForSale !== true && variants.length > 0 && variants.every((v) => v.availableForSale === false);
338
+ const buyable = soldOut ? [] : variants.filter((v) => v.availableForSale !== false);
339
+ const atLowest = buyable.filter((v) => priceAmount(v.price) === lowest);
340
+ const discounted = atLowest.find((v) => priceAmount(v.compareAtPrice) > lowest);
341
+ const withId = (list) => list.find((v) => v.id != null);
342
+ const anyBuyable = priced.length === 0 ? withId(buyable) : void 0;
343
+ const base = { varies, amount, soldOut };
344
+ const formatted = money(lowest, currency);
345
+ if (formatted || Number.isFinite(lowest) && !seller) {
346
+ const cmp = discounted ? priceAmount(discounted.compareAtPrice) : NaN;
347
+ const cents = !Number.isInteger(lowest) || Number.isFinite(cmp) && !Number.isInteger(cmp);
348
+ const show = (n) => money(n, currency) ?? plain(n, cents);
349
+ const variant = discounted ? discounted.id != null ? discounted : void 0 : withId(atLowest) ?? anyBuyable;
350
+ return { text: show(lowest), from: varies, compareAt: discounted ? show(cmp) : void 0, variant, ...base };
351
+ }
352
+ if (seller) {
353
+ const named = priceTextAmount(seller) === lowest ? withId(atLowest) : void 0;
354
+ const struck = text(p.compareAtPrice);
355
+ return {
356
+ text: seller,
357
+ from: p.priceVaries === true && !startsWithFrom(seller, fromLabel),
358
+ compareAt: !soldOut && struck && struckAbove(seller, struck) ? struck : void 0,
359
+ variant: named ?? anyBuyable,
360
+ ...base
361
+ };
362
+ }
363
+ return { from: false, variant: anyBuyable, ...base };
364
+ }
256
365
  export {
257
366
  MARK_KINDS,
367
+ cardPrice,
258
368
  defaultPlan,
259
369
  elementOverrides,
260
370
  imageSrcSet,