fontdue-js 3.5.1 → 3.7.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 (151) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +2 -0
  3. package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.d.ts +3 -1
  4. package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.js +1 -1
  5. package/dist/__generated__/CustomerSignInRequestLoginCodeMutation.graphql.d.ts +24 -0
  6. package/dist/__generated__/CustomerSignInRequestLoginCodeMutation.graphql.js +71 -0
  7. package/dist/__generated__/CustomerSignInVerifyLoginCodeMutation.graphql.d.ts +30 -0
  8. package/dist/__generated__/CustomerSignInVerifyLoginCodeMutation.graphql.js +98 -0
  9. package/dist/__generated__/FontFamiliesQuery.graphql.d.ts +4 -1
  10. package/dist/__generated__/FontFamiliesQuery.graphql.js +46 -39
  11. package/dist/__generated__/NewsletterSignupUpdateCustomerMutation.graphql.d.ts +3 -1
  12. package/dist/__generated__/NewsletterSignupUpdateCustomerMutation.graphql.js +1 -1
  13. package/dist/__generated__/PriceBar_node.graphql.d.ts +4 -1
  14. package/dist/__generated__/PriceBar_node.graphql.js +23 -16
  15. package/dist/__generated__/StoreModalFamily_collection.graphql.d.ts +3 -2
  16. package/dist/__generated__/StoreModalFamily_collection.graphql.js +3 -3
  17. package/dist/__generated__/StoreModalIndexPaginationQuery.graphql.d.ts +23 -0
  18. package/dist/__generated__/StoreModalIndexPaginationQuery.graphql.js +303 -0
  19. package/dist/__generated__/StoreModalIndexQuery.graphql.d.ts +4 -9
  20. package/dist/__generated__/StoreModalIndexQuery.graphql.js +90 -53
  21. package/dist/__generated__/StoreModalIndex_viewer.graphql.d.ts +24 -0
  22. package/dist/__generated__/StoreModalIndex_viewer.graphql.js +134 -0
  23. package/dist/__generated__/StoreModalProductContent_collection.graphql.d.ts +4 -1
  24. package/dist/__generated__/StoreModalProductContent_collection.graphql.js +12 -5
  25. package/dist/__generated__/StoreModalProductLicenseSelection_collection.graphql.d.ts +2 -1
  26. package/dist/__generated__/StoreModalProductLicenseSelection_collection.graphql.js +27 -24
  27. package/dist/__generated__/StoreModalProductQuery.graphql.d.ts +2 -19
  28. package/dist/__generated__/StoreModalProductQuery.graphql.js +143 -174
  29. package/dist/__generated__/StoreModalProductRefetchQuery.graphql.d.ts +1 -1
  30. package/dist/__generated__/StoreModalProductRefetchQuery.graphql.js +54 -53
  31. package/dist/__generated__/StoreModalProductSlugQuery.graphql.d.ts +38 -0
  32. package/dist/__generated__/StoreModalProductSlugQuery.graphql.js +750 -0
  33. package/dist/__generated__/StoreModalProductViewer.graphql.d.ts +34 -0
  34. package/dist/__generated__/StoreModalProductViewer.graphql.js +149 -0
  35. package/dist/__generated__/StoreModalUnifiedCheckoutUpdateCustomerMutation.graphql.d.ts +3 -1
  36. package/dist/__generated__/StoreModalUnifiedCheckoutUpdateCustomerMutation.graphql.js +1 -1
  37. package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.d.ts +3 -1
  38. package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.js +1 -1
  39. package/dist/__generated__/TypeTestersIDQuery.graphql.d.ts +1 -1
  40. package/dist/__generated__/TypeTestersIDQuery.graphql.js +17 -10
  41. package/dist/__generated__/TypeTestersRefetchQuery.graphql.d.ts +1 -1
  42. package/dist/__generated__/TypeTestersRefetchQuery.graphql.js +17 -10
  43. package/dist/__generated__/TypeTestersSlugQuery.graphql.d.ts +1 -1
  44. package/dist/__generated__/TypeTestersSlugQuery.graphql.js +17 -10
  45. package/dist/__generated__/storefrontEventsTrackStorefrontEventMutation.graphql.d.ts +41 -0
  46. package/dist/__generated__/storefrontEventsTrackStorefrontEventMutation.graphql.js +65 -0
  47. package/dist/__generated__/useCurrentCustomerQuery.graphql.d.ts +23 -0
  48. package/dist/__generated__/{ServerConfigProviderQuery.graphql.js → useCurrentCustomerQuery.graphql.js} +24 -39
  49. package/dist/__tests__/bundleSelectionOrder.test.js +92 -0
  50. package/dist/__tests__/collectionBundleSelection.test.js +441 -1446
  51. package/dist/__tests__/customerSession.test.js +456 -0
  52. package/dist/__tests__/infiniteScroll.test.js +180 -0
  53. package/dist/__tests__/licenseExclusions.test.js +2 -1
  54. package/dist/__tests__/storeModalRoutes.test.js +118 -0
  55. package/dist/__tests__/storefrontEvents.test.js +167 -0
  56. package/dist/__tests__/stripeElements.test.js +173 -0
  57. package/dist/__tests__/typeTesterAutofitFeatures.test.js +138 -0
  58. package/dist/__tests__/typeTesterInitialSize.test.js +82 -0
  59. package/dist/__tests__/typeTesterTruncate.test.js +218 -0
  60. package/dist/auth/session.d.ts +42 -0
  61. package/dist/auth/session.js +139 -0
  62. package/dist/auth/tokenStore.d.ts +61 -0
  63. package/dist/auth/tokenStore.js +162 -0
  64. package/dist/components/Cart/Checkout.js +19 -9
  65. package/dist/components/Cart/storefrontEvents.d.ts +34 -0
  66. package/dist/components/Cart/storefrontEvents.js +138 -0
  67. package/dist/components/ConfigContext.d.ts +2 -2
  68. package/dist/components/CustomerSignIn/index.d.ts +33 -0
  69. package/dist/components/CustomerSignIn/index.js +170 -0
  70. package/dist/components/FontFamilies/index.js +4 -3
  71. package/dist/components/NewsletterSignup/index.js +5 -1
  72. package/dist/components/Precart/index.js +6 -0
  73. package/dist/components/PriceBar/index.js +5 -3
  74. package/dist/components/Root/index.js +4 -34
  75. package/dist/components/Root/productState.d.ts +2 -8
  76. package/dist/components/Root/productState.js +46 -57
  77. package/dist/components/SKUPrice/index.js +7 -8
  78. package/dist/components/StoreModal/StoreModalFamily.js +4 -2
  79. package/dist/components/StoreModal/StoreModalIndex.js +48 -7
  80. package/dist/components/StoreModal/StoreModalProduct.d.ts +5 -1
  81. package/dist/components/StoreModal/StoreModalProduct.js +69 -16
  82. package/dist/components/StoreModal/index.js +14 -14
  83. package/dist/components/StoreModal/routes.js +9 -1
  84. package/dist/components/StoreModalProductLicenseSelection/StoreModalProductLicense.d.ts +2 -1
  85. package/dist/components/StoreModalProductLicenseSelection/StoreModalProductLicense.js +8 -3
  86. package/dist/components/StoreModalProductLicenseSelection/index.js +3 -2
  87. package/dist/components/TestFontsForm/index.js +5 -1
  88. package/dist/components/TypeTester/TypeTesterContent.js +9 -2
  89. package/dist/components/TypeTester/TypeTesterContext.js +17 -4
  90. package/dist/components/TypeTester/TypeTesterFitter.js +7 -2
  91. package/dist/components/TypeTester/TypeTesterStandalone.d.ts +2 -0
  92. package/dist/components/TypeTester/TypeTesterStandalone.js +2 -0
  93. package/dist/components/TypeTester/TypeTesterStandaloneElement.d.ts +1 -0
  94. package/dist/components/TypeTester/TypeTesterStandaloneElement.js +4 -1
  95. package/dist/components/TypeTester/TypeTesterVariableAxes.js +16 -10
  96. package/dist/components/TypeTester/index.d.ts +3 -1
  97. package/dist/components/TypeTester/index.js +2 -1
  98. package/dist/components/TypeTester/truncate.d.ts +12 -0
  99. package/dist/components/TypeTester/truncate.js +95 -0
  100. package/dist/components/TypeTester/useTypeTesterStyler.d.ts +3 -1
  101. package/dist/components/TypeTester/useTypeTesterStyler.js +36 -7
  102. package/dist/components/TypeTesters/TypeTestersElement.d.ts +2 -1
  103. package/dist/components/TypeTesters/TypeTestersElement.js +4 -1
  104. package/dist/components/TypeTesters/index.d.ts +2 -0
  105. package/dist/components/elements/StoreModalContainer/StoreModalContainerContext.d.ts +1 -0
  106. package/dist/components/elements/StoreModalContainer/index.js +9 -4
  107. package/dist/components/elements/StoreModalIndexLoadMore/index.d.ts +8 -0
  108. package/dist/components/elements/StoreModalIndexLoadMore/index.js +21 -0
  109. package/dist/components/elements/StoreModalUnifiedCheckout.d.ts +1 -1
  110. package/dist/components/elements/StoreModalUnifiedCheckout.js +20 -17
  111. package/dist/components/useInfiniteScroll.d.ts +28 -0
  112. package/dist/components/useInfiniteScroll.js +50 -0
  113. package/dist/fontdue.css +63 -0
  114. package/dist/hooks/useAutofit.d.ts +2 -1
  115. package/dist/hooks/useAutofit.js +10 -4
  116. package/dist/hooks/useCurrentCustomer.d.ts +34 -0
  117. package/dist/hooks/useCurrentCustomer.js +93 -0
  118. package/dist/reducer.d.ts +5 -5
  119. package/dist/reducer.js +9 -87
  120. package/dist/relay/environment.js +91 -50
  121. package/dist/selection.d.ts +40 -0
  122. package/dist/selection.js +494 -0
  123. package/dist/storeModalRoutes.d.ts +38 -0
  124. package/dist/storeModalRoutes.js +110 -0
  125. package/dist/stripeElements.d.ts +18 -0
  126. package/dist/stripeElements.js +49 -0
  127. package/dist/utils.d.ts +1 -12
  128. package/dist/utils.js +1 -36
  129. package/dist/version.js +1 -1
  130. package/package.json +1 -1
  131. package/dist/__generated__/NodePasswordFormIDQuery.graphql.d.ts +0 -21
  132. package/dist/__generated__/NodePasswordFormIDQuery.graphql.js +0 -86
  133. package/dist/__generated__/NodePasswordFormSlugQuery.graphql.d.ts +0 -25
  134. package/dist/__generated__/NodePasswordFormSlugQuery.graphql.js +0 -108
  135. package/dist/__generated__/ServerConfigProviderQuery.graphql.d.ts +0 -24
  136. package/dist/__generated__/TestModeBannerQuery.graphql.d.ts +0 -20
  137. package/dist/__generated__/TestModeBannerQuery.graphql.js +0 -87
  138. package/dist/__generated__/ThemeConfigQuery.graphql.d.ts +0 -20
  139. package/dist/__generated__/ThemeConfigQuery.graphql.js +0 -87
  140. package/dist/components/CartButton/index.server.d.ts +0 -3
  141. package/dist/components/CartButton/index.server.js +0 -18
  142. package/dist/components/CorsErrorModal.d.ts +0 -1
  143. package/dist/components/CorsErrorModal.js +0 -92
  144. package/dist/components/FontdueProvider/FontdueProviderClientComponent.d.ts +0 -14
  145. package/dist/components/FontdueProvider/FontdueProviderClientComponent.js +0 -61
  146. package/dist/components/ThemeConfig/index.server.d.ts +0 -2
  147. package/dist/components/ThemeConfig/index.server.js +0 -17
  148. package/dist/components/TypeTester/features.d.ts +0 -5
  149. package/dist/components/TypeTester/features.js +0 -143
  150. package/dist/data/unicodeData.d.ts +0 -4
  151. package/dist/data/unicodeData.js +0 -1
