fontdue-js 3.10.1 → 3.11.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 3.11.0
2
+
3
+ - `TestFontsForm` can group its family checkboxes by superfamily. With *Family list* set to *Grouped by superfamily* under *Settings → Test fonts* – or `testFonts: { familyGrouping: "superfamily" }` in the config, or the `familyGrouping` prop (`family-grouping` on `<fontdue-test-fonts-form>`) – each superfamily gets a checkbox that ticks all of its families, listed beneath it in `.test-fonts__family-group__families`.
4
+
1
5
  ## 3.10.1
2
6
 
3
7
  - Fixed the `TypeTester`, `FeatureTesters`, `CharacterViewer` and `StoreModal` never rendering a font in some Safari versions when its family or style name contains punctuation, such as an apostrophe, or starts with a digit.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @generated SignedSource<<47f718aeae16846330759690c693dce6>>
2
+ * @generated SignedSource<<0a0e4ebec2aeeb79d7f6e04437e7b7cc>>
3
3
  * @lightSyntaxTransform
4
4
  * @nogrep
5
5
  */
@@ -12,6 +12,7 @@ export type TestFontsForm_Query$data = {
12
12
  readonly viewer: {
13
13
  readonly fontdueConfig: {
14
14
  readonly testFonts: {
15
+ readonly familyGrouping: string;
15
16
  readonly familySelection: string;
16
17
  };
17
18
  } | null;
@@ -26,6 +27,10 @@ export type TestFontsForm_Query$data = {
26
27
  readonly families: ReadonlyArray<{
27
28
  readonly id: string;
28
29
  readonly name: string;
30
+ readonly parent: {
31
+ readonly id: string;
32
+ readonly name: string;
33
+ } | null;
29
34
  }>;
30
35
  } | null;
31
36
  };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @generated SignedSource<<47f718aeae16846330759690c693dce6>>
2
+ * @generated SignedSource<<0a0e4ebec2aeeb79d7f6e04437e7b7cc>>
3
3
  * @lightSyntaxTransform
4
4
  * @nogrep
5
5
  */
@@ -77,6 +77,12 @@ const node = function () {
77
77
  "kind": "ScalarField",
78
78
  "name": "familySelection",
79
79
  "storageKey": null
80
+ }, {
81
+ "alias": null,
82
+ "args": null,
83
+ "kind": "ScalarField",
84
+ "name": "familyGrouping",
85
+ "storageKey": null
80
86
  }],
81
87
  "storageKey": null
82
88
  }],
@@ -90,6 +96,13 @@ const node = function () {
90
96
  "storageKey": null
91
97
  },
