fontdue-js 2.19.0 → 2.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/reducer.js CHANGED
@@ -82,13 +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
+ 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
+ }
85
99
  return smallest;
86
100
  }, null);
87
101
  if (collectionId) return [collectionId, differences[collectionId]];
88
102
  return [null, null];
89
103
  };
90
104
  exports.collectionSkuIdWithDiscount = collectionSkuIdWithDiscount;
91
- 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));
92
106
  const setKeysFalse = (acc, key) => {
93
107
  acc[key] = false;
94
108
  return acc;
@@ -100,12 +114,24 @@ const unselectedMembersSkuIds = (state, action) => {
100
114
  selected
101
115
  } = action;
102
116
  if (!selected) return {}; // only do this in case we are selecting something
103
- const collectionMemberSkus = state.collectionStyleSkus[skuId];
104
- if (!collectionMemberSkus) return {}; // we don't have data for this sku
105
- return {
106
- ...collectionMemberSkus.fontStyleSkuIds.reduce(setKeysFalse, {}),
107
- ...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
+ }
108
132
  };
133
+ collectMembers(skuId);
134
+ return result;
109
135
  };
110
136
  const selectSkuId = (state, action) => {
111
137
  if (action.type !== 'SELECT_SKU_ID') return state;
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,13 +55,11 @@ 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));
50
- const sumOfSelectedSkuIdsInColl = selectedSkuIdsInColl.map(selectedSkuId => state.skuPrices[selectedSkuId]).reduce((acc, price) => acc + price, 0);
51
- acc[collectionSkuId] = state.skuPrices[collectionSkuId] - sumOfSelectedSkuIdsInColl;
60
+ const selectedSkuIdsInColl = Object.keys(state.selectedSkuIds).filter(selectedSkuId => state.selectedSkuIds[selectedSkuId] === true && collTransitivelyContainsSkuId(state.collectionStyleSkus, collectionSkuId, selectedSkuId));
61
+ const sumOfSelectedSkuIdsInColl = selectedSkuIdsInColl.map(selectedSkuId => state.skuPrices[selectedSkuId] ?? 0).reduce((acc, price) => acc + price, 0);
62
+ acc[collectionSkuId] = (state.skuPrices[collectionSkuId] ?? 0) - sumOfSelectedSkuIdsInColl;
52
63
  return acc;
53
64
  }, {});
54
65
  exports.collectionSkuIdsDifferences = collectionSkuIdsDifferences;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fontdue-js",
3
- "version": "2.19.0",
3
+ "version": "2.19.2",
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",
@@ -8,6 +8,8 @@
8
8
  "build-css": "sass --load-path=node_modules --no-source-map src/index.scss dist/fontdue.css",
9
9
  "relay": "relay-compiler",
10
10
  "update-unicode-data": "node src/scripts/updateUnicodeData.js",
11
+ "test": "vitest run",
12
+ "test:watch": "vitest",
11
13
  "prepublishOnly": "npm run build"
12
14
  },
13
15
  "author": "Fontdue",
@@ -60,7 +62,8 @@
60
62
  "react-dom": "^19.0.0",
61
63
  "relay-compiler": "^18.0.0",
62
64
  "sass": "^1.62",
63
- "typescript": "^5.9"
65
+ "typescript": "^5.9",
66
+ "vitest": "^4.1.1"
64
67
  },
65
68
  "relay": {
66
69
  "src": "./src",
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'vitest/config';
2
+ import path from 'path';
3
+
4
+ export default defineConfig({
5
+ test: {
6
+ alias: {
7
+ '__generated__': path.resolve(__dirname, 'src/__generated__'),
8
+ },
9
+ },
10
+ });