@@ -3,6 +3,7 @@ import { Alignment, Direction, FeaturesProp } from './types.js';
3
3
  import { SerializablePreloadedQuery, type LoadQueryOptions } from '../../relay/loadSerializableQuery.js';
4
4
  import { TypeTesterStandaloneQuery } from '../../__generated__/TypeTesterStandaloneQuery.graphql.js';
5
5
  import { Config } from '../ConfigContext.js';
6
+ import type { TruncateOption } from './truncate.js';
6
7
  interface TypeTesterStandaloneComponent_props {
7
8
  lineHeight?: number | null;
8
9
  letterSpacing?: number | null;
@@ -22,6 +23,7 @@ interface TypeTesterStandaloneComponent_props {
22
23
  direction?: Direction;
23
24
  alignment?: Alignment;
24
25
  autofit?: boolean;
26
+ truncate?: TruncateOption;
25
27
  onFocus?: () => void;
26
28
  onBlur?: () => void;
27
29
  }
@@ -30,6 +30,7 @@ function TypeTesterStandaloneComponent(_ref) {
30
30
  alignment,
31
31
  fontSize,
32
32
  autofit,
33
+ truncate,
33
34
  onFocus,
34
35
  onBlur,
35
36
  data
@@ -95,6 +96,7 @@ function TypeTesterStandaloneComponent(_ref) {
95
96
  features: normalizeFeaturesProp(features),
96
97
  productId: ((_data$viewer2 = data.viewer) === null || _data$viewer2 === void 0 ? void 0 : (_data$viewer2$fontSty = _data$viewer2.fontStyle) === null || _data$viewer2$fontSty === void 0 ? void 0 : (_data$viewer2$fontSty2 = _data$viewer2$fontSty.family) === null || _data$viewer2$fontSty2 === void 0 ? void 0 : (_data$viewer2$fontSty3 = _data$viewer2$fontSty2.parent) === null || _data$viewer2$fontSty3 === void 0 ? void 0 : _data$viewer2$fontSty3.id) ?? ((_data$viewer3 = data.viewer) === null || _data$viewer3 === void 0 ? void 0 : (_data$viewer3$fontSty = _data$viewer3.fontStyle) === null || _data$viewer3$fontSty === void 0 ? void 0 : (_data$viewer3$fontSty2 = _data$viewer3$fontSty.family) === null || _data$viewer3$fontSty2 === void 0 ? void 0 : _data$viewer3$fontSty2.id),
97
98
  groupEdit: false,
99
+ truncate: truncate,
98
100
  onFocus: onFocus,
99
101
  onBlur: onBlur
100
102
  }));
