fontdue-js 3.6.0 → 3.8.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.
Files changed (167) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +5 -4
  3. package/dist/__generated__/AddToCartBannerQuery.graphql.d.ts +1 -1
  4. package/dist/__generated__/AddToCartBannerQuery.graphql.js +16 -10
  5. package/dist/__generated__/AddToCartBanner_item.graphql.d.ts +4 -1
  6. package/dist/__generated__/AddToCartBanner_item.graphql.js +7 -2
  7. package/dist/__generated__/AddToCartBanner_order.graphql.d.ts +4 -1
  8. package/dist/__generated__/AddToCartBanner_order.graphql.js +6 -1
  9. package/dist/__generated__/BuyButtonIDQuery.graphql.d.ts +3 -3
  10. package/dist/__generated__/BuyButtonIDQuery.graphql.js +31 -16
  11. package/dist/__generated__/BuyButtonPackIDQuery.graphql.d.ts +21 -0
  12. package/dist/__generated__/BuyButtonPackIDQuery.graphql.js +129 -0
  13. package/dist/__generated__/BuyButtonPackSlugQuery.graphql.d.ts +25 -0
  14. package/dist/__generated__/BuyButtonPackSlugQuery.graphql.js +167 -0
  15. package/dist/__generated__/BuyButtonSlugQuery.graphql.d.ts +3 -3
  16. package/dist/__generated__/BuyButtonSlugQuery.graphql.js +45 -17
  17. package/dist/__generated__/BuyButton_product.graphql.d.ts +27 -0
  18. package/dist/__generated__/BuyButton_product.graphql.js +72 -0
  19. package/dist/__generated__/CartItemProduct_product.graphql.d.ts +1 -1
  20. package/dist/__generated__/CartItemProduct_product.graphql.js +7 -2
  21. package/dist/__generated__/CartOrderCompleteOrderMutation.graphql.d.ts +1 -1
  22. package/dist/__generated__/CartOrderCompleteOrderMutation.graphql.js +26 -20
  23. package/dist/__generated__/CartOrderRemoveDiscountMutation.graphql.d.ts +1 -1
  24. package/dist/__generated__/CartOrderRemoveDiscountMutation.graphql.js +26 -20
  25. package/dist/__generated__/CartOrderUpdateMutation.graphql.d.ts +1 -1
  26. package/dist/__generated__/CartOrderUpdateMutation.graphql.js +26 -20
  27. package/dist/__generated__/CartQuery.graphql.d.ts +1 -1
  28. package/dist/__generated__/CartQuery.graphql.js +26 -20
  29. package/dist/__generated__/CartStateUpdateMutation.graphql.d.ts +1 -1
  30. package/dist/__generated__/CartStateUpdateMutation.graphql.js +26 -20
  31. package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.d.ts +1 -1
  32. package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.js +26 -20
  33. package/dist/__generated__/CheckoutUpdateOrderMutation.graphql.d.ts +1 -1
  34. package/dist/__generated__/CheckoutUpdateOrderMutation.graphql.js +26 -20
  35. package/dist/__generated__/CollectionAa_Query.graphql.d.ts +1 -1
  36. package/dist/__generated__/CollectionAa_Query.graphql.js +20 -14
  37. package/dist/__generated__/CollectionAa_product.graphql.d.ts +7 -1
  38. package/dist/__generated__/CollectionAa_product.graphql.js +22 -16
  39. package/dist/__generated__/FontFamiliesQuery.graphql.d.ts +4 -1
  40. package/dist/__generated__/FontFamiliesQuery.graphql.js +46 -39
  41. package/dist/__generated__/NodePasswordFormElementIDQuery.graphql.d.ts +3 -3
  42. package/dist/__generated__/NodePasswordFormElementIDQuery.graphql.js +9 -9
  43. package/dist/__generated__/NodePasswordFormElementPackSlugQuery.graphql.d.ts +25 -0
  44. package/dist/__generated__/NodePasswordFormElementPackSlugQuery.graphql.js +108 -0
  45. package/dist/__generated__/PrecartAddToCartMutation.graphql.d.ts +1 -1
  46. package/dist/__generated__/PrecartAddToCartMutation.graphql.js +32 -26
  47. package/dist/__generated__/PriceBar_node.graphql.d.ts +4 -1
  48. package/dist/__generated__/PriceBar_node.graphql.js +23 -16
  49. package/dist/__generated__/StoreModalCartQuery.graphql.d.ts +1 -1
  50. package/dist/__generated__/StoreModalCartQuery.graphql.js +37 -31
  51. package/dist/__generated__/StoreModalFamily_collection.graphql.d.ts +3 -2
  52. package/dist/__generated__/StoreModalFamily_collection.graphql.js +3 -3
  53. package/dist/__generated__/StoreModalIndexPackItem_fontPack.graphql.d.ts +23 -0
  54. package/dist/__generated__/StoreModalIndexPackItem_fontPack.graphql.js +58 -0
  55. package/dist/__generated__/StoreModalIndexPaginationQuery.graphql.d.ts +23 -0
  56. package/dist/__generated__/StoreModalIndexPaginationQuery.graphql.js +303 -0
  57. package/dist/__generated__/StoreModalIndexQuery.graphql.d.ts +6 -3
  58. package/dist/__generated__/StoreModalIndexQuery.graphql.js +218 -117
  59. package/dist/__generated__/StoreModalIndex_viewer.graphql.d.ts +24 -0
  60. package/dist/__generated__/StoreModalIndex_viewer.graphql.js +134 -0
  61. package/dist/__generated__/StoreModalPackButton_fontPack.graphql.d.ts +33 -0
  62. package/dist/__generated__/StoreModalPackButton_fontPack.graphql.js +119 -0
  63. package/dist/__generated__/StoreModalPack_fontPack.graphql.d.ts +45 -0
  64. package/dist/__generated__/StoreModalPack_fontPack.graphql.js +150 -0
  65. package/dist/__generated__/StoreModalProductContent_collection.graphql.d.ts +6 -2
  66. package/dist/__generated__/StoreModalProductContent_collection.graphql.js +43 -19
  67. package/dist/__generated__/{StoreModalProductLicenseSelection_collection.graphql.d.ts → StoreModalProductLicenseSelection_licenseable.graphql.d.ts} +7 -6
  68. package/dist/__generated__/StoreModalProductLicenseSelection_licenseable.graphql.js +43 -0
  69. package/dist/__generated__/StoreModalProductQuery.graphql.d.ts +1 -1
  70. package/dist/__generated__/StoreModalProductQuery.graphql.js +199 -142
  71. package/dist/__generated__/StoreModalProductRefetchQuery.graphql.d.ts +1 -1
  72. package/dist/__generated__/StoreModalProductRefetchQuery.graphql.js +190 -133
  73. package/dist/__generated__/StoreModalProductSlugQuery.graphql.d.ts +5 -1
  74. package/dist/__generated__/StoreModalProductSlugQuery.graphql.js +254 -176
  75. package/dist/__generated__/StoreModalProductSummaryRefetchQuery.graphql.d.ts +1 -1
  76. package/dist/__generated__/StoreModalProductSummaryRefetchQuery.graphql.js +9 -4
  77. package/dist/__generated__/StoreModalProductSummary_viewer.graphql.d.ts +7 -1
  78. package/dist/__generated__/StoreModalProductSummary_viewer.graphql.js +7 -2
  79. package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.d.ts +1 -1
  80. package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.js +26 -20
  81. package/dist/__generated__/TypeTestersIDQuery.graphql.d.ts +1 -1
  82. package/dist/__generated__/TypeTestersIDQuery.graphql.js +17 -10
  83. package/dist/__generated__/TypeTestersRefetchQuery.graphql.d.ts +1 -1
  84. package/dist/__generated__/TypeTestersRefetchQuery.graphql.js +17 -10
  85. package/dist/__generated__/TypeTestersSlugQuery.graphql.d.ts +1 -1
  86. package/dist/__generated__/TypeTestersSlugQuery.graphql.js +17 -10
  87. package/dist/__generated__/productState_Query.graphql.d.ts +99 -1
  88. package/dist/__generated__/productState_Query.graphql.js +43 -4
  89. package/dist/__generated__/productState_fontPack.graphql.d.ts +111 -0
  90. package/dist/__generated__/productState_fontPack.graphql.js +117 -0
  91. package/dist/__generated__/storefrontEventsTrackStorefrontEventMutation.graphql.d.ts +41 -0
  92. package/dist/__generated__/storefrontEventsTrackStorefrontEventMutation.graphql.js +65 -0
  93. package/dist/__tests__/bundleSelectionOrder.test.js +94 -0
  94. package/dist/__tests__/collectionBundleSelection.test.js +443 -1446
  95. package/dist/__tests__/fontPackSelection.test.js +435 -0
  96. package/dist/__tests__/infiniteScroll.test.js +180 -0
  97. package/dist/__tests__/licenseExclusions.test.js +4 -1
  98. package/dist/__tests__/storeModalPackButton.test.js +102 -0
  99. package/dist/__tests__/storeModalPackUnlisted.test.js +112 -0
  100. package/dist/__tests__/storeModalRoutes.test.js +9 -0
  101. package/dist/__tests__/storefrontEvents.test.js +167 -0
  102. package/dist/__tests__/typeTesterAutofitFeatures.test.js +138 -0
  103. package/dist/components/AddToCartBanner/index.js +1 -1
  104. package/dist/components/BuyButton/index.d.ts +29 -9
  105. package/dist/components/BuyButton/index.js +123 -26
  106. package/dist/components/BuyButton/index.server.d.ts +4 -2
  107. package/dist/components/BuyButton/index.server.js +24 -3
  108. package/dist/components/Cart/CartItem/CartItemProduct.js +3 -3
  109. package/dist/components/Cart/Checkout.js +11 -1
  110. package/dist/components/Cart/storefrontEvents.d.ts +34 -0
  111. package/dist/components/Cart/storefrontEvents.js +138 -0
  112. package/dist/components/CollectionAa/index.js +2 -2
  113. package/dist/components/FontFamilies/index.js +4 -3
  114. package/dist/components/NodePasswordForm/NodePasswordFormElement.d.ts +1 -1
  115. package/dist/components/NodePasswordForm/NodePasswordFormElement.js +45 -21
  116. package/dist/components/NodePasswordForm/index.d.ts +7 -4
  117. package/dist/components/NodePasswordForm/index.js +6 -6
  118. package/dist/components/Precart/index.js +6 -0
  119. package/dist/components/PriceBar/index.js +5 -3
  120. package/dist/components/Root/productState.d.ts +2 -8
  121. package/dist/components/Root/productState.js +100 -61
  122. package/dist/components/SKUPrice/index.js +7 -8
  123. package/dist/components/StoreModal/StoreModalFamily.d.ts +7 -0
  124. package/dist/components/StoreModal/StoreModalFamily.js +11 -7
  125. package/dist/components/StoreModal/StoreModalIndex.js +58 -7
  126. package/dist/components/StoreModal/StoreModalIndexEntry.d.ts +13 -0
  127. package/dist/components/StoreModal/StoreModalIndexEntry.js +43 -0
  128. package/dist/components/StoreModal/StoreModalIndexItem.js +7 -30
  129. package/dist/components/StoreModal/StoreModalIndexPackItem.d.ts +7 -0
  130. package/dist/components/StoreModal/StoreModalIndexPackItem.js +21 -0
  131. package/dist/components/StoreModal/StoreModalPack.d.ts +9 -0
  132. package/dist/components/StoreModal/StoreModalPack.js +79 -0
  133. package/dist/components/StoreModal/StoreModalPackButton.d.ts +8 -0
  134. package/dist/components/StoreModal/StoreModalPackButton.js +53 -0
  135. package/dist/components/StoreModal/StoreModalProduct.js +49 -19
  136. package/dist/components/StoreModalProductLicenseSelection/StoreModalProductLicense.d.ts +2 -1
  137. package/dist/components/StoreModalProductLicenseSelection/StoreModalProductLicense.js +8 -3
  138. package/dist/components/StoreModalProductLicenseSelection/index.d.ts +3 -3
  139. package/dist/components/StoreModalProductLicenseSelection/index.js +7 -6
  140. package/dist/components/StoreModalProductSummary/index.js +1 -1
  141. package/dist/components/TypeTester/useTypeTesterStyler.js +1 -0
  142. package/dist/components/elements/StoreModalContainer/StoreModalContainerContext.d.ts +1 -0
  143. package/dist/components/elements/StoreModalContainer/index.js +9 -4
  144. package/dist/components/elements/StoreModalFamily/index.d.ts +1 -1
  145. package/dist/components/elements/StoreModalFamily/index.js +1 -1
  146. package/dist/components/elements/StoreModalIndexLoadMore/index.d.ts +8 -0
  147. package/dist/components/elements/StoreModalIndexLoadMore/index.js +21 -0
  148. package/dist/components/elements/StoreModalUnifiedCheckout.js +12 -0
  149. package/dist/components/useInfiniteScroll.d.ts +28 -0
  150. package/dist/components/useInfiniteScroll.js +50 -0
  151. package/dist/fontdue.css +39 -0
  152. package/dist/hooks/useAutofit.d.ts +2 -1
  153. package/dist/hooks/useAutofit.js +10 -4
  154. package/dist/reducer.d.ts +12 -5
  155. package/dist/reducer.js +18 -87
  156. package/dist/selection.d.ts +44 -0
  157. package/dist/selection.js +539 -0
  158. package/dist/storeModalRoutes.js +5 -4
  159. package/dist/utils.d.ts +1 -12
  160. package/dist/utils.js +1 -36
  161. package/dist/version.js +1 -1
  162. package/package.json +1 -1
  163. package/dist/__generated__/BuyButton_collection.graphql.d.ts +0 -25
  164. package/dist/__generated__/BuyButton_collection.graphql.js +0 -49
  165. package/dist/__generated__/StoreModalProductLicenseSelection_collection.graphql.js +0 -40
  166. package/fontdue.css +0 -2733
  167. package/fontdue.css.map +0 -1