92
98
  v5 = {
99
+ "alias": null,
100
+ "args": null,
101
+ "kind": "ScalarField",
102
+ "name": "name",
103
+ "storageKey": null
104
+ },
105
+ v6 = {
93
106
  "alias": null,
94
107
  "args": [{
95
108
  "kind": "Variable",
@@ -117,11 +130,14 @@ const node = function () {
117
130
  "kind": "LinkedField",
118
131
  "name": "families",
119
132
  "plural": true,
120
- "selections": [v4 /*: any*/, {
133
+ "selections": [v4 /*: any*/, v5 /*: any*/, {
121
134
  "alias": null,
122
135
  "args": null,
123
- "kind": "ScalarField",
124
- "name": "name",
136
+ "concreteType": "FontCollection",
137
+ "kind": "LinkedField",
138
+ "name": "parent",
139
+ "plural": false,
140
+ "selections": [v4 /*: any*/, v5 /*: any*/],
125
141
  "storageKey": null
126
142
  }],
127
143
  "storageKey": null
@@ -141,7 +157,7 @@ const node = function () {
141
157
  "kind": "LinkedField",
142
158
  "name": "viewer",
143
159
  "plural": false,
144
- "selections": [v2 /*: any*/, v3 /*: any*/, v5 /*: any*/],
160
+ "selections": [v2 /*: any*/, v3 /*: any*/, v6 /*: any*/],
145
161
  "storageKey": null
146
162
  }],
147
163
  "type": "RootQueryType",
@@ -159,19 +175,19 @@ const node = function () {
159
175
  "kind": "LinkedField",
160
176
  "name": "viewer",
161
177
  "plural": false,
162
- "selections": [v2 /*: any*/, v3 /*: any*/, v5 /*: any*/, v4 /*: any*/],
178
+ "selections": [v2 /*: any*/, v3 /*: any*/, v6 /*: any*/, v4 /*: any*/],
163
179
  "storageKey": null
164
180
  }]
165
181
  },
166
182
  "params": {
167
- "cacheID": "42a58842afe113b89a0bc969738246a8",
183
+ "cacheID": "f0d8dba45eec4868101fe71af251133a",
168
184
  "id": null,
169
185
  "metadata": {},
170
186
  "name": "TestFontsForm_Query",
171
187
  "operationKind": "query",
172
- "text": "query TestFontsForm_Query(\n $collectionIds: [ID!]\n $collectionSlugs: [String!]\n) {\n viewer {\n settings {\n eulaAgreementLabel(format: HTML)\n newsletterOptInLabel(format: HTML)\n recaptchaEnabled\n recaptchaSiteKey\n }\n fontdueConfig {\n testFonts {\n familySelection\n }\n }\n testFonts(collectionIds: $collectionIds, collectionSlugs: $collectionSlugs) {\n archiveUrl\n families {\n id\n name\n }\n }\n id\n }\n}\n"
188
+ "text": "query TestFontsForm_Query(\n $collectionIds: [ID!]\n $collectionSlugs: [String!]\n) {\n viewer {\n settings {\n eulaAgreementLabel(format: HTML)\n newsletterOptInLabel(format: HTML)\n recaptchaEnabled\n recaptchaSiteKey\n }\n fontdueConfig {\n testFonts {\n familySelection\n familyGrouping\n }\n }\n testFonts(collectionIds: $collectionIds, collectionSlugs: $collectionSlugs) {\n archiveUrl\n families {\n id\n name\n parent {\n id\n name\n }\n }\n }\n id\n }\n}\n"
173
189
  }
174
190
  };
175
191
  }();
176
- node.hash = "da75e312a602ac0ee2dfb79271299c77";
192
+ node.hash = "9ef2c42a7043469fec07a31649623766";
177
193
  export default node;
@@ -51,6 +51,7 @@ const families = [{
51
51
  }];
52
52
  const viewer = function (familySelection) {
53
53
  let list = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : families;
54
+ let familyGrouping = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'flat';
54
55
  return {
55
56
  viewer: {
56
57
  settings: {
@@ -61,7 +62,8 @@ const viewer = function (familySelection) {
61
62
  },
62
63
  fontdueConfig: {
63
64
  testFonts: {
64
- familySelection
65
+ familySelection,
66
+ familyGrouping
65
67
  }
66
68
  },
67
69
  testFonts: {
@@ -147,4 +149,75 @@ describe('TestFontsForm family choice', () => {
147
149
  expect(container.querySelector('form')).toBeNull();
148
150
  expect(container.textContent).toContain('Test fonts aren’t available for this family.');
149
151
  });
152
+ });
153
+ describe('TestFontsForm family grouping', () => {
154
+ const sans = {
155
+ id: 'U3VwZXI6MQ==',
156
+ name: 'Example'
157
+ };
158
+ const grouped = [{
159
+ id: 'RmFtaWx5OjE=',
160
+ name: 'Example Sans',
161
+ parent: sans
162
+ }, {
163
+ id: 'RmFtaWx5OjI=',
164
+ name: 'Solo',
165
+ parent: null
166
+ }, {
167
+ id: 'RmFtaWx5OjM=',
168
+ name: 'Example Serif',
169
+ parent: sans
170
+ },
171
+ // The only family offered from its superfamily: listed on its own.
172
+ {
173
+ id: 'RmFtaWx5OjQ=',
174
+ name: 'Lonely',
175
+ parent: {
176
+ id: 'U3VwZXI6Mg==',
177
+ name: 'Lonely Super'
178
+ }
179
+ }];
180
+ const superfamilyBoxes = () => [...container.querySelectorAll('.test-fonts__superfamily input')];
181
+ it('lists the families flat by default', () => {
182
+ queryRef.data = viewer('choice', grouped);
183
+ render();
184
+ expect(superfamilyBoxes()).toHaveLength(0);
185
+ expect(familyCheckboxes().map(box => box.value)).toEqual(grouped.map(f => f.id));
186
+ });
187
+ it('groups families under their superfamily, which ticks them all', () => {
188
+ queryRef.data = viewer('choice', grouped, 'superfamily');
189
+ render();
190
+ expect(superfamilyBoxes()).toHaveLength(1);
191
+ const group = container.querySelector('.test-fonts__family-group');
192
+ expect(group.textContent).toContain('Example');
193
+ expect([...group.querySelectorAll('.test-fonts__family input')].map(b => b.value)).toEqual(['RmFtaWx5OjE=', 'RmFtaWx5OjM=']);
194
+ // The group sits where its first family fell; the rest keep their order.
195
+ expect(familyCheckboxes().map(box => box.value)).toEqual(['RmFtaWx5OjE=', 'RmFtaWx5OjM=', 'RmFtaWx5OjI=', 'RmFtaWx5OjQ=']);
196
+ expect(container.textContent).not.toContain('Lonely Super');
197
+ act(() => superfamilyBoxes()[0].click());
198
+ expect(hiddenIds()).toEqual(['RmFtaWx5OjE=', 'RmFtaWx5OjM=']);
199
+ expect(superfamilyBoxes()[0].checked).toBe(true);
200
+
201
+ // Unticking one family leaves the superfamily unticked.
202
+ act(() => familyCheckboxes()[1].click());
203
+ expect(hiddenIds()).toEqual(['RmFtaWx5OjE=']);
204
+ expect(superfamilyBoxes()[0].checked).toBe(false);
205
+ act(() => superfamilyBoxes()[0].click());
206
+ act(() => superfamilyBoxes()[0].click());
207
+ expect(hiddenIds()).toEqual([]);
208
+ });
209
+ it('lets the prop and code config override the dashboard grouping', () => {
210
+ queryRef.data = viewer('choice', grouped, 'superfamily');
211
+ render({}, {
212
+ testFonts: {
213
+ familyGrouping: 'flat'
214
+ }
215
+ });
216
+ expect(superfamilyBoxes()).toHaveLength(0);
217
+ queryRef.data = viewer('choice', grouped);
218
+ render({
219
+ familyGrouping: 'superfamily'
220
+ });
221
+ expect(superfamilyBoxes()).toHaveLength(1);
222
+ });
150
223
  });
@@ -29,8 +29,10 @@ interface PreviewConfig {
29
29
  clientSide?: boolean;
30
30
  }
31
31
  export type TestFontsFamilySelection = 'all' | 'choice';
32
+ export type TestFontsFamilyGrouping = 'flat' | 'superfamily';
32
33
  interface TestFontsConfig {
33
34
  familySelection?: TestFontsFamilySelection;
35
+ familyGrouping?: TestFontsFamilyGrouping;
34
36
  }
35
37
  export interface Config {
36
38
  form?: FormConfig;
@@ -101,6 +103,7 @@ export declare const makeConfig: (config?: Config) => {
101
103
  };
102
104
  testFonts: {
103
105
  familySelection: TestFontsFamilySelection | undefined;
106
+ familyGrouping: TestFontsFamilyGrouping | undefined;
104
107
  };
105
108
  storeModal: {
106
109
  indexLayout: "styled-aa" | "styled-font-names";
@@ -182,6 +185,7 @@ declare const _default: React.Context<{
182
185
  };
183
186
  testFonts: {
184
187
  familySelection: TestFontsFamilySelection | undefined;
188
+ familyGrouping: TestFontsFamilyGrouping | undefined;
185
189
  };
186
190
  storeModal: {
187
191
  indexLayout: "styled-aa" | "styled-font-names";
@@ -88,7 +88,7 @@ export const mergeConfig = (base, override) => {
88
88
  return deepMerge(base, override);
89
89
  };
90
90
  export const makeConfig = config => {
91
- var _config$form, _config$testFonts, _config$storeModal, _config$storeModal2, _config$storeModal3, _config$storeModal4, _config$stripe, _config$tracking, _config$tracking2, _config$tracking3, _config$tracking4, _config$preview, _config$preview2, _config$preview3;
91
+ var _config$form, _config$testFonts, _config$testFonts2, _config$storeModal, _config$storeModal2, _config$storeModal3, _config$storeModal4, _config$stripe, _config$tracking, _config$tracking2, _config$tracking3, _config$tracking4, _config$preview, _config$preview2, _config$preview3;
92
92
  return {
93
93
  typeTester: makeTypeTesterConfig(config === null || config === void 0 ? void 0 : config.typeTester),
94
94
  form: {
@@ -98,7 +98,8 @@ export const makeConfig = config => {
98
98
  // Deliberately no default: the form falls back to the dashboard setting
99
99
  // it reads in its own query (the npm path never merges server config
100
100
  // into `children`'s context – see ServerConfigProvider).
101
- familySelection: config === null || config === void 0 ? void 0 : (_config$testFonts = config.testFonts) === null || _config$testFonts === void 0 ? void 0 : _config$testFonts.familySelection
101
+ familySelection: config === null || config === void 0 ? void 0 : (_config$testFonts = config.testFonts) === null || _config$testFonts === void 0 ? void 0 : _config$testFonts.familySelection,
102
+ familyGrouping: config === null || config === void 0 ? void 0 : (_config$testFonts2 = config.testFonts) === null || _config$testFonts2 === void 0 ? void 0 : _config$testFonts2.familyGrouping
102
103
  },
103
104
  storeModal: {
104
105
  indexLayout: (config === null || config === void 0 ? void 0 : (_config$storeModal = config.storeModal) === null || _config$storeModal === void 0 ? void 0 : _config$storeModal.indexLayout) ?? 'styled-aa',
@@ -6,6 +6,7 @@ interface TestFontsFormElement_props {
6
6
  collectionId?: string;
7
7
  collectionSlug?: string;
8
8
  familySelection?: string;
9
+ familyGrouping?: string;
9
10
  }
10
- declare const TestFontsFormElement: ({ familySelection, ...props }: TestFontsFormElement_props) => React.JSX.Element;
11
+ declare const TestFontsFormElement: ({ familySelection, familyGrouping, ...props }: TestFontsFormElement_props) => React.JSX.Element;
11
12
  export default TestFontsFormElement;
@@ -2,12 +2,15 @@ function _extends() { return _extends = Object.assign ? Object.assign.bind() : f
2
2
  import React from 'react';
3
3
  import TestFontsFormFetcher from './index.js';
4
4
  const FAMILY_SELECTIONS = ['all', 'choice'];
5
+ const FAMILY_GROUPINGS = ['flat', 'superfamily'];
5
6
  const TestFontsFormElement = _ref => {
6
7
  let {
7
8
  familySelection,
9
+ familyGrouping,
8
10
  ...props
9
11
  } = _ref;
10
12
  return /*#__PURE__*/React.createElement(TestFontsFormFetcher, _extends({}, props, {
13
+ familyGrouping: familyGrouping && FAMILY_GROUPINGS.includes(familyGrouping) ? familyGrouping : undefined,
11
14
  familySelection: familySelection && FAMILY_SELECTIONS.includes(familySelection) ? familySelection : undefined,
12
15
  newsletterCheckboxChecked: props.newsletterCheckboxChecked === 'true' ? true : false
13
16
  }));
@@ -0,0 +1,16 @@
1
+ import { TestFontsFamilyGrouping } from '../ConfigContext.js';
2
+ interface Superfamily {
3
+ readonly id: string;
4
+ readonly name: string | null | undefined;
5
+ }
6
+ export interface TestFontsFamily {
7
+ readonly id: string;
8
+ readonly name: string | null | undefined;
9
+ readonly parent?: Superfamily | null;
10
+ }
11
+ export interface FamilyGroup<Family extends TestFontsFamily> {
12
+ superfamily: Superfamily | null;
13
+ families: Family[];
14
+ }
15
+ export declare function groupFamilies<Family extends TestFontsFamily>(families: readonly Family[], grouping: TestFontsFamilyGrouping): FamilyGroup<Family>[];
16
+ export {};
@@ -0,0 +1,34 @@
1
+ // One row of the family checkboxes: a lone family (`superfamily` null), or a
2
+ // superfamily with its families nested beneath it.
3
+
4
+ // Lays the families out for the form's checkboxes, keeping their order: flat
5
+ // lists each family on its own; 'superfamily' gathers families under their
6
+ // superfamily, placed where its first family falls. A superfamily with only one
7
+ // family to offer adds nothing over that family, so the family stands alone.
8
+ export function groupFamilies(families, grouping) {
9
+ const alone = family => ({
10
+ superfamily: null,
11
+ families: [family]
12
+ });
13
+ if (grouping !== 'superfamily') return families.map(alone);
14
+ const groups = [];
15
+ const bySuperfamily = new Map();
16
+ for (const family of families) {
17
+ const superfamily = family.parent;
18
+ if (!superfamily) {
19
+ groups.push(alone(family));
20
+ continue;
21
+ }
22
+ let group = bySuperfamily.get(superfamily.id);
23
+ if (!group) {
24
+ group = {
25
+ superfamily,
26
+ families: []
27
+ };
28
+ bySuperfamily.set(superfamily.id, group);
29
+ groups.push(group);
30
+ }
31
+ group.families.push(family);
32
+ }
33
+ return groups.map(group => group.families.length > 1 ? group : alone(group.families[0]));
34
+ }
@@ -1,13 +1,14 @@
1
1
  import React from 'react';
2
2
  import { TestFontsForm_Query } from '../../__generated__/TestFontsForm_Query.graphql.js';
3
3
  import { SerializablePreloadedQuery, type LoadQueryOptions } from '../../relay/loadSerializableQuery.js';
4
- import { Config, TestFontsFamilySelection } from '../ConfigContext.js';
4
+ import { Config, TestFontsFamilyGrouping, TestFontsFamilySelection } from '../ConfigContext.js';
5
5
  import { TestFontsFormCollectionProps } from './variables.js';
6
6
  export interface TestFontsForm_props extends TestFontsFormCollectionProps {
7
7
  agreementLabel?: string;
8
8
  downloadLabel?: string;
9
9
  newsletterCheckboxChecked?: boolean;
10
10
  familySelection?: TestFontsFamilySelection;
11
+ familyGrouping?: TestFontsFamilyGrouping;
11
12
  }
12
13
  export type TestFontsFormPreloadedQuery = SerializablePreloadedQuery<TestFontsForm_Query>;
13
14
  export type LoadTestFontsFormQueryVariables = TestFontsFormCollectionProps;
@@ -18,6 +18,7 @@ import useSerializablePreloadedQuery from '../../relay/useSerializablePreloadedQ
18
18
  import { EnsureFontdueContext, LazyQueryBoundary } from '../FontdueContextProvider/index.js';
19
19
  import ConfigContext from '../ConfigContext.js';
20
20
  import { testFontsFormQueryVariables } from './variables.js';
21
+ import { groupFamilies } from './familyGroups.js';
21
22
  import { useRecaptchaTimeout, isRecaptchaScriptLoaded, RECAPTCHA_UNAVAILABLE_MESSAGE } from '../../hooks/useRecaptchaTimeout.js';
22
23
  const updateCustomerMutation = (_TestFontsFormUpdateCustomerMutation.hash && _TestFontsFormUpdateCustomerMutation.hash !== "ba56958399f0893bd667ff02c33a6975" && console.error("The definition of 'TestFontsFormUpdateCustomerMutation' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _TestFontsFormUpdateCustomerMutation);
23
24
  // The archive endpoint's family filter: `collection_ids[]` narrows the ZIP to
@@ -41,7 +42,7 @@ const TestFontsDownloading = _ref => {
41
42
  }, "here"), ".")));
42
43
  };
43
44
  const TestFontsFormComponent = _ref2 => {
44
- var _data$viewer$testFont, _data$viewer$fontdueC, _data$viewer$settings, _data$viewer$settings2, _data$viewer$testFont2;
45
+ var _data$viewer$testFont, _data$viewer$fontdueC, _data$viewer$fontdueC2, _data$viewer$settings, _data$viewer$settings2, _data$viewer$testFont2;
45
46
  let {
46
47
  data,
47
48
  agreementLabel,
@@ -49,7 +50,8 @@ const TestFontsFormComponent = _ref2 => {
49
50
  newsletterCheckboxChecked = false,
50
51
  collectionId,
51
52
  collectionSlug,
52
- familySelection: familySelectionProp
53
+ familySelection: familySelectionProp,
54
+ familyGrouping: familyGroupingProp
53
55
  } = _ref2;
54
56
  const [email, setEmail] = useState('');
55
57
  const [name, setName] = useState('');
@@ -82,11 +84,30 @@ const TestFontsFormComponent = _ref2 => {
82
84
  // This form's prop, then code config (which, on the script-tag path, already
83
85
  // has the dashboard setting merged beneath it), then the dashboard setting.
84
86
  const familySelection = familySelectionProp ?? config.testFonts.familySelection ?? ((_data$viewer$fontdueC = data.viewer.fontdueConfig) === null || _data$viewer$fontdueC === void 0 ? void 0 : _data$viewer$fontdueC.testFonts.familySelection) ?? 'all';
87
+ // Resolved the same way as the family selection.
88
+ const familyGrouping = familyGroupingProp ?? config.testFonts.familyGrouping ?? ((_data$viewer$fontdueC2 = data.viewer.fontdueConfig) === null || _data$viewer$fontdueC2 === void 0 ? void 0 : _data$viewer$fontdueC2.testFonts.familyGrouping) ?? 'flat';
85
89
  // One family leaves nothing to choose.
86
90
  const choosing = !limited && familySelection === 'choice' && families.length > 1;
87
91
  const downloadIds = limited ? families.map(family => family.id) : choosing ? families.map(family => family.id).filter(id => chosenIds.includes(id)) : [];
88
- const toggleFamily = (id, checked) => setChosenIds(ids => checked ? [...ids, id] : ids.filter(other => other !== id));
89
- const allChosen = families.every(family => chosenIds.includes(family.id));
92
+
93
+ // Ticks or unticks families – one, or a superfamily's.
94
+ const toggleFamilies = (toggled, checked) => setChosenIds(ids => [...ids.filter(id => !toggled.includes(id)), ...(checked ? toggled : [])]);
95
+ const allChosen = list => list.every(family => chosenIds.includes(family.id));
96
+ const familyGroups = groupFamilies(families, familyGrouping);
97
+ const familyField = family => {
98
+ const fieldId = `${idPrefix}-family-${families.indexOf(family)}`;
99
+ return /*#__PURE__*/React.createElement("div", {
100
+ className: "test-fonts__checkbox-field test-fonts__family",
101
+ key: family.id
102
+ }, /*#__PURE__*/React.createElement(Checkbox, {
103
+ id: fieldId,
104
+ value: family.id,
105
+ checked: chosenIds.includes(family.id),
106
+ onChange: event => toggleFamilies([family.id], event.target.checked)
107
+ }), /*#__PURE__*/React.createElement("label", {
108
+ htmlFor: fieldId
109
+ }, family.name));
110
+ };
90
111
  const recaptchaEnabled = ((_data$viewer$settings = data.viewer.settings) === null || _data$viewer$settings === void 0 ? void 0 : _data$viewer$settings.recaptchaEnabled) ?? false;
91
112
  const recaptchaSiteKey = (_data$viewer$settings2 = data.viewer.settings) === null || _data$viewer$settings2 === void 0 ? void 0 : _data$viewer$settings2.recaptchaSiteKey;
92
113
  const submitForm = useCallback(token => {
@@ -212,21 +233,30 @@ const TestFontsFormComponent = _ref2 => {
212
233
  className: "test-fonts__checkbox-field test-fonts__families__all"
213
234
  }, /*#__PURE__*/React.createElement(Checkbox, {
214
235
  id: `${idPrefix}-families-all`,
215
- checked: allChosen,
236
+ checked: allChosen(families),
216
237
  onChange: event => setChosenIds(event.target.checked ? families.map(family => family.id) : [])
217
238
  }), /*#__PURE__*/React.createElement("label", {
218
239
  htmlFor: `${idPrefix}-families-all`
219
- }, "Select all")), families.map((family, index) => /*#__PURE__*/React.createElement("div", {
220
- className: "test-fonts__checkbox-field test-fonts__family",
221
- key: family.id
222
- }, /*#__PURE__*/React.createElement(Checkbox, {
223
- id: `${idPrefix}-family-${index}`,
224
- value: family.id,
225
- checked: chosenIds.includes(family.id),
226
- onChange: event => toggleFamily(family.id, event.target.checked)
227
- }), /*#__PURE__*/React.createElement("label", {
228
- htmlFor: `${idPrefix}-family-${index}`
229
- }, family.name)))), /*#__PURE__*/React.createElement("div", {
240
+ }, "Select all")), familyGroups.map((_ref4, groupIndex) => {
241
+ let {
242
+ superfamily,
243
+ families: grouped
244
+ } = _ref4;
245
+ return superfamily ? /*#__PURE__*/React.createElement("div", {
246
+ className: "test-fonts__family-group",
247
+ key: superfamily.id
248
+ }, /*#__PURE__*/React.createElement("div", {
249
+ className: "test-fonts__checkbox-field test-fonts__superfamily"
250
+ }, /*#__PURE__*/React.createElement(Checkbox, {
251
+ id: `${idPrefix}-superfamily-${groupIndex}`,
252
+ checked: allChosen(grouped),
253
+ onChange: event => toggleFamilies(grouped.map(family => family.id), event.target.checked)
254
+ }), /*#__PURE__*/React.createElement("label", {
255
+ htmlFor: `${idPrefix}-superfamily-${groupIndex}`
256
+ }, superfamily.name)), /*#__PURE__*/React.createElement("div", {
257
+ className: "test-fonts__family-group__families"
258
+ }, grouped.map(familyField))) : familyField(grouped[0]);
259
+ })), /*#__PURE__*/React.createElement("div", {
230
260
  className: "test-fonts__section"
231
261
  }, /*#__PURE__*/React.createElement(TextField, {
232
262
  label: "Name *",
@@ -289,7 +319,7 @@ const TestFontsFormComponent = _ref2 => {
289
319
  value: id
290
320
  }))));
291
321
  };
292
- const query = (_TestFontsForm_Query.hash && _TestFontsForm_Query.hash !== "da75e312a602ac0ee2dfb79271299c77" && console.error("The definition of 'TestFontsForm_Query' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _TestFontsForm_Query);
322
+ const query = (_TestFontsForm_Query.hash && _TestFontsForm_Query.hash !== "9ef2c42a7043469fec07a31649623766" && console.error("The definition of 'TestFontsForm_Query' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _TestFontsForm_Query);
293
323
  // Pass the same `collectionId` / `collectionSlug` as the form's props.
294
324
  export async function loadTestFontsFormQuery(variables, options) {
295
325
  // Before the collection variables existed this took `options` alone.
@@ -298,11 +328,11 @@ export async function loadTestFontsFormQuery(variables, options) {
298
328
  }
299
329
  return loadSerializableQuery(TestFontsFormQueryNode, testFontsFormQueryVariables(variables ?? {}), options);
300
330
  }
301
- export function TestFontsFormPreloadedQueryRenderer(_ref4) {
331
+ export function TestFontsFormPreloadedQueryRenderer(_ref5) {
302
332
  let {
303
333
  preloadedQuery,
304
334
  ...rest
305
- } = _ref4;
335
+ } = _ref5;
306
336
  // The query node lets the hook commit the payload into the store, so
307
337
  // usePreloadedQuery resolves synchronously during SSR instead of
308
338
  // refetching (the response cache only exists in the browser).
@@ -322,11 +352,11 @@ function TestFontsFormLazyQueryRenderer(props) {
322
352
  // (server-side preload path) or fetches lazily on mount. `EnsureFontdueContext`
323
353
  // only self-wraps with `FontdueContextProvider` when no parent Fontdue
324
354
  // context is present, seeding it with `config`.
325
- export default function TestFontsForm(_ref5) {
355
+ export default function TestFontsForm(_ref6) {
326
356
  let {
327
357
  config,
328
358
  ...props
329
- } = _ref5;
359
+ } = _ref6;
330
360
  return /*#__PURE__*/React.createElement(EnsureFontdueContext, {
331
361
  config: config
332
362
  }, props.preloadedQuery ? /*#__PURE__*/React.createElement(TestFontsFormPreloadedQueryRenderer, _extends({}, props, {
@@ -14,12 +14,14 @@ interface VerticalMetrics {
14
14
  * CSS must use to render it: `"<cssFamily> <styleName>"`, made safe to parse
15
15
  * as an unquoted CSS font-family.
16
16
  *
17
- * Some Safari builds parse the FontFace family argument as CSS, so a name
18
- * like "Hell's Gate fairypools" (the apostrophe opens an unterminated string)
19
- * or "0712 Regular" (an identifier can't start with a digit) makes `load()`
20
- * reject with "SyntaxError: The string did not match the expected pattern."
21
- * and the style never renders, while Chrome and other WebKit builds take the
22
- * argument literally. A name that is valid either way works everywhere:
17
+ * Safari 26.0.x (macOS Tahoe / iOS 26 before their point updates) parses the
18
+ * FontFace family argument as CSS, so a name like "Hell's Gate fairypools"
19
+ * (the apostrophe opens an unterminated string) or "0712 Regular" (an
20
+ * identifier can't start with a digit) makes `load()` reject with
21
+ * "SyntaxError: The string did not match the expected pattern." and the style
22
+ * never renders. Chrome and other Safari builds (17.3, 18.4, 26.4+, 27.0 were
23
+ * checked) take the argument literally. A name that is valid either way
24
+ * works everywhere:
23
25
  *
24
26
  * - an ASCII character other than a letter, digit, `-` or `_` is escaped as
25
27
  * `_<hex code>` (`'` → `_27`), so distinct names stay distinct;
@@ -7,12 +7,14 @@ const RESERVED_FAMILY_WORDS = new Set(['inherit', 'initial', 'unset', 'revert',
7
7
  * CSS must use to render it: `"<cssFamily> <styleName>"`, made safe to parse
8
8
  * as an unquoted CSS font-family.
9
9
  *
10
- * Some Safari builds parse the FontFace family argument as CSS, so a name
11
- * like "Hell's Gate fairypools" (the apostrophe opens an unterminated string)
12
- * or "0712 Regular" (an identifier can't start with a digit) makes `load()`
13
- * reject with "SyntaxError: The string did not match the expected pattern."
14
- * and the style never renders, while Chrome and other WebKit builds take the
15
- * argument literally. A name that is valid either way works everywhere:
10
+ * Safari 26.0.x (macOS Tahoe / iOS 26 before their point updates) parses the
11
+ * FontFace family argument as CSS, so a name like "Hell's Gate fairypools"
12
+ * (the apostrophe opens an unterminated string) or "0712 Regular" (an
13
+ * identifier can't start with a digit) makes `load()` reject with
14
+ * "SyntaxError: The string did not match the expected pattern." and the style
15
+ * never renders. Chrome and other Safari builds (17.3, 18.4, 26.4+, 27.0 were
16
+ * checked) take the argument literally. A name that is valid either way
17
+ * works everywhere:
16
18
  *
17
19
  * - an ASCII character other than a letter, digit, `-` or `_` is escaped as
18
20
  * `_<hex code>` (`'` → `_27`), so distinct names stay distinct;
package/dist/fontdue.css CHANGED
@@ -185,6 +185,9 @@
185
185
  .test-fonts__families .test-fonts__checkbox-field {
186
186
  margin-bottom: 10px;
187
187
  }
188
+ .test-fonts__family-group__families {
189
+ padding-left: calc(var(--checkbox-size, 20px) + var(--checkbox-margin, 10px));
190
+ }
188
191
  .test-fonts__agreement[data-highlighted=true] {
189
192
  color: red;
190
193
  }
package/dist/version.js CHANGED
@@ -8,4 +8,4 @@
8
8
  // (Fontage.ClientVersions) to spot sites that need to upgrade before a
9
9
  // feature that depends on a newer client can work for them.
10
10
  export const CLIENT_VERSION_HEADER = 'fontdue-client-version';
11
- export const version = "3.10.1";
11
+ export const version = "3.11.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fontdue-js",
3
- "version": "3.10.1",
3
+ "version": "3.11.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "build": "npm run relay && run-p build-js build-css build-ts",