fontdue-js 2.19.1 → 2.20.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 (125) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +11 -10
  3. package/dist/__generated__/CartItemProduct_product.graphql.d.ts +1 -7
  4. package/dist/__generated__/CartItemProduct_product.graphql.js +11 -36
  5. package/dist/__generated__/CartOrderCompleteOrderMutation.graphql.d.ts +1 -1
  6. package/dist/__generated__/CartOrderCompleteOrderMutation.graphql.js +65 -23
  7. package/dist/__generated__/CartOrderRemoveDiscountMutation.graphql.d.ts +1 -1
  8. package/dist/__generated__/CartOrderRemoveDiscountMutation.graphql.js +65 -23
  9. package/dist/__generated__/CartOrderUpdateMutation.graphql.d.ts +1 -1
  10. package/dist/__generated__/CartOrderUpdateMutation.graphql.js +65 -23
  11. package/dist/__generated__/CartQuery.graphql.d.ts +1 -1
  12. package/dist/__generated__/CartQuery.graphql.js +119 -87
  13. package/dist/__generated__/CartStateUpdateMutation.graphql.d.ts +1 -1
  14. package/dist/__generated__/CartStateUpdateMutation.graphql.js +65 -23
  15. package/dist/__generated__/CharacterViewerIDQuery.graphql.d.ts +1 -1
  16. package/dist/__generated__/CharacterViewerIDQuery.graphql.js +40 -26
  17. package/dist/__generated__/CharacterViewerSlugQuery.graphql.d.ts +1 -1
  18. package/dist/__generated__/CharacterViewerSlugQuery.graphql.js +40 -26
  19. package/dist/__generated__/CharacterViewerStyleRefetchQuery.graphql.d.ts +1 -1
  20. package/dist/__generated__/CharacterViewerStyleRefetchQuery.graphql.js +62 -41
  21. package/dist/__generated__/CharacterViewer_collection.graphql.d.ts +1 -3
  22. package/dist/__generated__/CharacterViewer_collection.graphql.js +6 -13
  23. package/dist/__generated__/CharacterViewer_family.graphql.d.ts +1 -2
  24. package/dist/__generated__/CharacterViewer_family.graphql.js +2 -8
  25. package/dist/__generated__/CharacterViewer_style.graphql.d.ts +2 -1
  26. package/dist/__generated__/CharacterViewer_style.graphql.js +6 -2
  27. package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.d.ts +1 -1
  28. package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.js +65 -23
  29. package/dist/__generated__/CheckoutUpdateOrderMutation.graphql.d.ts +1 -1
  30. package/dist/__generated__/CheckoutUpdateOrderMutation.graphql.js +65 -23
  31. package/dist/__generated__/CollectionAa_Query.graphql.d.ts +1 -1
  32. package/dist/__generated__/CollectionAa_Query.graphql.js +57 -3
  33. package/dist/__generated__/Family_node.graphql.d.ts +1 -2
  34. package/dist/__generated__/Family_node.graphql.js +2 -8
  35. package/dist/__generated__/FontFamiliesQuery.graphql.d.ts +1 -1
  36. package/dist/__generated__/FontFamiliesQuery.graphql.js +80 -31
  37. package/dist/__generated__/FontStyle_fontStyle.graphql.d.ts +2 -3
  38. package/dist/__generated__/FontStyle_fontStyle.graphql.js +4 -12
  39. package/dist/__generated__/PrecartAddToCartMutation.graphql.d.ts +1 -1
  40. package/dist/__generated__/PrecartAddToCartMutation.graphql.js +71 -29
  41. package/dist/__generated__/ServerConfigProviderQuery.graphql.d.ts +24 -0
  42. package/dist/__generated__/ServerConfigProviderQuery.graphql.js +108 -0
  43. package/dist/__generated__/StoreModalCartQuery.graphql.d.ts +1 -1
  44. package/dist/__generated__/StoreModalCartQuery.graphql.js +108 -76
  45. package/dist/__generated__/StoreModalContainerQuery.graphql.d.ts +4 -7
  46. package/dist/__generated__/StoreModalContainerQuery.graphql.js +58 -25
  47. package/dist/__generated__/StoreModalFamily_collection.graphql.d.ts +1 -2
  48. package/dist/__generated__/StoreModalFamily_collection.graphql.js +2 -8
  49. package/dist/__generated__/StoreModalIndexItem_fontCollection.graphql.d.ts +1 -4
  50. package/dist/__generated__/StoreModalIndexItem_fontCollection.graphql.js +2 -17
  51. package/dist/__generated__/StoreModalIndexQuery.graphql.d.ts +1 -1
  52. package/dist/__generated__/StoreModalIndexQuery.graphql.js +48 -9
  53. package/dist/__generated__/StoreModalProductQuery.graphql.d.ts +1 -1
  54. package/dist/__generated__/StoreModalProductQuery.graphql.js +85 -41
  55. package/dist/__generated__/StoreModalProductRefetchQuery.graphql.d.ts +1 -1
  56. package/dist/__generated__/StoreModalProductRefetchQuery.graphql.js +78 -34
  57. package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.d.ts +1 -1
  58. package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.js +65 -23
  59. package/dist/__generated__/TypeTesterStandaloneChangedStylesQuery.graphql.d.ts +1 -1
  60. package/dist/__generated__/TypeTesterStandaloneChangedStylesQuery.graphql.js +55 -7
  61. package/dist/__generated__/TypeTesterStandaloneQuery.graphql.d.ts +1 -1
  62. package/dist/__generated__/TypeTesterStandaloneQuery.graphql.js +56 -8
  63. package/dist/__generated__/TypeTester_fontStyle.graphql.d.ts +1 -4
  64. package/dist/__generated__/TypeTester_fontStyle.graphql.js +2 -17
  65. package/dist/__generated__/TypeTestersChangedStylesQuery.graphql.d.ts +1 -1
  66. package/dist/__generated__/TypeTestersChangedStylesQuery.graphql.js +55 -7
  67. package/dist/__generated__/TypeTestersIDQuery.graphql.d.ts +1 -1
  68. package/dist/__generated__/TypeTestersIDQuery.graphql.js +55 -7
  69. package/dist/__generated__/TypeTestersRefetchQuery.graphql.d.ts +1 -1
  70. package/dist/__generated__/TypeTestersRefetchQuery.graphql.js +55 -7
  71. package/dist/__generated__/TypeTestersSlugQuery.graphql.d.ts +1 -1
  72. package/dist/__generated__/TypeTestersSlugQuery.graphql.js +55 -7
  73. package/dist/__generated__/useFontStyle_fontStyle.graphql.d.ts +28 -0
  74. package/dist/__generated__/useFontStyle_fontStyle.graphql.js +94 -0
  75. package/dist/__tests__/collectionBundleSelection.test.js +453 -1
  76. package/dist/components/Cart/CartItem/CartItemProduct.js +3 -9
  77. package/dist/components/CharacterViewer/index.js +5 -11
  78. package/dist/components/ConfigContext.d.ts +13 -0
  79. package/dist/components/ConfigContext.js +6 -2
  80. package/dist/components/ConsentBanner/consent.d.ts +18 -0
  81. package/dist/components/ConsentBanner/consent.js +93 -0
  82. package/dist/components/ConsentBanner/index.d.ts +3 -0
  83. package/dist/components/ConsentBanner/index.js +84 -0
  84. package/dist/components/Family/index.js +2 -5
  85. package/dist/components/FontStyle/index.js +2 -8
  86. package/dist/components/FontdueProvider/FontdueProviderClientComponent.js +14 -1
  87. package/dist/components/FontdueProvider/index.js +1 -2
  88. package/dist/components/FontdueProvider/index.server.js +1 -2
  89. package/dist/components/ServerConfigProvider/index.d.ts +8 -0
  90. package/dist/components/ServerConfigProvider/index.js +41 -0
  91. package/dist/components/StoreModal/StoreModalContainer.js +15 -17
  92. package/dist/components/StoreModal/StoreModalFamily.js +1 -5
  93. package/dist/components/StoreModal/StoreModalIndexItem.js +6 -11
  94. package/dist/components/Tracking/index.d.ts +2 -0
  95. package/dist/components/Tracking/index.js +166 -0
  96. package/dist/components/TypeTester/TypeTesterSlider.js +10 -3
  97. package/dist/components/TypeTester/index.js +2 -5
  98. package/dist/components/TypeTester/useTypeTesterStyler.js +27 -85
  99. package/dist/components/UrlContext.d.ts +3 -0
  100. package/dist/components/UrlContext.js +15 -0
  101. package/dist/components/elements/StoreModalContainer/index.d.ts +2 -6
  102. package/dist/components/elements/StoreModalContainer/index.js +3 -8
  103. package/dist/components/elements/StoreModalFamily/index.d.ts +0 -1
  104. package/dist/components/elements/StoreModalFamily/index.js +1 -2
  105. package/dist/components/useConsent.d.ts +9 -0
  106. package/dist/components/useConsent.js +26 -0
  107. package/dist/components/useFont.d.ts +29 -0
  108. package/dist/components/useFont.js +77 -0
  109. package/dist/components/useFontLoaded.d.ts +24 -0
  110. package/dist/components/useFontLoaded.js +60 -0
  111. package/dist/components/useFontStyle.d.ts +3 -4
  112. package/dist/components/useFontStyle.js +24 -16
  113. package/dist/deepMerge.d.ts +4 -0
  114. package/dist/deepMerge.js +24 -0
  115. package/dist/fontLoader.d.ts +13 -0
  116. package/dist/fontLoader.js +55 -0
  117. package/dist/fontdue.css +65 -0
  118. package/dist/hooks/useAutofit.d.ts +20 -0
  119. package/dist/hooks/useAutofit.js +114 -0
  120. package/dist/react-ranger.js +5 -1
  121. package/dist/reducer.d.ts +0 -4
  122. package/dist/reducer.js +32 -21
  123. package/dist/utils.d.ts +3 -0
  124. package/dist/utils.js +19 -8
  125. package/package.json +5 -2
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _react = require("react");
8
+ const FALLBACK_FAMILY = 'Fallback';
9
+
10
+ /**
11
+ * Hook for external consumers (npm clients) to detect when a font is loaded.
12
+ *
13
+ * Fonts can be loaded by CSS @font-face rules (e.g. via PreloadWebfonts) or
14
+ * by fontdue-js components via the FontFace API. Uses document.fonts.load()
15
+ * to force the browser to load matching fonts even when no DOM text uses
16
+ * the font-family yet.
17
+ *
18
+ * Usage:
19
+ * const { style, loaded } = useFontLoaded({ fontFamily: "Helvetica Now Regular" });
20
+ */
21
+ const useFontLoaded = _ref => {
22
+ let {
23
+ fontFamily
24
+ } = _ref;
25
+ const [loaded, setLoaded] = (0, _react.useState)(false);
26
+ (0, _react.useEffect)(() => {
27
+ let cancelled = false;
28
+ const tryLoad = () => {
29
+ document.fonts.load(`400 16px "${fontFamily}"`).then(fontFaces => {
30
+ if (!cancelled && fontFaces.length > 0) setLoaded(true);
31
+ }).catch(() => {});
32
+ };
33
+
34
+ // Try immediately (works if @font-face exists or FontFace already added)
35
+ tryLoad();
36
+
37
+ // Also listen for future font loads (e.g., fontdue-js component adds
38
+ // FontFace via API after this hook mounts)
39
+ document.fonts.addEventListener('loadingdone', tryLoad);
40
+ return () => {
41
+ cancelled = true;
42
+ document.fonts.removeEventListener('loadingdone', tryLoad);
43
+ };
44
+ }, [fontFamily]);
45
+ const cssStyle = loaded ? {
46
+ fontFamily: `"${fontFamily}", ${FALLBACK_FAMILY}`,
47
+ fontWeight: 400,
48
+ fontStyle: 'normal'
49
+ } : {
50
+ fontFamily: FALLBACK_FAMILY,
51
+ fontWeight: 'normal',
52
+ fontStyle: 'normal'
53
+ };
54
+ return {
55
+ style: cssStyle,
56
+ loaded
57
+ };
58
+ };
59
+ var _default = useFontLoaded;
60
+ exports.default = _default;
@@ -1,10 +1,9 @@
1
1
  import React from 'react';