@@ -1,17 +1,17 @@
1
- import _StoreModalProductLicenseSelection_collection from "../../__generated__/StoreModalProductLicenseSelection_collection.graphql.js";
1
+ import _StoreModalProductLicenseSelection_licenseable from "../../__generated__/StoreModalProductLicenseSelection_licenseable.graphql.js";
2
2
  import React from 'react';
3
3
  import { graphql, useFragment } from 'react-relay';
4
4
  import StoreModalProductLicense from './StoreModalProductLicense.js';
5
5
  export default function StoreModalProductLicenseSelection(_ref) {
6
- var _collection$licenses;
6
+ var _licenseable$licenses;
7
7
  let {
8
- collection: collectionKey,
8
+ licenseable: licenseableKey,
9
9
  disabled,
10
10
  children: {
11
11
  label
12
12
  }
13
13
  } = _ref;
14
- const collection = useFragment((_StoreModalProductLicenseSelection_collection.hash && _StoreModalProductLicenseSelection_collection.hash !== "2767ff3f8ddb4354051074bc3d0312e6" && console.error("The definition of 'StoreModalProductLicenseSelection_collection' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _StoreModalProductLicenseSelection_collection), collectionKey);
14
+ const licenseable = useFragment((_StoreModalProductLicenseSelection_licenseable.hash && _StoreModalProductLicenseSelection_licenseable.hash !== "4016af73fd5c8c03fa7bcdfba6ce0b9b" && console.error("The definition of 'StoreModalProductLicenseSelection_licenseable' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _StoreModalProductLicenseSelection_licenseable), licenseableKey);
15
15
  return /*#__PURE__*/React.createElement("div", {
16
16
  className: "store-modal__license-selection__container",
17
17
  "data-disabled": disabled
@@ -19,8 +19,9 @@ export default function StoreModalProductLicenseSelection(_ref) {
19
19
  className: "store-modal__license-selection__title"
20
20
  }, label), /*#__PURE__*/React.createElement("div", {
21
21
  className: "store-modal__license-selection__licenses"
22
- }, collection === null || collection === void 0 ? void 0 : (_collection$licenses = collection.licenses) === null || _collection$licenses === void 0 ? void 0 : _collection$licenses.map(license => license && /*#__PURE__*/React.createElement(StoreModalProductLicense, {
22
+ }, licenseable === null || licenseable === void 0 ? void 0 : (_licenseable$licenses = licenseable.licenses) === null || _licenseable$licenses === void 0 ? void 0 : _licenseable$licenses.map(license => license && /*#__PURE__*/React.createElement(StoreModalProductLicense, {
23
23
  key: license.id,
24
- license: license
24
+ license: license,
25
+ collectionId: licenseable.id
25
26
  }))));
26
27
  }
@@ -57,7 +57,7 @@ const StoreModalProductSummary = _ref => {
57
57
  const refetchVariables = useMemo(() => ({
58
58
  selectedSkuIds: selectedSkuIdsArray
59
59
  }), [selectedSkuIdsArray]);
60
- const [viewer, refetch] = useRefetchableFragment((_StoreModalProductSummary_viewer.hash && _StoreModalProductSummary_viewer.hash !== "81a5f120d76cbb84753d158a7ff38c19" && console.error("The definition of 'StoreModalProductSummary_viewer' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _StoreModalProductSummary_viewer), viewerKey);
60
+ const [viewer, refetch] = useRefetchableFragment((_StoreModalProductSummary_viewer.hash && _StoreModalProductSummary_viewer.hash !== "afd13ac5f41519eea722d5138b244b25" && console.error("The definition of 'StoreModalProductSummary_viewer' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _StoreModalProductSummary_viewer), viewerKey);
61
61
  useRefetchOnLicenseChanges({
62
62
  environment,
63
63
  refetch,
@@ -40,6 +40,7 @@ const useTypeTesterStyler = _ref => {
40
40
  fontWeight: fontStyles.fontWeight,
41
41
  fontStyle: fontStyles.fontStyle,
42
42
  fontVariationSettings,
43
+ fontFeatureSettings,
43
44
  enabled: autofit && (!contentEdited || autofitOnChange),
44
45
  padding: sideBuffer
45
46
  });
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  interface StoreModalContainerContextProps {
3
3
  scrollToTop?: () => void;
4
+ scrollContainer?: React.RefObject<HTMLDivElement | null>;
4
5
  }
5
6
  declare const _default: React.Context<StoreModalContainerContextProps>;
6
7
  export default _default;
@@ -1,4 +1,4 @@
1
- import React, { useRef, useCallback } from 'react';
1
+ import React, { useRef, useCallback, useMemo } from 'react';
2
2
  import { ArrowLeft, CartNew, X } from '../../Icons/index.js';
3
3
  import StoreModalContainerContext from './StoreModalContainerContext.js';
4
4
  const StoreModalContainer = _ref => {
@@ -25,10 +25,15 @@ const StoreModalContainer = _ref => {
25
25
  left: 0
26
26
  });
27
27
  }, [container]);
28
+
29
+ // Stable, so an effect that depends on the context value (the index's
30
+ // load-more observer) isn't rebuilt on every render of the modal.
31
+ const context = useMemo(() => ({
32
+ scrollToTop: handleScrollToTop,
33
+ scrollContainer: container
34
+ }), [handleScrollToTop]);
28
35
  return /*#__PURE__*/React.createElement(StoreModalContainerContext.Provider, {
29
- value: {
30
- scrollToTop: handleScrollToTop
31
- }
36
+ value: context
32
37
  }, /*#__PURE__*/React.createElement("div", {
33
38
  className: "store-modal__container__overlay",
34
39
  "data-route": routeName
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  interface StoreModalFamilyProps {
3
3
  children: {
4
4
  title: React.ReactNode;
5
- familyButton: React.ReactElement<any>;
5
+ familyButton?: React.ReactElement<any> | null;
6
6
  bundles?: React.ReactElement<any>[] | null;
7
7
  styleGroups?: React.ReactElement<any>[][] | null;
8
8
  };
@@ -12,7 +12,7 @@ const StoreModalFamily = _ref => {
12
12
  className: "store-modal__family__container"
13
13
  }, title && /*#__PURE__*/React.createElement("h3", {
14
14
  className: "store-modal__family__title"
15
- }, title), /*#__PURE__*/React.cloneElement(familyButton, {
15
+ }, title), familyButton && /*#__PURE__*/React.cloneElement(familyButton, {
16
16
  className: 'store-modal__family__family-button'
17
17
  }), bundles === null || bundles === void 0 ? void 0 : bundles.map(bundle => /*#__PURE__*/React.cloneElement(bundle, {
18
18
  className: 'store-modal__family__bundle-button'
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ interface StoreModalIndexLoadMoreProps {
3
+ sentinelRef: (node: Element | null) => void;
4
+ onClick: () => void;
5
+ loading: boolean;
6
+ }
7
+ declare const StoreModalIndexLoadMore: React.FunctionComponent<StoreModalIndexLoadMoreProps>;
8
+ export default StoreModalIndexLoadMore;
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ // Sits after the last collection while more remain. The button is what a
3
+ // visitor sees while a page is on its way, and the way to go on without
4
+ // scrolling — with a keyboard, or if the observer never fires.
5
+ const StoreModalIndexLoadMore = _ref => {
6
+ let {
7
+ sentinelRef,
8
+ onClick,
9
+ loading
10
+ } = _ref;
11
+ return /*#__PURE__*/React.createElement("div", {
12
+ className: "store-modal__index__load-more",
13
+ ref: sentinelRef
14
+ }, /*#__PURE__*/React.createElement("button", {
15
+ type: "button",
16
+ className: "store-modal__index__load-more__button",
17
+ onClick: onClick,
18
+ disabled: loading
19
+ }, loading ? 'Loading…' : 'Load more'));
20
+ };
21
+ export default StoreModalIndexLoadMore;
@@ -79,6 +79,7 @@ import { Check, X } from '../Icons/index.js';
79
79
  import ConfigContext from '../ConfigContext.js';
80
80
  import { Price } from '../Price/index.js';
81
81
  import { sendOrderTracking } from '../Cart/orderTracking.js';
82
+ import { trackPaymentInfoEntered } from '../Cart/storefrontEvents.js';
82
83
  import { validateAddressElements, validateElements } from '../../stripeElements.js';
83
84
  import { useRecaptchaTimeout, isRecaptchaScriptLoaded, RECAPTCHA_UNAVAILABLE_MESSAGE } from '../../hooks/useRecaptchaTimeout.js';
84
85
 
@@ -287,6 +288,16 @@ export default function StoreModalUnifiedCheckout(_ref2) {
287
288
  useEffect(() => {
288
289
  sendOrderTracking(environment);
289
290
  }, [environment]);
291
+
292
+ // The Payment Element reports every keystroke; the first time the details
293
+ // are complete is the "Payment Info Entered" moment. The server keeps it to
294
+ // one per order, so this only saves requests.
295
+ const paymentInfoTracked = useRef(false);
296
+ const handlePaymentChange = useCallback(event => {
297
+ if (!event.complete || paymentInfoTracked.current) return;
298
+ paymentInfoTracked.current = true;
299
+ trackPaymentInfoEntered(environment);
300
+ }, [environment]);
290
301
  const [error, setError] = useState(null);
291
302
  const [errorsObject, setErrorsObject] = useState(null);
292
303
  const [customerErrorsObject, setCustomerErrorsObject] = useState(null);
@@ -789,6 +800,7 @@ export default function StoreModalUnifiedCheckout(_ref2) {
789
800
  }, /*#__PURE__*/React.createElement(Sentry.ErrorBoundary, {
790
801
  fallback: /*#__PURE__*/React.createElement("div", null, "Something went wrong")
791
802
  }, /*#__PURE__*/React.createElement(PaymentElement, {
803
+ onChange: handlePaymentChange,
792
804
  options: {
793
805
  layout: {
794
806
  type: 'tabs',
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+ interface UseInfiniteScrollOptions {
3
+ enabled: boolean;
4
+ onLoadMore: () => void;
5
+ rootRef?: React.RefObject<Element | null>;
6
+ rootMargin?: string;
7
+ }
8
+ /**
9
+ * Calls `onLoadMore` when a sentinel element scrolls into view, for lists that
10
+ * page as the visitor reaches the end of them.
11
+ *
12
+ * Returns a ref callback to put on that sentinel — render it after the last
13
+ * item, and only while there is more to load:
14
+ *
15
+ * const sentinelRef = useInfiniteScroll({
16
+ * enabled: hasNext && !isLoadingNext,
17
+ * onLoadMore: () => loadNext(25),
18
+ * rootRef: scrollContainer,
19
+ * });
20
+ *
21
+ * {hasNext && <div ref={sentinelRef} />}
22
+ *
23
+ * The sentinel is tracked as state rather than a ref so that mounting and
24
+ * unmounting it re-runs the effect; `onLoadMore` is read through a ref, so a
25
+ * fresh callback on every render doesn't tear the observer down and rebuild it.
26
+ */
27
+ declare const useInfiniteScroll: ({ enabled, onLoadMore, rootRef, rootMargin, }: UseInfiniteScrollOptions) => ((node: Element | null) => void);
28
+ export default useInfiniteScroll;
@@ -0,0 +1,50 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef, useState } from 'react';
4
+ /**
5
+ * Calls `onLoadMore` when a sentinel element scrolls into view, for lists that
6
+ * page as the visitor reaches the end of them.
7
+ *
8
+ * Returns a ref callback to put on that sentinel — render it after the last
9
+ * item, and only while there is more to load:
10
+ *
11
+ * const sentinelRef = useInfiniteScroll({
12
+ * enabled: hasNext && !isLoadingNext,
13
+ * onLoadMore: () => loadNext(25),
14
+ * rootRef: scrollContainer,
15
+ * });
16
+ *
17
+ * {hasNext && <div ref={sentinelRef} />}
18
+ *
19
+ * The sentinel is tracked as state rather than a ref so that mounting and
20
+ * unmounting it re-runs the effect; `onLoadMore` is read through a ref, so a
21
+ * fresh callback on every render doesn't tear the observer down and rebuild it.
22
+ */
23
+ const useInfiniteScroll = _ref => {
24
+ let {
25
+ enabled,
26
+ onLoadMore,
27
+ rootRef,
28
+ rootMargin
29
+ } = _ref;
30
+ const [sentinel, setSentinel] = useState(null);
31
+ const savedOnLoadMore = useRef(onLoadMore);
32
+ savedOnLoadMore.current = onLoadMore;
33
+ useEffect(() => {
34
+ if (!sentinel || !enabled) return;
35
+ // Absent while server-rendering, and in test environments without one.
36
+ if (typeof IntersectionObserver === 'undefined') return;
37
+ const observer = new IntersectionObserver(entries => {
38
+ if (entries.some(entry => entry.isIntersecting)) {
39
+ savedOnLoadMore.current();
40
+ }
41
+ }, {
42
+ root: (rootRef === null || rootRef === void 0 ? void 0 : rootRef.current) ?? null,
43
+ rootMargin
44
+ });
45
+ observer.observe(sentinel);
46
+ return () => observer.disconnect();
47
+ }, [sentinel, enabled, rootRef, rootMargin]);
48
+ return setSentinel;
49
+ };
50
+ export default useInfiniteScroll;
package/dist/fontdue.css CHANGED
@@ -2017,6 +2017,12 @@ input:checked + .checkbox__icon {
2017
2017
  display: none;
2018
2018
  }
2019
2019
 
2020
+ .store-modal__family-button__styles-price {
2021
+ color: var(--secondary_text_color);
2022
+ text-decoration: line-through;
2023
+ margin-right: 0.5em;
2024
+ }
2025
+
2020
2026
  .store-modal__family-button__name {
2021
2027
  font-size: 32px;
2022
2028
  line-height: normal;
@@ -2038,6 +2044,39 @@ input:checked + .checkbox__icon {
2038
2044
  font-size: 20px;
2039
2045
  }
2040
2046
 
2047
+ .store-modal__index__load-more {
2048
+ grid-column: 1/-1;
2049
+ padding-bottom: 20px;
2050
+ text-align: center;
2051
+ }
2052
+
2053
+ .store-modal__index__load-more__button {
2054
+ background: none;
2055
+ color: inherit;
2056
+ font: inherit;
2057
+ text-decoration: inherit;
2058
+ text-align: inherit;
2059
+ border: 0;
2060
+ border-radius: 0;
2061
+ padding: 0;
2062
+ margin: 0;
2063
+ appearance: none;
2064
+ text-transform: inherit;
2065
+ }
2066
+ .store-modal__index__load-more__button:active, .store-modal__index__load-more__button:focus {
2067
+ outline: none;
2068
+ }
2069
+ .store-modal__index__load-more__button:not(:disabled) {
2070
+ cursor: pointer;
2071
+ }
2072
+ .store-modal__index__load-more__button {
2073
+ color: var(--secondary_text_color);
2074
+ text-decoration: underline;
2075
+ }
2076
+ .store-modal__index__load-more__button:disabled {
2077
+ text-decoration: none;
2078
+ }
2079
+
2041
2080
  .store-modal__page__body:has(> .store-modal__index-item__button) {
2042
2081
  display: grid;
2043
2082
  grid-template-columns: auto 1fr auto;
@@ -8,6 +8,7 @@ export interface UseAutofitProps {
8
8
  fontWeight?: React.CSSProperties['fontWeight'];
9
9
  fontStyle?: string;
10
10
  fontVariationSettings?: string;
11
+ fontFeatureSettings?: string;
11
12
  enabled?: boolean;
12
13
  padding?: number;
13
14
  }
@@ -16,5 +17,5 @@ export interface UseAutofitResult {
16
17
  fontSize: number;
17
18
  ready: boolean;
18
19
  }
19
- declare const useAutofit: ({ text, fontFamily, fontSize: externalFontSize, min, max, letterSpacing, fontWeight, fontStyle, fontVariationSettings, enabled, padding, }: UseAutofitProps) => UseAutofitResult;
20
+ declare const useAutofit: ({ text, fontFamily, fontSize: externalFontSize, min, max, letterSpacing, fontWeight, fontStyle, fontVariationSettings, fontFeatureSettings, enabled, padding, }: UseAutofitProps) => UseAutofitResult;
20
21
  export default useAutofit;
@@ -9,7 +9,8 @@ function getCanvasContext() {
9
9
  return sharedCanvas.getContext('2d');
10
10
  }
11
11
 
12
- // DOM measurement (fallback for variable fonts)
12
+ // DOM measurement (fallback for variable fonts and OpenType features, which
13
+ // canvas can't express: `ctx.font` takes neither variation nor feature settings)
13
14
  let measureEl = null;
14
15
  function getMeasureElement() {
15
16
  if (!measureEl) {
@@ -31,7 +32,7 @@ function measureWithCanvas(text, fontFamily, fontWeight, fontStyleValue, letterS
31
32
  ctx.letterSpacing = '0px';
32
33
  return width;
33
34
  }
34
- function measureWithDOM(text, fontFamily, fontWeight, fontStyleValue, letterSpacing, fontVariationSettings) {
35
+ function measureWithDOM(text, fontFamily, fontWeight, fontStyleValue, letterSpacing, fontVariationSettings, fontFeatureSettings) {
35
36
  const el = getMeasureElement();
36
37
  el.style.fontFamily = fontFamily;
37
38
  el.style.fontWeight = `${fontWeight}`;
@@ -39,6 +40,7 @@ function measureWithDOM(text, fontFamily, fontWeight, fontStyleValue, letterSpac
39
40
  el.style.fontSize = `${REF_SIZE}px`;
40
41
  el.style.letterSpacing = `${letterSpacing}em`;
41
42
  el.style.fontVariationSettings = fontVariationSettings;
43
+ el.style.fontFeatureSettings = fontFeatureSettings;
42
44
  el.textContent = text;
43
45
  return el.getBoundingClientRect().width;
44
46
  }
@@ -53,6 +55,7 @@ const useAutofit = _ref => {
53
55
  fontWeight = 400,
54
56
  fontStyle = 'normal',
55
57
  fontVariationSettings,
58
+ fontFeatureSettings,
56
59
  enabled = true,
57
60
  padding = 0
58
61
  } = _ref;
@@ -83,7 +86,10 @@ const useAutofit = _ref => {
83
86
  // Split by newlines and measure the widest line, since the rendered
84
87
  // text preserves line breaks (Draft.js uses white-space: pre-wrap).
85
88
  const lines = text.split('\n');
86
- const measure = fontVariationSettings ? line => measureWithDOM(line, fontFamily, fontWeight, fontStyle, letterSpacing, fontVariationSettings) : line => measureWithCanvas(line, fontFamily, fontWeight, fontStyle, letterSpacing);
89
+ // Enabled features can change glyph widths (stylistic sets, small caps,
90
+ // ligatures), so the text has to be measured with them applied. The
91
+ // measure element is shared, so both settings are written on every call.
92
+ const measure = fontVariationSettings || fontFeatureSettings ? line => measureWithDOM(line, fontFamily, fontWeight, fontStyle, letterSpacing, fontVariationSettings || 'normal', fontFeatureSettings || 'normal') : line => measureWithCanvas(line, fontFamily, fontWeight, fontStyle, letterSpacing);
87
93
  const refWidth = Math.max(...lines.map(measure));
88
94
  if (refWidth <= 0) return;
89
95
 
@@ -92,7 +98,7 @@ const useAutofit = _ref => {
92
98
  const newSize = Math.min(Math.max(min, Math.floor(availableWidth / (refWidth / REF_SIZE))), max);
93
99
  setFontSize(newSize);
94
100
  if (!measured) setMeasured(true);
95
- }, [text, fontFamily, fontWeight, fontStyle, fontVariationSettings, letterSpacing, containerWidth, enabled, min, max, padding, fontVersion]);
101
+ }, [text, fontFamily, fontWeight, fontStyle, fontVariationSettings, fontFeatureSettings, letterSpacing, containerWidth, enabled, min, max, padding, fontVersion]);
96
102
  if (!enabled) return {
97
103
  ref,
98
104
  fontSize: externalFontSize,
package/dist/reducer.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { CollectionStyleSkusMap } from './components/Root/productState.js';
1
+ import { SkuStyleIds } from './components/Root/productState.js';
2
2
  import { Reducer } from 'redux';
3
3
  import { productState_Query$data } from './__generated__/productState_Query.graphql.js';
4
4
  import type { RouteConfig } from './components/StoreModal/routes.js';
5
- import { collectionSkuIdsDifferences, isSelected } from './utils.js';
5
+ import { isSelected } from './selection.js';
6
6
  import { Config } from './components/ConfigContext.js';
7
- export { collectionSkuIdsDifferences, isSelected };
7
+ export { isSelected };
8
8
  interface LicenseOptionSpec {
9
9
  licenseId: string;
10
10
  licenseOptions: {
@@ -22,11 +22,16 @@ export interface FontdueState {
22
22
  selectedSkuIds: {
23
23
  [skuId: string]: boolean;
24
24
  };
25
+ requestedSkuIds: string[];
25
26
  licenseOptions: LicenseOptionSpec[];
26
- collectionStyleSkus: CollectionStyleSkusMap;
27
+ skuStyleIds: SkuStyleIds;
27
28
  collectionSkus: {
28
29
  [collectionSkuId: string]: string | null;
29
30
  };
31
+ productSkuIds: {
32
+ [productId: string]: string[];
33
+ };
34
+ viewedProductId: string | null;
30
35
  skuPrices: {
31
36
  [collectionSkuId: string]: number;
32
37
  };
@@ -87,6 +92,9 @@ export type FontdueAction = {
87
92
  variables?: object;
88
93
  } | {
89
94
  type: 'STORE_MODAL_BACK';
95
+ } | {
96
+ type: 'STORE_MODAL_VIEW_PRODUCT';
97
+ productId: string | null;
90
98
  } | {
91
99
  type: 'UPDATE_ORDER_VARIABLE_SELECTIONS';
92
100
  selections: OrderVariableSelectionState[];
@@ -94,7 +102,6 @@ export type FontdueAction = {
94
102
  type: 'UPDATE_LICENSEE_IS_BILLING_IDENTITY';
95
103
  value: boolean;
96
104
  };
97
- export declare const collectionSkuIdWithDiscount: (state: FontdueState, skuId: string) => [string, number] | [null, null];
98
105
  declare const reducer: Reducer<FontdueState, FontdueAction>;
99
106
  export declare function createDefaultStore(config: Config | undefined): import("redux").Store<FontdueState, FontdueAction, unknown>;
100
107
  export default reducer;
package/dist/reducer.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { populateProductStateReducer } from './components/Root/productState.js';
2
2
  import { createStore } from 'redux';
3
3
  import { shallowEqual } from 'react-redux';
4
- import { collectionSkuIdsDifferences, collTransitivelyContainsSkuId, isSelected } from './utils.js';
4
+ import { isSelected, repriceSelection, selectSku } from './selection.js';
5
5
  import { makeConfig } from './components/ConfigContext.js';
6
- export { collectionSkuIdsDifferences, isSelected };
6
+ export { isSelected };
7
7
  const unselectLicense = (state, action) => {
8
8
  if (!(action.type === 'PRECART_UNSELECT_LICENSE' || action.type === 'PRECART_TOGGLE_LICENSE')) return state;
9
9
  return {
@@ -61,96 +61,17 @@ const selectLicenseOption = (state, action) => {
61
61
  })
62
62
  };
63
63
  };
64
- export const collectionSkuIdWithDiscount = (state, skuId) => {
65
- const differences = collectionSkuIdsDifferences(state, skuId);
66
- const collectionId = Object.keys(differences).filter(collSkuId => differences[collSkuId] <= state.skuPrices[skuId]).reduce((smallest, key) => {
67
- if (!smallest || differences[key] < differences[smallest]) return key;
68
- if (differences[key] === differences[smallest]) {
69
- // When differences are equal, prefer a collection that is not the
70
- // SKU being selected — this ensures collection bundles are preferred
71
- // over selecting the family itself at the same price
72
- if (smallest === skuId && key !== skuId) return key;
73
- // Among non-self candidates, prefer the one covering more unique
74
- // font styles — this ensures the most comprehensive collection wins
75
- if (smallest !== skuId && key !== skuId) {
76
- var _state$collectionStyl, _state$collectionStyl2;
77
- const smallestStyles = new Set((_state$collectionStyl = state.collectionStyleSkus[smallest]) === null || _state$collectionStyl === void 0 ? void 0 : _state$collectionStyl.fontStyleSkuIds).size;
78
- const keyStyles = new Set((_state$collectionStyl2 = state.collectionStyleSkus[key]) === null || _state$collectionStyl2 === void 0 ? void 0 : _state$collectionStyl2.fontStyleSkuIds).size;
79
- if (keyStyles > smallestStyles) return key;
80
- }
81
- }
82
- return smallest;
83
- }, null);
84
- if (collectionId) return [collectionId, differences[collectionId]];
85
- return [null, null];
86
- };
87
- const selectedCollectionSkuIdContainingSkuId = (state, skuId) => Object.keys(state.selectedSkuIds).filter(id => state.selectedSkuIds[id] === true).find(selectedSkuId => collTransitivelyContainsSkuId(state.collectionStyleSkus, selectedSkuId, skuId));
88
- const setKeysFalse = (acc, key) => {
89
- acc[key] = false;
90
- return acc;
91
- };
92
- const unselectedMembersSkuIds = (state, action) => {
93
- if (action.type !== 'SELECT_SKU_ID') throw new Error('Wrong action ${action.type}');
94
- const {
95
- skuId,
96
- selected
97
- } = action;
98
- if (!selected) return {}; // only do this in case we are selecting something
99
-
100
- // Recursively collect all descendant SKU IDs so that transitive
101
- // selections (e.g., collection bundle → family → bundle) are all
102
- // marked as false
103
- const result = {};
104
- const collectMembers = collSkuId => {
105
- const members = state.collectionStyleSkus[collSkuId];
106
- if (!members) return;
107
- for (const id of members.fontStyleSkuIds) {
108
- result[id] = false;
109
- }
110
- for (const id of members.childrenSkuIds) {
111
- result[id] = false;
112
- collectMembers(id);
113
- }
114
- };
115
- collectMembers(skuId);
116
- return result;
117
- };
118
64
  const selectSkuId = (state, action) => {
119
65
  if (action.type !== 'SELECT_SKU_ID') return state;
120
- const {
121
- skuId,
122
- selected
123
- } = action;
124
- let actualSkuId = skuId;
125
- if (selected) {
126
- // check if selecting any collections instead of this sku
127
- // would reduce the selection price
128
- const [collectionSkuId, _amount] = collectionSkuIdWithDiscount(state, skuId);
129
- actualSkuId = collectionSkuId || actualSkuId;
130
- } else if (state.selectedSkuIds[skuId] === false) {
131
- // we're clicking "deselect" on an item that's not actually selected,
132
- // it's a member of a collection which is selected. in this case
133
- // we'll find the collection and deselect that collection instead
134
- const parentId = selectedCollectionSkuIdContainingSkuId(state, skuId);
135
- if (parentId) actualSkuId = parentId;
136
- }
137
66
  return {
138
- ...state,
139
- precartOpen: true,
140
- selectedSkuIds: {
141
- ...state.selectedSkuIds,
142
- [actualSkuId]: selected,
143
- ...unselectedMembersSkuIds(state, {
144
- ...action,
145
- skuId: actualSkuId,
146
- selected
147
- })
148
- }
67
+ ...selectSku(state, action.skuId, action.selected),
68
+ precartOpen: true
149
69
  };
150
70
  };
151
71
  const unselectSkus = state => ({
152
72
  ...state,
153
- selectedSkuIds: {}
73
+ selectedSkuIds: {},
74
+ requestedSkuIds: []
154
75
  });
155
76
  const storeModalNavigate = (state, action) => {
156
77
  if (action.type !== 'STORE_MODAL_NAVIGATE') return state;
@@ -197,7 +118,7 @@ const reducer = (state, action) => {
197
118
  if (!state) throw 'Reducer called without state';
198
119
  switch (action.type) {
199
120
  case 'POPULATE_PRODUCT_STATE':
200
- return populateProductStateReducer(state, action);
121
+ return repriceSelection(populateProductStateReducer(state, action));
201
122
  case 'PRECART_TOGGLE_LICENSE':
202
123
  return toggleLicense(state, action);
203
124
  case 'PRECART_UNSELECT_LICENSE':
@@ -236,6 +157,13 @@ const reducer = (state, action) => {
236
157
  return storeModalReplace(state, action);
237
158
  case 'STORE_MODAL_BACK':
238
159
  return storeModalBack(state);
160
+ // The product page being viewed bounds what a pick on it may sit inside
161
+ // (selection.ts), so a change of page re-prices the selection.
162
+ case 'STORE_MODAL_VIEW_PRODUCT':
163
+ return state.viewedProductId === action.productId ? state : repriceSelection({
164
+ ...state,
165
+ viewedProductId: action.productId
166
+ });
239
167
  case 'UPDATE_ORDER_VARIABLE_SELECTIONS':
240
168
  return updateOrderVariableSelections(state, action);
241
169
  case 'UPDATE_LICENSEE_IS_BILLING_IDENTITY':
@@ -260,12 +188,15 @@ export function createDefaultStore(config) {
260
188
  return createStore(reducer, {
261
189
  selectedSkuIds: {},
262
190
  // { [skuId]: Boolean }
191
+ requestedSkuIds: [],
263
192
  licenseOptions: [],
264
193
  cartOpen: false,
265
194
  precartOpen: false,
266
- collectionStyleSkus: {},
195
+ skuStyleIds: {},
267
196
  skuPrices: {},
268
197
  collectionSkus: {},
198
+ productSkuIds: {},
199
+ viewedProductId: null,
269
200
  fetchedCollectionIds: [],
270
201
  storeModalRoute: initialRoute,
271
202
  storeModalHistory: [],
@@ -0,0 +1,44 @@
1
+ import type { FontdueState } from './reducer.js';
2
+ type Catalog = Pick<FontdueState, 'skuStyleIds' | 'skuPrices'>;
3
+ interface ViewedProduct {
4
+ id: string;
5
+ skuIds: ReadonlySet<string>;
6
+ }
7
+ /**
8
+ * The cheapest set of SKUs in which each of `picks` sits inside one SKU
9
+ * granting all of its styles, sorted. Picks whose styles or price aren't known
10
+ * are left out.
11
+ *
12
+ * @example
13
+ * cheapestSkuIds(['sku:bold', 'sku:regular', 'sku:light'], catalog)
14
+ * // => ['sku:light+regular+bold'] rather than a pair plus a single
15
+ */
16
+ export declare const cheapestSkuIds: (picks: readonly string[], catalog: Catalog, viewed?: ViewedProduct | null) => string[];
17
+ /**
18
+ * Re-derives `selectedSkuIds` from the picks. A pick whose styles or price
19
+ * aren't known yet – selected from the host page before its product data
20
+ * arrived – is bought as it is until they are.
21
+ */
22
+ export declare const repriceSelection: (state: FontdueState) => FontdueState;
23
+ /**
24
+ * Adds a pick, or takes out every pick sharing a style with the clicked SKU.
25
+ *
26
+ * If what's left is still cheapest bought with a SKU granting the clicked one's
27
+ * styles anyway – it's part of a bundle that costs no more without it – the
28
+ * click would change nothing, so the picks inside the selected SKUs granting
29
+ * them go too, as deselecting a member of a selected bundle always has.
30
+ */
31
+ export declare const selectSku: (state: FontdueState, skuId: string, selected: boolean) => FontdueState;
32
+ /**
33
+ * Whether a SKU – or a style with no SKU of its own – is in the selection:
34
+ * selected itself, or every style it grants is granted by what is selected.
35
+ */
36
+ export declare const isSelected: (skuIdOrStyleId: string | null) => (state: FontdueState) => boolean;
37
+ /**
38
+ * What picking a SKU would add to the selection's price: the cheapest
39
+ * combination with it, less the current one. Never more than the SKU's own
40
+ * price, and less when it completes a cheaper bundle – the Store Modal's
41
+ * “+$X”. Null when the SKU's styles or price aren't known.
42
+ */
43
+ export declare const additionalCost: (state: FontdueState, skuId: string) => number | null;
44
+ export {};