@@ -13,6 +13,7 @@ interface TypeTesterElement_props {
13
13
  direction?: string;
14
14
  alignment?: string;
15
15
  autofit?: string;
16
+ truncate?: string;
16
17
  }
17
18
  export declare const TypeTesterStandaloneElement: React.FC<TypeTesterElement_props>;
18
19
  export default TypeTesterStandaloneElement;
@@ -2,6 +2,7 @@ function _extends() { return _extends = Object.assign ? Object.assign.bind() : f
2
2
  import React from 'react';
3
3
  import TypeTesterStandalone from './TypeTesterStandalone.js';
4
4
  import { parseVariableSettings } from './parseVariableSettings.js';
5
+ import { parseTruncateAttribute } from './truncate.js';
5
6
  const isAlignment = str => {
6
7
  return str === 'left' || str === 'center' || str === 'right';
7
8
  };
@@ -26,6 +27,7 @@ export const TypeTesterStandaloneElement = _ref => {
26
27
  alignment,
27
28
  direction,
28
29
  autofit,
30
+ truncate,
29
31
  ...rest
30
32
  } = _ref;
31
33
  return /*#__PURE__*/React.createElement(TypeTesterStandalone, _extends({
@@ -41,7 +43,8 @@ export const TypeTesterStandaloneElement = _ref => {
41
43
  })) : undefined,
42
44
  alignment: isAlignment(alignment) ? alignment : undefined,
43
45
  direction: isDirection(direction) ? direction : undefined,
44
- variableSettings: parseVariableSettings(variableSettings)
46
+ variableSettings: parseVariableSettings(variableSettings),
47
+ truncate: parseTruncateAttribute(truncate, 'fontdue-type-tester')
45
48
  }, rest));