2
+ import { useFontStyle_fontStyle$key } from '../__generated__/useFontStyle_fontStyle.graphql';
2
3
  interface UseFontStyle_props {
3
- fontFamily: string;
4
- fontWeight?: string;
5
- fontStyle?: string;
4
+ fontStyle: useFontStyle_fontStyle$key | null | undefined;
6
5
  }
7
- declare const useFontStyle: ({ fontFamily, fontWeight, fontStyle, }: UseFontStyle_props) => {
6
+ declare const useFontStyle: ({ fontStyle: fontStyleKey, }: UseFontStyle_props) => {
8
7
  style: React.CSSProperties;
9
8
  loaded: boolean;
10
9
  };
@@ -4,36 +4,44 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = void 0;
7
- var _fontfaceobserver = _interopRequireDefault(require("fontfaceobserver"));
7
+ var _useFontStyle_fontStyle2 = _interopRequireDefault(require("../__generated__/useFontStyle_fontStyle.graphql"));
8
8
  var _react = require("react");
9
+ var _reactRelay = require("react-relay");
10
+ var _fontLoader = require("../fontLoader");
9
11
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
10
12
  const FALLBACK_FAMILY = 'Fallback';
11
13
  const useFontStyle = _ref => {
12
14
  let {
13
- fontFamily,
14
- fontWeight,
15
- fontStyle
15
+ fontStyle: fontStyleKey
16
16
  } = _ref;
17
+ const fontStyle = (0, _reactRelay.useFragment)((_useFontStyle_fontStyle2.default.hash && _useFontStyle_fontStyle2.default.hash !== "cc36ae8da5627792c27e9d3ad2b07258" && console.error("The definition of 'useFontStyle_fontStyle' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _useFontStyle_fontStyle2.default), fontStyleKey ?? null);
18
+ const fontFamily = fontStyle ? `${fontStyle.cssFamily} ${fontStyle.name}` : null;
19
+ const sources = fontStyle === null || fontStyle === void 0 ? void 0 : fontStyle.webfontSources;
20
+ const verticalMetrics = fontStyle === null || fontStyle === void 0 ? void 0 : fontStyle.verticalMetrics;
21
+
22
+ // Always initialize as false to avoid SSR/client hydration mismatch.
23
+ // The effect below will detect already-loaded fonts on mount.
17
24
  const [loaded, setLoaded] = (0, _react.useState)(false);
18
25
  const mounted = (0, _react.useRef)(false);
19
- (0, _react.useEffect)(() => {
20
- setLoaded(false);
21
- const observer = new _fontfaceobserver.default(fontFamily, {
22
- weight: fontWeight,
23
- style: fontStyle
24
- });
25
- observer.load(null, 90000).then(() => {
26
- // since there's no easy way to cancel the promise...
27
- if (mounted.current) setLoaded(true);
28
- }).catch(console.error);
29
- }, [fontFamily, fontWeight, fontStyle]);
30
26
  (0, _react.useEffect)(() => {
31
27
  mounted.current = true;
32
28
  return () => {
33
29
  mounted.current = false;
34
30
  };
35
31
  }, []);
36
- const cssStyle = loaded ? {
32
+ (0, _react.useEffect)(() => {
33
+ if (!fontFamily) return;
34
+ if ((0, _fontLoader.isFontLoaded)(fontFamily)) {
35
+ setLoaded(true);
36
+ return;
37
+ }
38
+ if (!sources || sources.length === 0) return;
39
+ setLoaded(false);
40
+ (0, _fontLoader.loadFont)(fontFamily, sources, verticalMetrics).then(() => {
41
+ if (mounted.current) setLoaded(true);
42
+ }).catch(console.error);
43
+ }, [fontFamily]);
44
+ const cssStyle = loaded && fontFamily ? {
37
45
  fontFamily: `"${fontFamily}", ${FALLBACK_FAMILY}`,
38
46
  fontWeight: 400,
39
47
  fontStyle: 'normal'
@@ -0,0 +1,4 @@
1
+ type Obj = Record<string, unknown>;
2
+ /** Deep merge two objects. Properties from `overlay` take precedence. */
3
+ export declare function deepMerge<T extends Obj>(base: Obj, overlay: Obj): T;
4
+ export {};
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.deepMerge = deepMerge;
7
+ function isPlainObject(value) {
8
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
9
+ }
10
+
11
+ /** Deep merge two objects. Properties from `overlay` take precedence. */
12
+ function deepMerge(base, overlay) {
13
+ const result = {
14
+ ...base
15
+ };
16
+ for (const key of Object.keys(overlay)) {
17
+ if (isPlainObject(result[key]) && isPlainObject(overlay[key])) {
18
+ result[key] = deepMerge(result[key], overlay[key]);
19
+ } else {
20
+ result[key] = overlay[key];
21
+ }
22
+ }
23
+ return result;
24
+ }
@@ -0,0 +1,13 @@
1
+ interface FontSource {
2
+ readonly url: string | null;
3
+ readonly format: string | null;
4
+ }
5
+ interface VerticalMetrics {
6
+ readonly unitsPerEm: number;
7
+ readonly ascender: number;
8
+ readonly descender: number;
9
+ readonly lineGap: number | null;
10
+ }
11
+ export declare function isFontLoaded(fontFamily: string): boolean;
12
+ export declare function loadFont(fontFamily: string, sources: ReadonlyArray<FontSource | null>, verticalMetrics?: VerticalMetrics | null): Promise<FontFace | null>;
13
+ export {};
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.isFontLoaded = isFontLoaded;
7
+ exports.loadFont = loadFont;
8
+ const cache = new Map();
9
+ const loaded = new Set();
10
+ function isFontLoaded(fontFamily) {
11
+ return loaded.has(fontFamily);
12
+ }
13
+ async function loadFont(fontFamily, sources, verticalMetrics) {
14
+ // Return cached promise if loading is in progress or complete
15
+ const existing = cache.get(fontFamily);
16
+ if (existing) return existing;
17
+ const promise = doLoadFont(fontFamily, sources, verticalMetrics);
18
+ cache.set(fontFamily, promise);
19
+ promise.catch(() => {
20
+ cache.delete(fontFamily);
21
+ loaded.delete(fontFamily);
22
+ });
23
+ return promise;
24
+ }
25
+ async function doLoadFont(fontFamily, sources, verticalMetrics) {
26
+ const validSources = sources.filter(s => s != null && s.url != null);
27
+ const source = validSources.find(s => s.format === 'woff2') ?? validSources[0];
28
+ if (!source || !source.url) throw new Error(`No font sources for ${fontFamily}`);
29
+ const response = await fetch(source.url);
30
+ if (!response.ok) throw new Error(`Failed to fetch font: ${response.status}`);
31
+ const buffer = await response.arrayBuffer();
32
+ const descriptors = {
33
+ weight: '400',
34
+ style: 'normal',
35
+ display: 'block'
36
+ };
37
+ if (verticalMetrics && verticalMetrics.unitsPerEm > 0) {
38
+ const {
39
+ unitsPerEm,
40
+ ascender,
41
+ descender,
42
+ lineGap
43
+ } = verticalMetrics;
44
+ descriptors.ascentOverride = `${ascender / unitsPerEm * 100}%`;
45
+ descriptors.descentOverride = `${descender / unitsPerEm * -100}%`;
46
+ if (lineGap != null) {
47
+ descriptors.lineGapOverride = `${lineGap / unitsPerEm * 100}%`;
48
+ }
49
+ }
50
+ const fontFace = new FontFace(fontFamily, buffer, descriptors);
51
+ await fontFace.load();
52
+ document.fonts.add(fontFace);
53
+ loaded.add(fontFamily);
54
+ return fontFace;
55
+ }
package/dist/fontdue.css CHANGED
@@ -876,6 +876,71 @@ div[data-component=TypeTesters] {
876
876
  opacity: 0.5;
877
877
  }
878
878
 
879
+ .fontdue-consent-banner {
880
+ position: fixed;
881
+ bottom: 16px;
882
+ left: 16px;
883
+ z-index: 999999;
884
+ max-width: 420px;
885
+ background-color: var(--secondary_background_color, #fafafa);
886
+ color: var(--primary_text_color, #000);
887
+ font-family: inherit;
888
+ font-size: 14px;
889
+ line-height: 1.5;
890
+ border: 1px solid var(--horizontal_rule_color, #ccc);
891
+ }
892
+ @media (max-width: 480px) {
893
+ .fontdue-consent-banner {
894
+ left: 8px;
895
+ right: 8px;
896
+ max-width: none;
897
+ }
898
+ }
899
+ .fontdue-consent-banner__container {
900
+ padding: 20px 24px;
901
+ display: flex;
902
+ flex-direction: column;
903
+ gap: 16px;
904
+ }
905
+ .fontdue-consent-banner__message {
906
+ margin: 0;
907
+ }
908
+ .fontdue-consent-banner__actions {
909
+ display: flex;
910
+ gap: 8px;
911
+ }
912
+ .fontdue-consent-banner__button {
913
+ appearance: none;
914
+ cursor: pointer;
915
+ font-family: inherit;
916
+ font-size: 14px;
917
+ line-height: 1;
918
+ padding: 10px 20px;
919
+ border-radius: 0;
920
+ white-space: nowrap;
921
+ transition: background-color 150ms ease, color 150ms ease;
922
+ }
923
+ .fontdue-consent-banner__button--primary {
924
+ background-color: var(--button_background_color, #000);
925
+ color: var(--button_text_color, #fff);
926
+ border: 1px solid var(--button_border_color, transparent);
927
+ }
928
+ .fontdue-consent-banner__button--primary:hover {
929
+ background-color: var(--button_hover_background_color, #333);
930
+ color: var(--button_hover_text_color, #fff);
931
+ border-color: var(--button_hover_border_color, transparent);
932
+ }
933
+ .fontdue-consent-banner__button--secondary {
934
+ background-color: transparent;
935
+ color: var(--primary_text_color, #000);
936
+ border: 1px solid var(--horizontal_rule_color, #ccc);
937
+ }
938
+ .fontdue-consent-banner__button--secondary:hover {
939
+ background-color: var(--button_hover_background_color, #333);
940
+ color: var(--button_hover_text_color, #fff);
941
+ border-color: var(--button_hover_border_color, transparent);
942
+ }
943
+
879
944
  :root {
880
945
  --section_spacing: 60px;
881
946
  }
@@ -0,0 +1,20 @@
1
+ export interface UseAutofitProps {
2
+ text: string;
3
+ fontFamily: string;
4
+ fontSize: number;
5
+ min?: number;
6
+ max?: number;
7
+ letterSpacing?: number;
8
+ fontWeight?: React.CSSProperties['fontWeight'];
9
+ fontStyle?: string;
10
+ fontVariationSettings?: string;
11
+ enabled?: boolean;
12
+ padding?: number;
13
+ }
14
+ export interface UseAutofitResult {
15
+ ref: React.RefObject<HTMLDivElement | null>;
16
+ fontSize: number;
17
+ ready: boolean;
18
+ }
19
+ declare const useAutofit: ({ text, fontFamily, fontSize: externalFontSize, min, max, letterSpacing, fontWeight, fontStyle, fontVariationSettings, enabled, padding, }: UseAutofitProps) => UseAutofitResult;
20
+ export default useAutofit;
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ var _react = require("react");
8
+ var _resizeObserver = _interopRequireDefault(require("@react-hook/resize-observer"));
9
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
10
+ const REF_SIZE = 100;
11
+
12
+ // Canvas measurement (fast path)
13
+ let sharedCanvas = null;
14
+ function getCanvasContext() {
15
+ if (!sharedCanvas) sharedCanvas = document.createElement('canvas');
16
+ return sharedCanvas.getContext('2d');
17
+ }
18
+
19
+ // DOM measurement (fallback for variable fonts)
20
+ let measureEl = null;
21
+ function getMeasureElement() {
22
+ if (!measureEl) {
23
+ measureEl = document.createElement('span');
24
+ measureEl.style.position = 'absolute';
25
+ measureEl.style.top = '-9999px';
26
+ measureEl.style.left = '-9999px';
27
+ measureEl.style.whiteSpace = 'pre';
28
+ measureEl.style.visibility = 'hidden';
29
+ document.body.appendChild(measureEl);
30
+ }
31
+ return measureEl;
32
+ }
33
+ function measureWithCanvas(text, fontFamily, fontWeight, fontStyleValue, letterSpacing) {
34
+ const ctx = getCanvasContext();
35
+ ctx.font = `${fontStyleValue} ${fontWeight} ${REF_SIZE}px ${fontFamily}`;
36
+ ctx.letterSpacing = `${letterSpacing}em`;
37
+ const width = ctx.measureText(text).width;
38
+ ctx.letterSpacing = '0px';
39
+ return width;
40
+ }
41
+ function measureWithDOM(text, fontFamily, fontWeight, fontStyleValue, letterSpacing, fontVariationSettings) {
42
+ const el = getMeasureElement();
43
+ el.style.fontFamily = fontFamily;
44
+ el.style.fontWeight = `${fontWeight}`;
45
+ el.style.fontStyle = fontStyleValue;
46
+ el.style.fontSize = `${REF_SIZE}px`;
47
+ el.style.letterSpacing = `${letterSpacing}em`;
48
+ el.style.fontVariationSettings = fontVariationSettings;
49
+ el.textContent = text;
50
+ return el.getBoundingClientRect().width;
51
+ }
52
+ const useAutofit = _ref => {
53
+ let {
54
+ text,
55
+ fontFamily,
56
+ fontSize: externalFontSize,
57
+ min = 8,
58
+ max = 999,
59
+ letterSpacing = 0,
60
+ fontWeight = 400,
61
+ fontStyle = 'normal',
62
+ fontVariationSettings,
63
+ enabled = true,
64
+ padding = 0
65
+ } = _ref;
66
+ const ref = (0, _react.useRef)(null);
67
+ const [fontSize, setFontSize] = (0, _react.useState)(externalFontSize);
68
+ const [containerWidth, setContainerWidth] = (0, _react.useState)(0);
69
+ const [fontVersion, setFontVersion] = (0, _react.useState)(0);
70
+ const [measured, setMeasured] = (0, _react.useState)(false);
71
+ (0, _resizeObserver.default)(ref, entry => {
72
+ setContainerWidth(entry.contentRect.width);
73
+ });
74
+
75
+ // Trigger re-measurement when the font becomes available.
76
+ // If already loaded (e.g. parent waited for font), the promise
77
+ // resolves as a microtask and the re-measurement is near-instant.
78
+ (0, _react.useLayoutEffect)(() => {
79
+ if (!enabled || !fontFamily) return;
80
+ const fontString = `${fontStyle} ${fontWeight} ${REF_SIZE}px ${fontFamily}`;
81
+ document.fonts.load(fontString).then(() => {
82
+ setFontVersion(v => v + 1);
83
+ });
84
+ }, [fontFamily, fontWeight, fontStyle, enabled]);
85
+ (0, _react.useLayoutEffect)(() => {
86
+ if (!enabled) return;
87
+ const availableWidth = containerWidth - padding;
88
+ if (availableWidth <= 0 || !text) return;
89
+
90
+ // Use DOM measurement when variable font settings are active
91
+ // (canvas doesn't support fontVariationSettings in most browsers).
92
+ // Otherwise use canvas for better performance (no layout reflow).
93
+ const refWidth = fontVariationSettings ? measureWithDOM(text, fontFamily, fontWeight, fontStyle, letterSpacing, fontVariationSettings) : measureWithCanvas(text, fontFamily, fontWeight, fontStyle, letterSpacing);
94
+ if (refWidth <= 0) return;
95
+
96
+ // Text width scales linearly with font size.
97
+ // Floor to avoid sub-pixel rounding that causes text to wrap.
98
+ const newSize = Math.min(Math.max(min, Math.floor(availableWidth / (refWidth / REF_SIZE))), max);
99
+ setFontSize(newSize);
100
+ if (!measured) setMeasured(true);
101
+ }, [text, fontFamily, fontWeight, fontStyle, fontVariationSettings, letterSpacing, containerWidth, enabled, min, max, padding, fontVersion]);
102
+ if (!enabled) return {
103
+ ref,
104
+ fontSize: externalFontSize,
105
+ ready: true
106
+ };
107
+ return {
108
+ ref,
109
+ fontSize,
110
+ ready: measured
111
+ };
112
+ };
113
+ var _default = useAutofit;
114
+ exports.default = _default;
@@ -165,7 +165,11 @@ function useRanger(_ref) {
165
165
  document.addEventListener('mouseup', handleRelease);
166
166
  document.addEventListener('touchend', handleRelease);
167
167
  }, [getLatest, handleDrag]);
168
- const getPercentageForValue = _react.default.useCallback(val => interpolator.getPercentageForValue(val, min, max), [interpolator, max, min]);
168
+ const getPercentageForValue = _react.default.useCallback(val => {
169
+ const pct = interpolator.getPercentageForValue(val, min, max);
170
+ // Round to 4 decimal places for consistent SSR/client hydration
171
+ return Math.round(pct * 10000) / 10000;
172
+ }, [interpolator, max, min]);
169
173
 
170
174
  // Build the ticks
171
175
  const ticks = _react.default.useMemo(() => {
package/dist/reducer.d.ts CHANGED
@@ -17,7 +17,6 @@ export interface OrderVariableSelectionState {
17
17
  countryCode?: string | null;
18
18
  }
19
19
  export interface FontdueState {
20
- stylesheets: string[];
21
20
  cartOpen: boolean;
22
21
  precartOpen: boolean;
23
22
  selectedSkuIds: {
@@ -73,9 +72,6 @@ export type FontdueAction = {
73
72
  selected: boolean;
74
73
  } | {
75
74
  type: 'UNSELECT_SKUS';
76
- } | {
77
- type: 'REGISTER_STYLESHEET';
78
- href: string;
79
75
  } | {
80
76
  type: 'POPULATE_PRODUCT_STATE';
81
77
  data: productState_Query$data;
package/dist/reducer.js CHANGED
@@ -82,17 +82,27 @@ const collectionSkuIdWithDiscount = (state, skuId) => {
82
82
  const differences = (0, _utils.collectionSkuIdsDifferences)(state, skuId);
83
83
  const collectionId = Object.keys(differences).filter(collSkuId => differences[collSkuId] <= state.skuPrices[skuId]).reduce((smallest, key) => {
84
84
  if (!smallest || differences[key] < differences[smallest]) return key;
85
- // When differences are equal, prefer a collection that is not the
86
- // SKU being selected this ensures collection bundles are preferred
87
- // over selecting the family itself at the same price
88
- if (differences[key] === differences[smallest] && smallest === skuId && key !== skuId) return key;
85
+ if (differences[key] === differences[smallest]) {
86
+ // When differences are equal, prefer a collection that is not the
87
+ // SKU being selected this ensures collection bundles are preferred
88
+ // over selecting the family itself at the same price
89
+ if (smallest === skuId && key !== skuId) return key;
90
+ // Among non-self candidates, prefer the one covering more unique
91
+ // font styles — this ensures the most comprehensive collection wins
92
+ if (smallest !== skuId && key !== skuId) {
93
+ var _state$collectionStyl, _state$collectionStyl2;
94
+ const smallestStyles = new Set((_state$collectionStyl = state.collectionStyleSkus[smallest]) === null || _state$collectionStyl === void 0 ? void 0 : _state$collectionStyl.fontStyleSkuIds).size;
95
+ const keyStyles = new Set((_state$collectionStyl2 = state.collectionStyleSkus[key]) === null || _state$collectionStyl2 === void 0 ? void 0 : _state$collectionStyl2.fontStyleSkuIds).size;
96
+ if (keyStyles > smallestStyles) return key;
97
+ }
98
+ }
89
99
  return smallest;
90
100
  }, null);
91
101
  if (collectionId) return [collectionId, differences[collectionId]];
92
102
  return [null, null];
93
103
  };
94
104
  exports.collectionSkuIdWithDiscount = collectionSkuIdWithDiscount;
95
- const selectedCollectionSkuIdContainingSkuId = (state, skuId) => Object.keys(state.selectedSkuIds).filter(id => state.selectedSkuIds[id] === true).find(selectedSkuId => (0, _utils.collContainsSkuId)(state.collectionStyleSkus[selectedSkuId], skuId));
105
+ const selectedCollectionSkuIdContainingSkuId = (state, skuId) => Object.keys(state.selectedSkuIds).filter(id => state.selectedSkuIds[id] === true).find(selectedSkuId => (0, _utils.collTransitivelyContainsSkuId)(state.collectionStyleSkus, selectedSkuId, skuId));
96
106
  const setKeysFalse = (acc, key) => {
97
107
  acc[key] = false;
98
108
  return acc;
@@ -104,12 +114,24 @@ const unselectedMembersSkuIds = (state, action) => {
104
114
  selected
105
115
  } = action;
106
116
  if (!selected) return {}; // only do this in case we are selecting something
107
- const collectionMemberSkus = state.collectionStyleSkus[skuId];
108
- if (!collectionMemberSkus) return {}; // we don't have data for this sku
109
- return {
110
- ...collectionMemberSkus.fontStyleSkuIds.reduce(setKeysFalse, {}),
111
- ...collectionMemberSkus.childrenSkuIds.reduce(setKeysFalse, {})
117
+
118
+ // Recursively collect all descendant SKU IDs so that transitive
119
+ // selections (e.g., collection bundle → family → bundle) are all
120
+ // marked as false
121
+ const result = {};
122
+ const collectMembers = collSkuId => {
123
+ const members = state.collectionStyleSkus[collSkuId];
124
+ if (!members) return;
125
+ for (const id of members.fontStyleSkuIds) {
126
+ result[id] = false;
127
+ }
128
+ for (const id of members.childrenSkuIds) {
129
+ result[id] = false;
130
+ collectMembers(id);
131
+ }
112
132
  };
133
+ collectMembers(skuId);
134
+ return result;
113
135
  };
114
136
  const selectSkuId = (state, action) => {
115
137
  if (action.type !== 'SELECT_SKU_ID') return state;
@@ -226,16 +248,6 @@ const reducer = (state, action) => {
226
248
  ...state,
227
249
  cartOpen: false
228
250
  };
229
- case 'REGISTER_STYLESHEET':
230
- const existing = document.querySelector(`link[href="${action.href}"]`);
231
- if (existing) return state;
232
- return {
233
- ...state,
234
- stylesheets: state.stylesheets.filter(href => href !== action.href).concat(action.href).sort()
235
-
236
- // sort is important here since moving stylesheets around causes them to
237
- // get re-evaluated
238
- };
239
251
  case 'STORE_MODAL_NAVIGATE':
240
252
  return storeModalNavigate(state, action);
241
253
  case 'STORE_MODAL_REPLACE':
@@ -264,7 +276,6 @@ function createDefaultStore(config) {
264
276
  collectionStyleSkus: {},
265
277
  skuPrices: {},
266
278
  collectionSkus: {},
267
- stylesheets: [],
268
279
  fetchedCollectionIds: [],
269
280
  storeModalRoute: {
270
281
  name: 'index',
package/dist/utils.d.ts CHANGED
@@ -4,6 +4,9 @@ export declare const pluralize: (count: number, singular: string, plural?: strin
4
4
  export declare function notEmpty<TValue>(value: TValue | null | undefined): value is TValue;
5
5
  export declare function kebabToCamel(str: string): string;
6
6
  export declare const collContainsSkuId: (coll: CollectionStyleSkus | undefined, skuIdOrStyleId: string) => boolean;
7
+ export declare const collTransitivelyContainsSkuId: (collectionStyleSkus: {
8
+ [key: string]: CollectionStyleSkus | undefined;
9
+ }, collSkuId: string, skuIdOrStyleId: string) => boolean;
7
10
  interface CollectionSkuIdsDifferences {
8
11
  [collectionSkuId: string]: number;
9
12
  }
package/dist/utils.js CHANGED
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.compareVariableSettings = exports.collectionSkuIdsDifferences = exports.collContainsSkuId = void 0;
6
+ exports.compareVariableSettings = exports.collectionSkuIdsDifferences = exports.collTransitivelyContainsSkuId = exports.collContainsSkuId = void 0;
7
7
  exports.getFeatureGlyphs = getFeatureGlyphs;
8
8
  exports.isSelected = exports.inspect = void 0;
9
9
  exports.kebabToCamel = kebabToCamel;
@@ -29,12 +29,25 @@ const collContainsSkuId = (coll, skuIdOrStyleId) => {
29
29
  if (!coll) return false;
30
30
  return coll.childrenSkuIds.indexOf(skuIdOrStyleId) >= 0 || coll.fontStyleSkuIds.indexOf(skuIdOrStyleId) >= 0 || coll.fontStyleIds.indexOf(skuIdOrStyleId) >= 0;
31
31
  };
32
+
33
+ // Transitive containment check — walks the collection hierarchy to find
34
+ // if a SKU is contained at any depth (e.g., collection bundle → family → bundle)
32
35
  exports.collContainsSkuId = collContainsSkuId;
36
+ const collTransitivelyContainsSkuId = (collectionStyleSkus, collSkuId, skuIdOrStyleId) => {
37
+ const coll = collectionStyleSkus[collSkuId];
38
+ if (!coll) return false;
39
+ if (collContainsSkuId(coll, skuIdOrStyleId)) return true;
40
+ return coll.childrenSkuIds.some(childSkuId => collTransitivelyContainsSkuId(collectionStyleSkus, childSkuId, skuIdOrStyleId));
41
+ };
42
+ exports.collTransitivelyContainsSkuId = collTransitivelyContainsSkuId;
33
43
  const isSelected = skuIdOrStyleId => state => {
34
44
  if (!skuIdOrStyleId) return false;
35
- // get the collections that contain this skuId as a fontStyle
36
- const collectionSkuIds = Object.keys(state.collectionStyleSkus).filter(collSkuId => collContainsSkuId(state.collectionStyleSkus[collSkuId], skuIdOrStyleId));
37
- return state.selectedSkuIds[skuIdOrStyleId] === true || collectionSkuIds.some(collSkuId => state.selectedSkuIds[collSkuId] === true);
45
+ if (state.selectedSkuIds[skuIdOrStyleId] === true) return true;
46
+ // get the collections that contain this skuId as a fontStyle or child
47
+ const parentCollectionSkuIds = Object.keys(state.collectionStyleSkus).filter(collSkuId => collContainsSkuId(state.collectionStyleSkus[collSkuId], skuIdOrStyleId));
48
+ // Recursively check ancestors — handles transitive selection
49
+ // (e.g., collection bundle → family → bundle)
50
+ return parentCollectionSkuIds.some(parentSkuId => isSelected(parentSkuId)(state));
38
51
  };
39
52
 
40
53
  // find any collection ancestors that contain this skuId,
@@ -42,11 +55,9 @@ const isSelected = skuIdOrStyleId => state => {
42
55
  // and return the delta between the collection price and the sum.
43
56
  exports.isSelected = isSelected;
44
57
  const collectionSkuIdsDifferences = (state, skuId) => Object.keys(state.collectionStyleSkus).filter(collectionSkuId => {
45
- const coll = state.collectionStyleSkus[collectionSkuId];
46
- return skuId === collectionSkuId || collContainsSkuId(coll, skuId);
58
+ return skuId === collectionSkuId || collTransitivelyContainsSkuId(state.collectionStyleSkus, collectionSkuId, skuId);
47
59
  }).reduce((acc, collectionSkuId) => {
48
- const coll = state.collectionStyleSkus[collectionSkuId];
49
- const selectedSkuIdsInColl = Object.keys(state.selectedSkuIds).filter(selectedSkuId => state.selectedSkuIds[selectedSkuId] === true && collContainsSkuId(coll, selectedSkuId));
60
+ const selectedSkuIdsInColl = Object.keys(state.selectedSkuIds).filter(selectedSkuId => state.selectedSkuIds[selectedSkuId] === true && collTransitivelyContainsSkuId(state.collectionStyleSkus, collectionSkuId, selectedSkuId));
50
61
  const sumOfSelectedSkuIdsInColl = selectedSkuIdsInColl.map(selectedSkuId => state.skuPrices[selectedSkuId] ?? 0).reduce((acc, price) => acc + price, 0);
51
62
  acc[collectionSkuId] = (state.skuPrices[collectionSkuId] ?? 0) - sumOfSelectedSkuIdsInColl;
52
63
  return acc;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fontdue-js",
3
- "version": "2.19.1",
3
+ "version": "2.20.0",
4
4
  "scripts": {
5
5
  "build": "npm run relay && run-p build-js build-css build-ts",
6
6
  "build-js": "babel src --out-dir dist --extensions .ts,.tsx,.js,.jsx",
@@ -115,6 +115,9 @@
115
115
  "react-server": "./dist/components/TypeTesters/index.server.js",
116
116
  "default": "./dist/components/TypeTesters/index.js"
117
117
  },
118
- "./useFontStyle": "./dist/components/useFontStyle.js"
118
+ "./useConsent": "./dist/components/useConsent.js",
119
+ "./useFont": "./dist/components/useFont.js",
120
+ "./useFontStyle": "./dist/components/useFont.js",
121
+ "./useAutofit": "./dist/hooks/useAutofit.js"
119
122
  }
120
123
  }