46
49
  };
47
50
  export default TypeTesterStandaloneElement;
@@ -35,15 +35,7 @@ const TypeTesterVariableAxes = _ref => {
35
35
  }, [requestedAxesKey, availableAxesKey]);
36
36
  if (!variableSettings) return null;
37
37
  if (!axes) return null;
38
- const handleChange = (axis, value) => {
39
- setVariableSettings({
40
- ...variableSettings,
41
- [axis]: value
42
- });
43
- };
44
- return /*#__PURE__*/React.createElement("div", {
45
- className: "type-tester__variable-axes"
46
- }, axes.filter(showAxis => showAxis in variableSettings) // only show the slider if the value is set
38
+ const visibleAxes = axes.filter(showAxis => showAxis in variableSettings) // only show the slider if the value is set
47
39
  .map(showAxis => {
48
40
  var _fontStyle$variableAx;
49
41
  return (_fontStyle$variableAx = fontStyle.variableAxes) === null || _fontStyle$variableAx === void 0 ? void 0 : _fontStyle$variableAx.find(_ref2 => {
@@ -52,7 +44,21 @@ const TypeTesterVariableAxes = _ref => {
52
44
  } = _ref2;
53
45
  return axis === showAxis;
54
46
  });
55
- }).filter(notEmpty).map(_ref3 => {
47
+ }).filter(notEmpty);
48
+
49
+ // Nothing to show (static style, or every requested axis is missing): render
50
+ // no container at all, so its margin/padding/border in the toolbar, features
51
+ // panel and dropdown placements never leave an empty gap behind.
52
+ if (visibleAxes.length === 0) return null;
53
+ const handleChange = (axis, value) => {
54
+ setVariableSettings({
55
+ ...variableSettings,
56
+ [axis]: value
57
+ });
58
+ };
59
+ return /*#__PURE__*/React.createElement("div", {
60
+ className: "type-tester__variable-axes"
61
+ }, visibleAxes.map(_ref3 => {
56
62
  let {
57
63
  axis,
58
64
  name,
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import { TypeTester_fontStyle$key } from '../../__generated__/TypeTester_fontStyle.graphql.js';
3
3
  import { Alignment, TypeTesterAxesPosition } from './types.js';
4
+ import type { TruncateOption } from './truncate.js';
4
5
  export interface TypeTesterConfig {
5
6
  min?: number;
6
7
  max?: number;
@@ -15,7 +16,7 @@ export interface TypeTesterConfig {
15
16
  toolsPosition?: 'inline' | 'floating';
16
17
  selectButtonStyle?: 'inline' | 'outlined';
17
18
  selectButtonLabel?: string;
18
- truncate?: boolean | number;
19
+ truncate?: TruncateOption;
19
20
  bulletStyle?: 'square' | 'round';
20
21
  priceText?: boolean;
21
22
  autofitOnChange?: boolean;
@@ -61,6 +62,7 @@ export interface TypeTesterBaseProps {
61
62
  features?: ReadonlyArray<string> | null;
62
63
  axes?: ReadonlyArray<string> | null;
63
64
  groupEdit?: boolean;
65
+ truncate?: TruncateOption;
64
66
  onFocus?: () => void;
65
67
  onBlur?: () => void;
66
68
  }
@@ -26,6 +26,7 @@ const TypeTester = _ref => {
26
26
  features,
27
27
  axes,
28
28
  groupEdit,
29
+ truncate,
29
30
  onStyleSelect,
30
31
  isLoading
31
32
  } = _ref;
@@ -150,7 +151,7 @@ const TypeTester = _ref => {
150
151
  ref: contentRef,
151
152
  fontStyles: style,
152
153
  verticalMetrics: verticalMetrics,
153
- truncate: config.truncate,
154
+ truncate: truncate ?? config.truncate,
154
155
  direction: props.direction,
155
156
  min: config.size.min,
156
157
  max: config.size.max,
@@ -0,0 +1,12 @@
1
+ export interface TruncateConfig {
2
+ lines?: number;
3
+ expandOnFocus?: boolean;
4
+ }
5
+ export type TruncateOption = boolean | number | TruncateConfig;
6
+ export interface ResolvedTruncate {
7
+ lines: number;
8
+ expandOnFocus: boolean;
9
+ }
10
+ export declare const normalizeTruncate: (value?: TruncateOption) => ResolvedTruncate;
11
+ export declare const shouldTruncate: ({ lines, expandOnFocus }: ResolvedTruncate, focused: boolean) => boolean;
12
+ export declare const parseTruncateAttribute: (input: string | undefined, elementName: string) => TruncateOption | undefined;
@@ -0,0 +1,95 @@
1
+ // Shape and normalization for the type tester's `truncate` option, shared by
2
+ // the styler and by the custom element attribute parsers.
3
+
4
+ const toLines = value => Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
5
+ export const normalizeTruncate = value => {
6
+ if (value === true) return {
7
+ lines: 1,
8
+ expandOnFocus: true
9
+ };
10
+ if (typeof value === 'number') return {
11
+ lines: toLines(value),
12
+ expandOnFocus: false
13
+ };
14
+ if (value && typeof value === 'object') return {
15
+ lines: value.lines === undefined ? 0 : toLines(value.lines),
16
+ expandOnFocus: value.expandOnFocus ?? false
17
+ };
18
+ return {
19
+ lines: 0,
20
+ expandOnFocus: false
21
+ };
22
+ };
23
+
24
+ // Whether the cap applies right now. Split out from the styler so the one
25
+ // behaviour FD-191 changes is unit-testable without a DOM.
26
+ export const shouldTruncate = (_ref, focused) => {
27
+ let {
28
+ lines,
29
+ expandOnFocus
30
+ } = _ref;
31
+ return lines > 0 && !(focused && expandOnFocus);
32
+ };
33
+
34
+ // A line count, or `true`/`false`.
35
+ const parseLineCount = token => {
36
+ if (token === 'true') return 1;
37
+ if (token === 'false') return 0;
38
+ const lines = Number(token);
39
+ if (token !== '' && Number.isInteger(lines) && lines >= 0) return lines;
40
+ return undefined;
41
+ };
42
+
43
+ // The optional second word: what kind of cap this is. Adjectives rather than
44
+ // imperatives, both because every other enumerated value in the config is one
45
+ // (`inline`/`floating`, `square`/`round`, `panel`/`dropdown`) and because they
46
+ // describe the cap accurately — nothing expands until a visitor clicks, so the
47
+ // cap is expandable, not expanding.
48
+ //
49
+ // Written out rather than looked up in an object so that `constructor` and
50
+ // friends are words like any other instead of inherited Object members.
51
+ const parseFocusWord = token => {
52
+ if (token === 'expandable') return true;
53
+ if (token === 'fixed') return false;
54
+ return undefined;
55
+ };
56
+
57
+ // `truncate="15"` caps at 15 lines, `truncate="true"` is the legacy one-line
58
+ // preview, and `truncate="false"` or `truncate="0"` turns it off for this
59
+ // tester.
60
+ //
61
+ // A second word says what kind of cap it is, which is the half of the object
62
+ // form (`expandOnFocus`) an attribute otherwise cannot carry:
63
+ // `truncate="15 expandable"` caps at 15 lines and still opens to the full
64
+ // paragraph when clicked, and `truncate="15 fixed"` spells out the default.
65
+ // Without it the scalar forms are untouched, so `truncate="true"` is still the
66
+ // boolean shorthand and `truncate="15"` still holds through focus.
67
+ //
68
+ // An absent attribute returns undefined so the tester inherits the site
69
+ // config. Anything else, including a valueless `truncate`, an unknown second
70
+ // word or a third one, warns and inherits: following parseVariableSettings, a
71
+ // typo must not silently read as "off". The warning lists the accepted forms,
72
+ // because the failure that grammar shipped in 3.4.1 was silent — a rejected
73
+ // value simply did nothing, with nothing in the markup explaining why.
74
+ export const parseTruncateAttribute = (input, elementName) => {
75
+ if (input === undefined) return undefined;
76
+ const [countToken = '', focusToken, ...rest] = input.trim().split(/\s+/);
77
+ const lines = parseLineCount(countToken);
78
+ const expandOnFocus = focusToken === undefined ? undefined : parseFocusWord(focusToken);
79
+ if (lines === undefined || rest.length > 0 || focusToken !== undefined && expandOnFocus === undefined) {
80
+ console.warn(`${elementName} failed to parse truncate value: "${input}", ignoring. ` + 'Expected a line count, "true", or "false", optionally followed by ' + '"expandable" or "fixed" — e.g. "15", "true", "15 expandable".');
81
+ return undefined;
82
+ }
83
+
84
+ // No second word: return the scalar the caller wrote, so the existing forms
85
+ // keep their exact shape rather than becoming objects that normalize the same.
86
+ if (expandOnFocus === undefined) {
87
+ if (countToken === 'true') return true;
88
+ if (countToken === 'false') return false;
89
+ return lines;
90
+ }
91
+ return {
92
+ lines,
93
+ expandOnFocus
94
+ };
95
+ };
@@ -2,6 +2,7 @@ import { EditorState } from 'draft-js';
2
2
  import { Alignment } from './types.js';
3
3
  import { VariableSettings } from '../../utils.js';
4
4
  import { VerticalMetrics } from '../useFontStyle.js';
5
+ import { TruncateOption } from './truncate.js';
5
6
  export type ColumnsConfig = false | {
6
7
  count: 'auto' | number;
7
8
  width: string;
@@ -20,7 +21,7 @@ export interface UseTypeTesterStylerProps {
20
21
  letterSpacing: number;
21
22
  features: string[];
22
23
  focused: boolean;
23
- truncate: boolean | number;
24
+ truncate: TruncateOption;
24
25
  fontStyles: React.CSSProperties;
25
26
  verticalMetrics?: VerticalMetrics | null;
26
27
  content: EditorState;
@@ -32,5 +33,6 @@ export interface UseTypeTesterStylerProps {
32
33
  declare const useTypeTesterStyler: ({ size, autofit, autofitOnChange, features, setSize, min, max, truncate, focused, lineHeight, letterSpacing, fontStyles, verticalMetrics, content, contentEdited, alignment, variableSettings, columns, }: UseTypeTesterStylerProps) => {
33
34
  ref: import("react").RefObject<HTMLDivElement | null>;
34
35
  style: import("react").CSSProperties;
36
+ truncated: boolean;
35
37
  };
36
38
  export default useTypeTesterStyler;
@@ -1,6 +1,7 @@
1
1
  import "../../global-shim.js";
2
2
  import { useLayoutEffect } from 'react';
3
3
  import useAutofit from '../../hooks/useAutofit.js';
4
+ import { normalizeTruncate, shouldTruncate as resolveShouldTruncate } from './truncate.js';
4
5
  const useTypeTesterStyler = _ref => {
5
6
  let {
6
7
  size,
@@ -39,6 +40,7 @@ const useTypeTesterStyler = _ref => {
39
40
  fontWeight: fontStyles.fontWeight,
40
41
  fontStyle: fontStyles.fontStyle,
41
42
  fontVariationSettings,
43
+ fontFeatureSettings,
42
44
  enabled: autofit && (!contentEdited || autofitOnChange),
43
45
  padding: sideBuffer
44
46
  });
@@ -53,11 +55,21 @@ const useTypeTesterStyler = _ref => {
53
55
  }
54
56
  }, [autofitSize]);
55
57
 
56
- // `truncate` is `true` (1 line), a positive number (that many lines), or
57
- // falsy (off). Editing always shows the full paragraph, so only truncate
58
- // when the tester is not focused.
59
- const truncateLines = truncate === true ? 1 : typeof truncate === 'number' ? Math.max(0, Math.floor(truncate)) : 0;
60
- const shouldTruncate = truncateLines > 0 && !focused;
58
+ // `truncate` is `true` (the original one-line preview), a line count, an
59
+ // explicit `{ lines, expandOnFocus }`, or falsy (off). `expandOnFocus`
60
+ // decides whether clicking in lifts the cap: true for the one-line preview,
61
+ // which exists to bloom open, and false for a line count, where the
62
+ // paragraph has to stay N lines while it is edited (FD-191). For a line
63
+ // count nothing below depends on `focused` any more, so focusing a truncated
64
+ // tester is a zero-reflow event.
65
+ const {
66
+ lines: truncateLines,
67
+ expandOnFocus
68
+ } = normalizeTruncate(truncate);
69
+ const shouldTruncate = resolveShouldTruncate({
70
+ lines: truncateLines,
71
+ expandOnFocus
72
+ }, focused);
61
73
 
62
74
  // How far a line's ink spills past its line-box, top and bottom, as a fraction
63
75
  // of the font-size (em). `fontLoader` feeds these exact metrics into the
@@ -131,7 +143,23 @@ const useTypeTesterStyler = _ref => {
131
143
  paddingRight: sideBearing,
132
144
  marginTop: `${-lineOverflowEm}em`,
133
145
  marginBottom: `${-lineOverflowEm}em`,
134
- overflow: 'hidden',
146
+ // `clip`, not `hidden`, once the cap outlives focus. A hidden box is
147
+ // still a scroll container, so the browser scrolls it to chase a
148
+ // caret past line N, and the offset is unrecoverable because there is
149
+ // no scrollbar and wheel events are ignored (measured: WebKit slides
150
+ // a whole column away on the first keystroke past the cap, and it
151
+ // survives blur). A clipped box is not a scroll container at all, so
152
+ // the visible lines can never slide.
153
+ //
154
+ // `expandOnFocus` stays on `hidden`, because there the cap is lifted
155
+ // before a caret can enter the box. That distinction is load-bearing:
156
+ // an unsupported CSSOM value is a no-op that leaves the previous
157
+ // inline value in place, so on an engine without `clip` a branch that
158
+ // toggles on focus would keep the `overflow: visible` written by the
159
+ // untruncated branch below and stop clipping for good. The branch
160
+ // that uses `clip` never toggles, so on those engines it falls
161
+ // through to the `[data-truncated]` rule in _type-tester.scss.
162
+ overflow: expandOnFocus ? 'hidden' : 'clip',
135
163
  // Fill each column to its full N lines before spilling to the next, and
136
164
  // allow a final column with a single line — otherwise the default
137
165
  // `widows: 2` / `column-fill: balance` rebalance a 4-line paragraph into
@@ -156,7 +184,8 @@ const useTypeTesterStyler = _ref => {
156
184
  };
157
185
  return {
158
186
  ref,
159
- style
187
+ style,
188
+ truncated: shouldTruncate
160
189
  };
161
190
  };
162
191
  export default useTypeTesterStyler;
@@ -7,6 +7,7 @@ interface TypeTestersElementProps {
7
7
  features?: string;
8
8
  tags?: string;
9
9
  excludeTags?: string;
10
+ truncate?: string;
10
11
  }
11
- declare function TypeTestersElement({ autofit, tags, excludeTags, defaultMode, features, ...rest }: TypeTestersElementProps): React.JSX.Element;
12
+ declare function TypeTestersElement({ autofit, tags, excludeTags, defaultMode, features, truncate, ...rest }: TypeTestersElementProps): React.JSX.Element;
12
13
  export default TypeTestersElement;
@@ -1,6 +1,7 @@
1
1
  function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
2
2
  import React from 'react';
3
3
  import TypeTesters from './index.js';
4
+ import { parseTruncateAttribute } from '../TypeTester/truncate.js';
4
5
  const getDefaultMode = (stringValue, autofit) => {
5
6
  if (autofit) return 'local';
6
7
  if (stringValue === 'group') return 'group';
@@ -13,6 +14,7 @@ function TypeTestersElement(_ref) {
13
14
  excludeTags,
14
15
  defaultMode,
15
16
  features,
17
+ truncate,
16
18
  ...rest
17
19
  } = _ref;
18
20
  return /*#__PURE__*/React.createElement(TypeTesters, _extends({
@@ -20,7 +22,8 @@ function TypeTestersElement(_ref) {
20
22
  excludeTags: excludeTags === null || excludeTags === void 0 ? void 0 : excludeTags.split(','),
21
23
  autofit: autofit ? autofit === 'true' : undefined,
22
24
  defaultMode: getDefaultMode(defaultMode, autofit === 'true'),
23
- features: features === null || features === void 0 ? void 0 : features.split(',')
25
+ features: features === null || features === void 0 ? void 0 : features.split(','),
26
+ truncate: parseTruncateAttribute(truncate, 'fontdue-type-testers')
24
27
  }, rest));
25
28
  }
26
29
  export default TypeTestersElement;
@@ -4,11 +4,13 @@ import { TypeTestersSlugQuery } from '../../__generated__/TypeTestersSlugQuery.g
4
4
  import { Config } from '../ConfigContext.js';
5
5
  import { FeaturesProp } from '../TypeTester/types.js';
6
6
  import { SerializablePreloadedQuery, type LoadQueryOptions } from '../../relay/loadSerializableQuery.js';
7
+ import type { TruncateOption } from '../TypeTester/truncate.js';
7
8
  export interface TypeTesters_props {
8
9
  collectionId?: string;
9
10
  collectionSlug?: string;
10
11
  defaultMode?: 'group' | 'local';
11
12
  autofit?: boolean;
13
+ truncate?: TruncateOption;
12
14
  features?: FeaturesProp;
13
15
  tags?: string[] | null;
14
16
  excludeTags?: string[] | null;
@@ -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
@@ -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;
@@ -27,7 +27,7 @@
27
27
  * - The Pay button is always enabled (as long as EULA is checked) even if billing
28
28
  * info isn't fully saved yet
29
29
  * - This is intentional: we need to allow the user to click Pay so we can call
30
- * elements.submit(), which triggers Stripe's validation and shows any validation
30
+ * validateElements(), which triggers Stripe's validation and shows any validation
31
31
  * errors inline in the Stripe UI elements
32
32
  * - After Stripe validation passes, we check if billing identity is saved
33
33
  * - If not saved, we show an error message above the Pay button asking them to
@@ -32,7 +32,7 @@ import _StoreModalUnifiedCheckout_viewer from "../../__generated__/StoreModalUni
32
32
  * - The Pay button is always enabled (as long as EULA is checked) even if billing
33
33
  * info isn't fully saved yet
34
34
  * - This is intentional: we need to allow the user to click Pay so we can call
35
- * elements.submit(), which triggers Stripe's validation and shows any validation
35
+ * validateElements(), which triggers Stripe's validation and shows any validation
36
36
  * errors inline in the Stripe UI elements
37
37
  * - After Stripe validation passes, we check if billing identity is saved
38
38
  * - If not saved, we show an error message above the Pay button asking them to
@@ -79,6 +79,8 @@ 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';
83
+ import { validateAddressElements, validateElements } from '../../stripeElements.js';
82
84
  import { useRecaptchaTimeout, isRecaptchaScriptLoaded, RECAPTCHA_UNAVAILABLE_MESSAGE } from '../../hooks/useRecaptchaTimeout.js';
83
85
 
84
86
  /* const LICENSEE_REQUIRED_FIELDS = ['organization', 'country'] as (keyof Identity)[]; */
@@ -286,6 +288,16 @@ export default function StoreModalUnifiedCheckout(_ref2) {
286
288
  useEffect(() => {
287
289
  sendOrderTracking(environment);
288
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]);
289
301
  const [error, setError] = useState(null);
290
302
  const [errorsObject, setErrorsObject] = useState(null);
291
303
  const [customerErrorsObject, setCustomerErrorsObject] = useState(null);
@@ -559,14 +571,10 @@ export default function StoreModalUnifiedCheckout(_ref2) {
559
571
  // Otherwise, only show when user has selected "Your client" option
560
572
  const showLicenseeSection = order.customer && (hasCountryMismatch || !(order.licenseeIsBillingIdentity ?? false));
561
573
  const handleCheckout = useCallback(async () => {
562
- // Validate Stripe elements first
563
- if (elements) {
564
- const result = await elements.submit();
565
- if (result.error) {
566
- // Stripe will show validation errors in the UI
567
- return;
568
- }
569
- }
574
+ // Validate the licensee address first; Stripe shows any errors in the UI.
575
+ // Not `elements.submit()`: a Stripe Tax order has no PaymentIntent, so
576
+ // this Elements group has no session and Stripe.js would throw.
577
+ if (!(await validateAddressElements(elements))) return;
570
578
 
571
579
  // If licensee section is shown, save licensee data before navigating to checkout
572
580
  if (showLicenseeSection) {
@@ -599,14 +607,8 @@ export default function StoreModalUnifiedCheckout(_ref2) {
599
607
  return;
600
608
  }
601
609
 
602
- // Validate Stripe elements first
603
- if (elements) {
604
- const result = await elements.submit();
605
- if (result.error) {
606
- // Stripe will show validation errors in the UI
607
- return;
608
- }
609
- }
610
+ // Validate Stripe elements first; Stripe shows any errors in the UI
611
+ if (!(await validateElements(elements))) return;
610
612
 
611
613
  // If billing identity is not saved yet, don't proceed with payment
612
614
  // The validation above will show errors for incomplete fields
@@ -798,6 +800,7 @@ export default function StoreModalUnifiedCheckout(_ref2) {
798
800
  }, /*#__PURE__*/React.createElement(Sentry.ErrorBoundary, {
799
801
  fallback: /*#__PURE__*/React.createElement("div", null, "Something went wrong")
800
802
  }, /*#__PURE__*/React.createElement(PaymentElement, {
803
+ onChange: handlePaymentChange,
801
804
  options: {
802
805
  layout: {
803
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;