fontdue-js 3.0.0-alpha9 → 3.0.1

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 (135) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +182 -13
  3. package/dist/__generated__/CartOrderCompleteOrderMutation.graphql.d.ts +1 -1
  4. package/dist/__generated__/CartOrderCompleteOrderMutation.graphql.js +9 -3
  5. package/dist/__generated__/CartOrderRemoveDiscountMutation.graphql.d.ts +1 -1
  6. package/dist/__generated__/CartOrderRemoveDiscountMutation.graphql.js +9 -3
  7. package/dist/__generated__/CartOrderUpdateMutation.graphql.d.ts +1 -1
  8. package/dist/__generated__/CartOrderUpdateMutation.graphql.js +9 -3
  9. package/dist/__generated__/CartQuery.graphql.d.ts +1 -1
  10. package/dist/__generated__/CartQuery.graphql.js +9 -3
  11. package/dist/__generated__/CartStateUpdateMutation.graphql.d.ts +1 -1
  12. package/dist/__generated__/CartStateUpdateMutation.graphql.js +9 -3
  13. package/dist/__generated__/CharacterViewerIDQuery.graphql.d.ts +1 -1
  14. package/dist/__generated__/CharacterViewerIDQuery.graphql.js +9 -3
  15. package/dist/__generated__/CharacterViewerSlugQuery.graphql.d.ts +1 -1
  16. package/dist/__generated__/CharacterViewerSlugQuery.graphql.js +9 -3
  17. package/dist/__generated__/CharacterViewerStyleRefetchQuery.graphql.d.ts +1 -1
  18. package/dist/__generated__/CharacterViewerStyleRefetchQuery.graphql.js +9 -3
  19. package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.d.ts +1 -1
  20. package/dist/__generated__/CheckoutUpdateCustomerMutation.graphql.js +9 -3
  21. package/dist/__generated__/CheckoutUpdateOrderMutation.graphql.d.ts +1 -1
  22. package/dist/__generated__/CheckoutUpdateOrderMutation.graphql.js +9 -3
  23. package/dist/__generated__/CollectionAa_Query.graphql.d.ts +1 -1
  24. package/dist/__generated__/CollectionAa_Query.graphql.js +9 -3
  25. package/dist/__generated__/FontFamiliesQuery.graphql.d.ts +1 -1
  26. package/dist/__generated__/FontFamiliesQuery.graphql.js +9 -3
  27. package/dist/__generated__/FontdueAdminToolbarQuery.graphql.d.ts +20 -0
  28. package/dist/__generated__/FontdueAdminToolbarQuery.graphql.js +80 -0
  29. package/dist/__generated__/FontdueAdminToolbarTokenMutation.graphql.d.ts +18 -0
  30. package/dist/__generated__/FontdueAdminToolbarTokenMutation.graphql.js +56 -0
  31. package/dist/__generated__/PrecartAddToCartMutation.graphql.d.ts +1 -1
  32. package/dist/__generated__/PrecartAddToCartMutation.graphql.js +9 -3
  33. package/dist/__generated__/StoreModalCartQuery.graphql.d.ts +1 -1
  34. package/dist/__generated__/StoreModalCartQuery.graphql.js +9 -3
  35. package/dist/__generated__/StoreModalContainerQuery.graphql.d.ts +1 -1
  36. package/dist/__generated__/StoreModalContainerQuery.graphql.js +9 -3
  37. package/dist/__generated__/StoreModalIndexQuery.graphql.d.ts +1 -1
  38. package/dist/__generated__/StoreModalIndexQuery.graphql.js +9 -3
  39. package/dist/__generated__/StoreModalProductQuery.graphql.d.ts +1 -1
  40. package/dist/__generated__/StoreModalProductQuery.graphql.js +9 -3
  41. package/dist/__generated__/StoreModalProductRefetchQuery.graphql.d.ts +1 -1
  42. package/dist/__generated__/StoreModalProductRefetchQuery.graphql.js +9 -3
  43. package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.d.ts +1 -1
  44. package/dist/__generated__/TestFontsFormUpdateCustomerMutation.graphql.js +9 -3
  45. package/dist/__generated__/TypeTesterStandaloneChangedStylesQuery.graphql.d.ts +1 -1
  46. package/dist/__generated__/TypeTesterStandaloneChangedStylesQuery.graphql.js +9 -3
  47. package/dist/__generated__/TypeTesterStandaloneQuery.graphql.d.ts +1 -1
  48. package/dist/__generated__/TypeTesterStandaloneQuery.graphql.js +9 -3
  49. package/dist/__generated__/TypeTestersChangedStylesQuery.graphql.d.ts +1 -1
  50. package/dist/__generated__/TypeTestersChangedStylesQuery.graphql.js +9 -3
  51. package/dist/__generated__/TypeTestersIDQuery.graphql.d.ts +1 -1
  52. package/dist/__generated__/TypeTestersIDQuery.graphql.js +9 -3
  53. package/dist/__generated__/TypeTestersRefetchQuery.graphql.d.ts +1 -1
  54. package/dist/__generated__/TypeTestersRefetchQuery.graphql.js +9 -3
  55. package/dist/__generated__/TypeTestersSlugQuery.graphql.d.ts +1 -1
  56. package/dist/__generated__/TypeTestersSlugQuery.graphql.js +9 -3
  57. package/dist/__generated__/useFontStyle_fontStyle.graphql.d.ts +2 -1
  58. package/dist/__generated__/useFontStyle_fontStyle.graphql.js +8 -2
  59. package/dist/__tests__/createFontdueFetch.test.js +276 -0
  60. package/dist/__tests__/imageLoader.test.js +62 -0
  61. package/dist/__tests__/metricFallback.test.js +74 -0
  62. package/dist/__tests__/networkFetch.test.js +125 -3
  63. package/dist/__tests__/nextAdapter.test.js +175 -60
  64. package/dist/__tests__/preview.test.js +217 -0
  65. package/dist/__tests__/previewServer.test.js +118 -0
  66. package/dist/__tests__/previewState.test.js +63 -0
  67. package/dist/__tests__/serverConfig.test.js +62 -0
  68. package/dist/components/BuyButton/index.d.ts +2 -2
  69. package/dist/components/BuyButton/index.js +3 -3
  70. package/dist/components/CharacterViewer/index.d.ts +2 -2
  71. package/dist/components/CharacterViewer/index.js +20 -11
  72. package/dist/components/ConfigContext.d.ts +21 -2
  73. package/dist/components/ConfigContext.js +12 -2
  74. package/dist/components/ConnectionErrorToolbar.d.ts +1 -0
  75. package/dist/components/ConnectionErrorToolbar.js +106 -0
  76. package/dist/components/FontdueAdminToolbar/index.d.ts +2 -0
  77. package/dist/components/FontdueAdminToolbar/index.js +299 -0
  78. package/dist/components/FontdueAdminToolbar/previewState.d.ts +7 -0
  79. package/dist/components/FontdueAdminToolbar/previewState.js +58 -0
  80. package/dist/components/FontdueContextProvider/index.js +6 -4
  81. package/dist/components/FontdueProvider/index.js +6 -1
  82. package/dist/components/FontdueProvider/index.server.d.ts +1 -0
  83. package/dist/components/FontdueProvider/index.server.js +10 -0
  84. package/dist/components/NewsletterSignup/index.d.ts +2 -2
  85. package/dist/components/NewsletterSignup/index.js +2 -2
  86. package/dist/components/Root/index.js +16 -2
  87. package/dist/components/TestFontsForm/index.d.ts +2 -2
  88. package/dist/components/TestFontsForm/index.js +2 -2
  89. package/dist/components/TypeTester/TypeTesterStandalone.d.ts +2 -2
  90. package/dist/components/TypeTester/TypeTesterStandalone.js +2 -2
  91. package/dist/components/TypeTesters/index.d.ts +2 -2
  92. package/dist/components/TypeTesters/index.js +3 -3
  93. package/dist/components/useFontStyle.d.ts +1 -0
  94. package/dist/components/useFontStyle.js +12 -3
  95. package/dist/corsError.d.ts +1 -5
  96. package/dist/corsError.js +23 -13
  97. package/dist/data/unicodeNamesUrl.d.ts +2 -0
  98. package/dist/data/unicodeNamesUrl.js +18 -0
  99. package/dist/data/unicodeNamesVersion.d.ts +1 -0
  100. package/dist/data/unicodeNamesVersion.js +4 -0
  101. package/dist/fallbackFontData.d.ts +2 -0
  102. package/dist/fallbackFontData.js +10 -0
  103. package/dist/fontdue.css +231 -4
  104. package/dist/loadFontdueProviderQuery.d.ts +2 -1
  105. package/dist/loadFontdueProviderQuery.js +5 -2
  106. package/dist/metricFallback.d.ts +48 -0
  107. package/dist/metricFallback.js +98 -0
  108. package/dist/next/image-loader.js +22 -3
  109. package/dist/next/index.d.ts +1 -2
  110. package/dist/next/index.js +14 -6
  111. package/dist/next/registerSingleTenantResolver.d.ts +1 -0
  112. package/dist/next/registerSingleTenantResolver.js +35 -0
  113. package/dist/next/revalidate.js +1 -1
  114. package/dist/next/tenant.d.ts +4 -4
  115. package/dist/next/tenant.js +89 -58
  116. package/dist/preview/constants.d.ts +9 -0
  117. package/dist/preview/constants.js +117 -0
  118. package/dist/preview/index.d.ts +53 -0
  119. package/dist/preview/index.js +190 -0
  120. package/dist/preview/server.d.ts +20 -0
  121. package/dist/preview/server.js +89 -0
  122. package/dist/relay/environment.d.ts +8 -0
  123. package/dist/relay/environment.js +81 -35
  124. package/dist/relay/loadSerializableQuery.d.ts +13 -3
  125. package/dist/relay/loadSerializableQuery.js +2 -0
  126. package/dist/relay/serverConfig.d.ts +5 -7
  127. package/dist/relay/serverConfig.js +83 -8
  128. package/dist/scripts/publishUnicodeData.js +68 -0
  129. package/dist/scripts/updateUnicodeData.js +41 -6
  130. package/dist/server/index.d.ts +37 -0
  131. package/dist/server/index.js +160 -0
  132. package/package.json +5 -1
  133. package/types/next-headers.d.ts +9 -0
  134. package/types/next-navigation.d.ts +4 -0
  135. package/vitest.config.ts +5 -0
@@ -0,0 +1,118 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { runWithPreview, isPreviewing, ambientPreviewHeaders } from '../preview/server.js';
3
+ import { createFontdueFetch } from '../server/index.js';
4
+ import { PREVIEW_TOKEN_COOKIE } from '../preview/constants.js';
5
+ beforeEach(() => {
6
+ vi.unstubAllGlobals();
7
+ });
8
+ function req(token) {
9
+ return new Request('https://store.test/', {
10
+ headers: token ? {
11
+ cookie: `${PREVIEW_TOKEN_COOKIE}=${token}`
12
+ } : {}
13
+ });
14
+ }
15
+ describe('runWithPreview', () => {
16
+ it('passes public (no-token) requests through untouched and cacheable', async () => {
17
+ const response = await runWithPreview(req(), async () => {
18
+ expect(isPreviewing()).toBe(false);
19
+ return new Response('ok', {
20
+ headers: {
21
+ 'Netlify-CDN-Cache-Control': 'public, durable, s-maxage=300'
22
+ }
23
+ });
24
+ });
25
+ expect(response.headers.get('Cache-Control')).toBeNull();
26
+ expect(response.headers.get('Netlify-CDN-Cache-Control')).toBe('public, durable, s-maxage=300');
27
+ });
28
+ it('exposes the token in ambient context while previewing', async () => {
29
+ await runWithPreview(req('tok123'), async () => {
30
+ expect(isPreviewing()).toBe(true);
31
+ expect(ambientPreviewHeaders()).toEqual({
32
+ authorization: 'Bearer tok123'
33
+ });
34
+ return new Response('ok');
35
+ });
36
+
37
+ // Context unwinds after the request.
38
+ expect(isPreviewing()).toBe(false);
39
+ expect(ambientPreviewHeaders()).toEqual({});
40
+ });
41
+ it('forces preview responses out of any shared/CDN cache', async () => {
42
+ const response = await runWithPreview(req('tok123'), async () => new Response('secret', {
43
+ headers: {
44
+ 'Netlify-CDN-Cache-Control': 'public, durable, s-maxage=31536000',
45
+ 'CDN-Cache-Control': 'public, s-maxage=300',
46
+ 'Cache-Control': 'public, max-age=0, must-revalidate'
47
+ }
48
+ }));
49
+ expect(response.headers.get('Cache-Control')).toBe('private, no-store');
50
+ expect(response.headers.get('Netlify-CDN-Cache-Control')).toBeNull();
51
+ expect(response.headers.get('CDN-Cache-Control')).toBeNull();
52
+ });
53
+ it('isolates concurrent preview contexts (no cross-request leak)', async () => {
54
+ const seen = {};
55
+ await Promise.all([runWithPreview(req('tokA'), async () => {
56
+ await new Promise(r => setTimeout(r, 15));
57
+ seen.a = ambientPreviewHeaders().authorization;
58
+ return new Response('a');
59
+ }), runWithPreview(req('tokB'), async () => {
60
+ seen.b = ambientPreviewHeaders().authorization;
61
+ return new Response('b');
62
+ }), runWithPreview(req(), async () => {
63
+ seen.public = ambientPreviewHeaders().authorization;
64
+ return new Response('public');
65
+ })]);
66
+ expect(seen.a).toBe('Bearer tokA');
67
+ expect(seen.b).toBe('Bearer tokB');
68
+ expect(seen.public).toBeUndefined();
69
+ });
70
+ });
71
+ describe('createFontdueFetch + ambient preview', () => {
72
+ function mockFetch() {
73
+ const fetchMock = vi.fn(async () => ({
74
+ status: 200,
75
+ json: async () => ({
76
+ data: {}
77
+ })
78
+ }));
79
+ vi.stubGlobal('fetch', fetchMock);
80
+ return fetchMock;
81
+ }
82
+ it('a module-level fetcher forwards the ambient token, no per-call plumbing', async () => {
83
+ const fetchMock = mockFetch();
84
+ const fetchGraphql = createFontdueFetch({
85
+ url: 'https://acme.fontdue.com'
86
+ });
87
+ await runWithPreview(req('admin-tok'), async () => {
88
+ await fetchGraphql('Q', 'query Q { __typename }');
89
+ return new Response('ok');
90
+ });
91
+ const init = fetchMock.mock.calls[0][1];
92
+ expect(init.headers.authorization).toBe('Bearer admin-tok');
93
+ });
94
+ it('does not forward a token for public requests', async () => {
95
+ const fetchMock = mockFetch();
96
+ const fetchGraphql = createFontdueFetch({
97
+ url: 'https://acme.fontdue.com'
98
+ });
99
+ await fetchGraphql('Q', 'query Q { __typename }');
100
+ const init = fetchMock.mock.calls[0][1];
101
+ expect(init.headers.authorization).toBeUndefined();
102
+ });
103
+ it('explicit per-fetcher headers override the ambient context', async () => {
104
+ const fetchMock = mockFetch();
105
+ const fetchGraphql = createFontdueFetch({
106
+ url: 'https://acme.fontdue.com',
107
+ headers: {
108
+ authorization: 'Bearer explicit'
109
+ }
110
+ });
111
+ await runWithPreview(req('ambient-tok'), async () => {
112
+ await fetchGraphql('Q', 'query Q { __typename }');
113
+ return new Response('ok');
114
+ });
115
+ const init = fetchMock.mock.calls[0][1];
116
+ expect(init.headers.authorization).toBe('Bearer explicit');
117
+ });
118
+ });
@@ -0,0 +1,63 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { derivePreviewState, expiresAtFromTokenResult, isSessionExpiredError } from '../components/FontdueAdminToolbar/previewState.js';
3
+
4
+ // The three-state derivation is the heart of the expired-preview fix, so it's
5
+ // tested in isolation (the full toolbar needs a browser + Relay environment +
6
+ // admin session to render, which isn't available here).
7
+ describe('derivePreviewState', () => {
8
+ const NOW = 1_000_000;
9
+ it('is "off" with no marker cookie', () => {
10
+ expect(derivePreviewState(false, undefined, NOW)).toBe('off');
11
+ expect(derivePreviewState(false, NOW + 1000, NOW)).toBe('off');
12
+ });
13
+ it('is "active" while the marker is present and the token is still valid', () => {
14
+ expect(derivePreviewState(true, NOW + 1, NOW)).toBe('active');
15
+ });
16
+ it('is "expired" once now has reached the stored expiry', () => {
17
+ expect(derivePreviewState(true, NOW, NOW)).toBe('expired');
18
+ expect(derivePreviewState(true, NOW - 1, NOW)).toBe('expired');
19
+ });
20
+ it('is "expired" for a marker with no/garbled expiry (stale legacy cookie)', () => {
21
+ expect(derivePreviewState(true, undefined, NOW)).toBe('expired');
22
+ });
23
+ });
24
+ describe('expiresAtFromTokenResult', () => {
25
+ it('accepts an epoch-ms number', () => {
26
+ expect(expiresAtFromTokenResult({
27
+ expiresAt: 123_456
28
+ })).toBe(123_456);
29
+ });
30
+ it('accepts an ISO-8601 string', () => {
31
+ const iso = '2030-01-01T00:00:00.000Z';
32
+ expect(expiresAtFromTokenResult({
33
+ expiresAt: iso
34
+ })).toBe(Date.parse(iso));
35
+ });
36
+ it('accepts a numeric string', () => {
37
+ expect(expiresAtFromTokenResult({
38
+ expiresAt: '987654321'
39
+ })).toBe(987654321);
40
+ });
41
+ it('falls back to now + 1h TTL when the field is absent (backend not yet shipped)', () => {
42
+ const before = Date.now();
43
+ expect(expiresAtFromTokenResult({
44
+ token: 'abc'
45
+ })).toBeGreaterThanOrEqual(before + 60 * 60 * 1000 - 5_000);
46
+ expect(expiresAtFromTokenResult(null)).toBeGreaterThanOrEqual(before + 60 * 60 * 1000 - 5_000);
47
+ });
48
+ });
49
+ describe('isSessionExpiredError', () => {
50
+ it('matches the resolver’s "Not authorized" message', () => {
51
+ expect(isSessionExpiredError([{
52
+ message: 'Not authorized: you must be signed in as an admin.'
53
+ }])).toBe(true);
54
+ });
55
+ it('is false for unrelated errors and empty/absent error lists', () => {
56
+ expect(isSessionExpiredError([{
57
+ message: 'Something else failed'
58
+ }])).toBe(false);
59
+ expect(isSessionExpiredError([])).toBe(false);
60
+ expect(isSessionExpiredError(null)).toBe(false);
61
+ expect(isSessionExpiredError(undefined)).toBe(false);
62
+ });
63
+ });
@@ -0,0 +1,62 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+
3
+ // The render-scoped slot and the ambient resolver are anchored on globalThis so
4
+ // they survive this module being bundled into more than one Next.js server
5
+ // chunk (the app/provider chunk vs. a per-embed chunk), which would otherwise
6
+ // give each chunk its own copy of the module-level state. Re-importing the
7
+ // module after vi.resetModules() faithfully simulates that second chunk: a
8
+ // fresh module instance with its own scope but the same globalThis.
9
+ const STORE_KEY = '__fontdueServerConfigStore__';
10
+ function clearStore() {
11
+ delete globalThis[STORE_KEY];
12
+ }
13
+ beforeEach(() => {
14
+ vi.resetModules();
15
+ clearStore();
16
+ });
17
+ afterEach(clearStore);
18
+ describe('serverConfig store (chunk-duplication safety, FD-712)', () => {
19
+ it('shares the ambient resolver across module instances', async () => {
20
+ var _await$a$resolveFontd, _await$a$resolveFontd2, _await$b$resolveFontd, _await$b$resolveFontd2;
21
+ // First "chunk": register a resolver (what fontdue-js/next does when
22
+ // <FontdueProvider> mounts in single-tenant mode).
23
+ const a = await import("../relay/serverConfig.js");
24
+ a.registerAmbientConfigResolver(async () => ({
25
+ url: 'https://acme.fontdue.com',
26
+ headers: {
27
+ authorization: 'Bearer admin-tok'
28
+ }
29
+ }));
30
+ expect((_await$a$resolveFontd = await a.resolveFontdueServerConfig()) === null || _await$a$resolveFontd === void 0 ? void 0 : (_await$a$resolveFontd2 = _await$a$resolveFontd.headers) === null || _await$a$resolveFontd2 === void 0 ? void 0 : _await$a$resolveFontd2.authorization).toBe('Bearer admin-tok');
31
+
32
+ // Second "chunk": a fresh module instance. Before the fix its module-level
33
+ // `ambientResolver` was undefined here, so an embed preloading from this
34
+ // chunk fetched without the preview token and a hidden-font reveal failed.
35
+ vi.resetModules();
36
+ const b = await import("../relay/serverConfig.js");
37
+ expect(b).not.toBe(a); // genuinely a re-evaluated module
38
+ expect((_await$b$resolveFontd = await b.resolveFontdueServerConfig()) === null || _await$b$resolveFontd === void 0 ? void 0 : (_await$b$resolveFontd2 = _await$b$resolveFontd.headers) === null || _await$b$resolveFontd2 === void 0 ? void 0 : _await$b$resolveFontd2.authorization).toBe('Bearer admin-tok');
39
+ });
40
+ it('reuses one shared store object across re-imports', async () => {
41
+ const a = await import("../relay/serverConfig.js");
42
+ // The store is created lazily on first use, not on import.
43
+ a.registerAmbientConfigResolver(() => undefined);
44
+ const first = globalThis[STORE_KEY];
45
+ expect(first).toBeDefined();
46
+ vi.resetModules();
47
+ const b = await import("../relay/serverConfig.js");
48
+ await b.resolveFontdueServerConfig();
49
+ const second = globalThis[STORE_KEY];
50
+
51
+ // Same store (slot factory + resolver), not a per-instance copy — so the
52
+ // React.cache-backed slot multi-tenant uses (setFontdueServerConfig via
53
+ // __prepareFontdueRender) is one shared cell every chunk reads/writes.
54
+ expect(second).toBe(first);
55
+ });
56
+ it('starts with no resolver (resolution unchanged until something registers)', async () => {
57
+ const {
58
+ resolveFontdueServerConfig
59
+ } = await import("../relay/serverConfig.js");
60
+ expect(await resolveFontdueServerConfig()).toBeUndefined();
61
+ });
62
+ });
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { BuyButtonIDQuery } from '../../__generated__/BuyButtonIDQuery.graphql.js';
3
3
  import { BuyButtonSlugQuery } from '../../__generated__/BuyButtonSlugQuery.graphql.js';
4
- import { SerializablePreloadedQuery } from '../../relay/loadSerializableQuery.js';
4
+ import { SerializablePreloadedQuery, type LoadQueryOptions } from '../../relay/loadSerializableQuery.js';
5
5
  import { Config } from '../ConfigContext.js';
6
6
  export interface BuyButton_props {
7
7
  collectionName?: string;
@@ -15,7 +15,7 @@ export type LoadBuyButtonQueryVariables = {
15
15
  collectionId?: never;
16
16
  collectionSlug: string;
17
17
  };
18
- export declare function loadBuyButtonQuery(variables: LoadBuyButtonQueryVariables): Promise<BuyButtonPreloadedQuery>;
18
+ export declare function loadBuyButtonQuery(variables: LoadBuyButtonQueryVariables, options?: LoadQueryOptions): Promise<BuyButtonPreloadedQuery>;
19
19
  export declare function BuyButtonPreloadedIDQueryRenderer({ preloadedQuery, ...rest }: BuyButton_props & {
20
20
  preloadedQuery: SerializablePreloadedQuery<BuyButtonIDQuery>;
21
21
  }): React.JSX.Element;
@@ -57,16 +57,16 @@ function BuyButtonComponent(_ref) {
57
57
  }
58
58
  const idQuery = (_BuyButtonIDQuery.hash && _BuyButtonIDQuery.hash !== "4fbf1dbf9e6c530a5d38c697b174d8b0" && console.error("The definition of 'BuyButtonIDQuery' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _BuyButtonIDQuery);
59
59
  const slugQuery = (_BuyButtonSlugQuery.hash && _BuyButtonSlugQuery.hash !== "6e750adea09698f7cb61f435cd88fd26" && console.error("The definition of 'BuyButtonSlugQuery' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _BuyButtonSlugQuery);
60
- export async function loadBuyButtonQuery(variables) {
60
+ export async function loadBuyButtonQuery(variables, options) {
61
61
  if (variables.collectionId) {
62
62
  return loadSerializableQuery(BuyButtonIDQueryNode, {
63
63
  collectionId: variables.collectionId
64
- });
64
+ }, options);
65
65
  }
66
66
  if (variables.collectionSlug) {
67
67
  return loadSerializableQuery(BuyButtonSlugQueryNode, {
68
68
  collectionSlug: variables.collectionSlug
69
- });
69
+ }, options);
70
70
  }
71
71
  throw new Error('loadBuyButtonQuery expected either a collectionId or collectionSlug');
72
72
  }
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { SerializablePreloadedQuery } from '../../relay/loadSerializableQuery.js';
2
+ import { SerializablePreloadedQuery, type LoadQueryOptions } from '../../relay/loadSerializableQuery.js';
3
3
  import { CharacterViewerIDQuery } from '../../__generated__/CharacterViewerIDQuery.graphql.js';
4
4
  import { CharacterViewerSlugQuery } from '../../__generated__/CharacterViewerSlugQuery.graphql.js';
5
5
  import { Config } from '../ConfigContext.js';
@@ -13,7 +13,7 @@ export type LoadCharacterViewerQueryVariables = {
13
13
  collectionId?: never;
14
14
  collectionSlug: string;
15
15
  };
16
- export declare function loadCharacterViewerQuery(variables: LoadCharacterViewerQueryVariables): Promise<CharacterViewerPreloadedQuery>;
16
+ export declare function loadCharacterViewerQuery(variables: LoadCharacterViewerQueryVariables, options?: LoadQueryOptions): Promise<CharacterViewerPreloadedQuery>;
17
17
  export declare function CharacterViewerPreloadedIDQueryRenderer({ preloadedQuery, ...rest }: CharacterViewer_props & {
18
18
  preloadedQuery: SerializablePreloadedQuery<CharacterViewerIDQuery>;
19
19
  }): React.JSX.Element | null;
@@ -6,7 +6,7 @@ import _CharacterViewerIDQuery from "../../__generated__/CharacterViewerIDQuery.
6
6
  import _CharacterViewer_style from "../../__generated__/CharacterViewer_style.graphql.js";
7
7
  import _CharacterViewer_collection from "../../__generated__/CharacterViewer_collection.graphql.js";
8
8
  import _CharacterViewer_family from "../../__generated__/CharacterViewer_family.graphql.js";
9
- import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
9
+ import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
10
10
  import { fetchQuery, graphql, useFragment, useLazyLoadQuery, usePreloadedQuery, useRefetchableFragment, useRelayEnvironment } from 'react-relay';
11
11
  import useResizeObserver from '../../hooks/useResizeObserver.js';
12
12
  import retryImport from '../../retryImport.js';
@@ -20,6 +20,8 @@ import StyleSelect from './StyleSelect.js';
20
20
  import Checkbox from '../Checkbox/index.js';
21
21
  import CharacterViewerStyleRefetchQueryNode from '../../__generated__/CharacterViewerStyleRefetchQuery.graphql.js';
22
22
  import { EnsureFontdueContext } from '../FontdueContextProvider/index.js';
23
+ import ConfigContext from '../ConfigContext.js';
24
+ import { unicodeNamesUrl } from '../../data/unicodeNamesUrl.js';
23
25
  function useSize() {
24
26
  const ref = useRef(null);
25
27
  const [size, setSize] = React.useState();
@@ -74,20 +76,27 @@ function GlyphMeta(_ref) {
74
76
  onChange: e => setFeaturesChecked(e.target.checked)
75
77
  }), glyph.features.join(', ')))));
76
78
  }
79
+
80
+ // Glyph unicode names (~1.2 MB) are fetched lazily from the Fontdue CDN rather
81
+ // than bundled — see data/unicodeNamesUrl.ts. Purely cosmetic (the “Unicode
82
+ // name” label), so a failed fetch degrades silently. `retryImport` is reused
83
+ // here as a generic retry wrapper for transient network blips.
77
84
  function useUnicodeData() {
85
+ const config = useContext(ConfigContext);
86
+ const url = unicodeNamesUrl(config.cdnUrl);
78
87
  const [data, setData] = useState();
79
88
  useEffect(() => {
80
- function fetchData() {
81
- retryImport(() => import('../../data/unicodeData.js')).then(data => {
82
- if (!ignore) setData(data.default);
83
- }).catch(() => {});
84
- }
85
89
  let ignore = false;
86
- fetchData();
90
+ retryImport(() => fetch(url).then(res => {
91
+ if (!res.ok) throw new Error(`unicode names request failed: ${res.status}`);
92
+ return res.json();
93
+ })).then(json => {
94
+ if (!ignore) setData(json);
95
+ }).catch(() => {});
87
96
  return () => {
88
97
  ignore = true;
89
98
  };
90
- }, []);
99
+ }, [url]);
91
100
  return data;
92
101
  }
93
102
  function compareGlyphs(a, b) {
@@ -416,16 +425,16 @@ function CharacterViewerComponent(_ref3) {
416
425
  }
417
426
  const idQuery = (_CharacterViewerIDQuery.hash && _CharacterViewerIDQuery.hash !== "f90b09a4df6d95307b0a5d5fda487cdc" && console.error("The definition of 'CharacterViewerIDQuery' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _CharacterViewerIDQuery);
418
427
  const slugQuery = (_CharacterViewerSlugQuery.hash && _CharacterViewerSlugQuery.hash !== "afa08a8f050e0434308892fea6e3c267" && console.error("The definition of 'CharacterViewerSlugQuery' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _CharacterViewerSlugQuery);
419
- export async function loadCharacterViewerQuery(variables) {
428
+ export async function loadCharacterViewerQuery(variables, options) {
420
429
  if (variables.collectionId) {
421
430
  return loadSerializableQuery(CharacterViewerIDQueryNode, {
422
431
  collectionId: variables.collectionId
423
- });
432
+ }, options);
424
433
  }
425
434
  if (variables.collectionSlug) {
426
435
  return loadSerializableQuery(CharacterViewerSlugQueryNode, {
427
436
  collectionSlug: variables.collectionSlug
428
- });
437
+ }, options);
429
438
  }
430
439
  throw new Error('loadCharacterViewerQuery expected either collectionId or collectionSlug');
431
440
  }
@@ -23,13 +23,20 @@ interface TrackingConfig {
23
23
  consentMessage?: string;
24
24
  segment?: SegmentConfig;
25
25
  }
26
+ interface PreviewConfig {
27
+ endpoint?: string;
28
+ revalidateEndpoint?: string;
29
+ clientSide?: boolean;
30
+ }
26
31
  export interface Config {
27
32
  form?: FormConfig;
28
33
  storeModal?: StoreModalConfig;
29
34
  typeTester?: TypeTesterConfig;
30
35
  stripe?: StripeConfig;
31
36
  tracking?: TrackingConfig;
37
+ preview?: PreviewConfig;
32
38
  corsErrorModal?: boolean;
39
+ cdnUrl?: string;
33
40
  }
34
41
  export declare const mergeConfig: (base?: Config, override?: Config) => Config;
35
42
  export declare const makeConfig: (config?: Config) => {
@@ -45,7 +52,7 @@ export declare const makeConfig: (config?: Config) => {
45
52
  selectable: boolean;
46
53
  priceBar: boolean;
47
54
  textInput: boolean;
48
- initialMode: "local" | "group";
55
+ initialMode: "group" | "local";
49
56
  groupEdit: boolean;
50
57
  bulletStyle: "round" | "square";
51
58
  openTypeFeatures: {
@@ -102,7 +109,13 @@ export declare const makeConfig: (config?: Config) => {
102
109
  consentMessage: string | undefined;
103
110
  segment: SegmentConfig | undefined;
104
111
  };
112
+ preview: {
113
+ endpoint: string;
114
+ revalidateEndpoint: string | undefined;
115
+ clientSide: boolean;
116
+ };
105
117
  corsErrorModal: boolean;
118
+ cdnUrl: string;
106
119
  };
107
120
  declare const _default: React.Context<{
108
121
  typeTester: {
@@ -117,7 +130,7 @@ declare const _default: React.Context<{
117
130
  selectable: boolean;
118
131
  priceBar: boolean;
119
132
  textInput: boolean;
120
- initialMode: "local" | "group";
133
+ initialMode: "group" | "local";
121
134
  groupEdit: boolean;
122
135
  bulletStyle: "round" | "square";
123
136
  openTypeFeatures: {
@@ -174,6 +187,12 @@ declare const _default: React.Context<{
174
187
  consentMessage: string | undefined;
175
188
  segment: SegmentConfig | undefined;
176
189
  };
190
+ preview: {
191
+ endpoint: string;
192
+ revalidateEndpoint: string | undefined;
193
+ clientSide: boolean;
194
+ };
177
195
  corsErrorModal: boolean;
196
+ cdnUrl: string;
178
197
  }>;
179
198
  export default _default;
@@ -1,6 +1,8 @@
1
1
  'use client';
2
2
 
3
3
  import React from 'react';
4
+ import { PREVIEW_ENDPOINT } from '../preview/constants.js';
5
+ import { FONTDUE_CDN_URL } from '../data/unicodeNamesUrl.js';
4
6
  const makeTypeTesterConfig = config => {
5
7
  var _config$openTypeFeatu, _config$openTypeFeatu2, _config$openTypeFeatu3, _config$openTypeFeatu4, _config$size, _config$size2, _config$size3, _config$lineHeight, _config$lineHeight2, _config$lineHeight3, _config$letterSpacing, _config$letterSpacing2, _config$letterSpacing3, _config$columns, _config$columns2, _config$columns3;
6
8
  let shy = (config === null || config === void 0 ? void 0 : config.shy) ?? false;
@@ -79,7 +81,7 @@ export const mergeConfig = (base, override) => {
79
81
  return deepMerge(base, override);
80
82
  };
81
83
  export const makeConfig = config => {
82
- var _config$form, _config$storeModal, _config$storeModal2, _config$storeModal3, _config$storeModal4, _config$stripe, _config$tracking, _config$tracking2, _config$tracking3, _config$tracking4;
84
+ var _config$form, _config$storeModal, _config$storeModal2, _config$storeModal3, _config$storeModal4, _config$stripe, _config$tracking, _config$tracking2, _config$tracking3, _config$tracking4, _config$preview, _config$preview2, _config$preview3;
83
85
  return {
84
86
  typeTester: makeTypeTesterConfig(config === null || config === void 0 ? void 0 : config.typeTester),
85
87
  form: {
@@ -101,7 +103,15 @@ export const makeConfig = config => {
101
103
  consentMessage: config === null || config === void 0 ? void 0 : (_config$tracking3 = config.tracking) === null || _config$tracking3 === void 0 ? void 0 : _config$tracking3.consentMessage,
102
104
  segment: config === null || config === void 0 ? void 0 : (_config$tracking4 = config.tracking) === null || _config$tracking4 === void 0 ? void 0 : _config$tracking4.segment
103
105
  },
104
- corsErrorModal: (config === null || config === void 0 ? void 0 : config.corsErrorModal) ?? true
106
+ preview: {
107
+ endpoint: (config === null || config === void 0 ? void 0 : (_config$preview = config.preview) === null || _config$preview === void 0 ? void 0 : _config$preview.endpoint) ?? PREVIEW_ENDPOINT,
108
+ // No default: an unset revalidate endpoint hides the toolbar's refresh
109
+ // button (frameworks without a client-callable purge leave it unset).
110
+ revalidateEndpoint: config === null || config === void 0 ? void 0 : (_config$preview2 = config.preview) === null || _config$preview2 === void 0 ? void 0 : _config$preview2.revalidateEndpoint,
111
+ clientSide: (config === null || config === void 0 ? void 0 : (_config$preview3 = config.preview) === null || _config$preview3 === void 0 ? void 0 : _config$preview3.clientSide) ?? false
112
+ },
113
+ corsErrorModal: (config === null || config === void 0 ? void 0 : config.corsErrorModal) ?? true,
114
+ cdnUrl: (config === null || config === void 0 ? void 0 : config.cdnUrl) ?? FONTDUE_CDN_URL
105
115
  };
106
116
  };
107
117
  export default /*#__PURE__*/React.createContext(makeConfig());
@@ -0,0 +1 @@
1
+ export declare function renderConnectionErrorToolbar(origin: string, fontdueUrl: string): void;
@@ -0,0 +1,106 @@
1
+ import React, { useState } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+
4
+ // Connection-error status, mounted in its own React root (like the modal it
5
+ // replaces) rather than inside the admin toolbar's component tree.
6
+ //
7
+ // A CORS/connection outage commonly makes the consumer's own Relay-driven
8
+ // components throw, and their error boundary can replace the whole route
9
+ // wholesale — which would take an in-tree status down with it (verified in a
10
+ // single-tree framework: the page became an "Oops!" error page and the toolbar
11
+ // vanished). An independent root keeps this status visible regardless, while
12
+ // reusing the admin toolbar's restrained styling so it reads as the same small
13
+ // platform chrome — not the old full-screen modal.
14
+ //
15
+ // It deliberately takes no contexts (Relay, config, url): a blocked connection
16
+ // can't confirm an admin anyway, so this shows to whoever is on the page, with
17
+ // just the origin + Fontdue URL the detector hands it.
18
+
19
+ // Deep link to where an admin adds allowed origins, derived from the blocked
20
+ // request's own URL. Null if it can't be parsed, so the link is simply omitted.
21
+ function settingsUrl(fontdueUrl) {
22
+ try {
23
+ return `${new URL(fontdueUrl).origin}/admin/settings/integration`;
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+ function ConnectionErrorToolbar(_ref) {
29
+ let {
30
+ origin,
31
+ fontdueUrl
32
+ } = _ref;
33
+ const [open, setOpen] = useState(false);
34
+ const fixUrl = settingsUrl(fontdueUrl);
35
+ return /*#__PURE__*/React.createElement("div", {
36
+ className: "fontdue-admin-toolbar",
37
+ "data-connection-error": "true"
38
+ }, open && /*#__PURE__*/React.createElement("div", {
39
+ className: "fontdue-admin-toolbar__panel"
40
+ }, /*#__PURE__*/React.createElement("div", {
41
+ className: "fontdue-admin-toolbar__header"
42
+ }, /*#__PURE__*/React.createElement("span", {
43
+ className: "fontdue-admin-toolbar__title"
44
+ }, "Fontdue")), /*#__PURE__*/React.createElement("div", {
45
+ className: "fontdue-admin-toolbar__error-status",
46
+ role: "alert"
47
+ }, /*#__PURE__*/React.createElement("p", {
48
+ className: "fontdue-admin-toolbar__error-status-text"
49
+ }, /*#__PURE__*/React.createElement(WarningIcon, null), "Fontdue couldn\u2019t be reached from this site."), /*#__PURE__*/React.createElement("p", {
50
+ className: "fontdue-admin-toolbar__hint"
51
+ }, "This is most likely a cross-origin (CORS) setting \u2014 ", origin, " isn\u2019t on Fontdue\u2019s allowed list. Add it under Settings \u2192 Integration and this page will reload automatically."), /*#__PURE__*/React.createElement("p", {
52
+ className: "fontdue-admin-toolbar__hint"
53
+ }, "If it\u2019s already listed, the connection may be failing for another reason \u2014 a network problem or a temporary Fontdue outage."), fixUrl && /*#__PURE__*/React.createElement("a", {
54
+ className: "fontdue-admin-toolbar__action",
55
+ href: fixUrl,
56
+ target: "_blank",
57
+ rel: "noreferrer"
58
+ }, "Open integration settings \u2197")), /*#__PURE__*/React.createElement("p", {
59
+ className: "fontdue-admin-toolbar__meta"
60
+ }, "Shown because Fontdue couldn\u2019t load on this page.")), /*#__PURE__*/React.createElement("button", {
61
+ type: "button",
62
+ className: "fontdue-admin-toolbar__button",
63
+ onClick: () => setOpen(v => !v),
64
+ "aria-expanded": open
65
+ }, /*#__PURE__*/React.createElement(WarningIcon, null), "Fontdue \xB7 connection issue"));
66
+ }
67
+
68
+ // Small inline warning glyph (a triangle with an exclamation), matching the
69
+ // admin toolbar's. Inherits the surrounding text color via currentColor.
70
+ function WarningIcon() {
71
+ return /*#__PURE__*/React.createElement("svg", {
72
+ className: "fontdue-admin-toolbar__warning-icon",
73
+ width: "13",
74
+ height: "13",
75
+ viewBox: "0 0 16 16",
76
+ fill: "none",
77
+ stroke: "currentColor",
78
+ strokeWidth: "1.4",
79
+ "aria-hidden": "true",
80
+ focusable: "false"
81
+ }, /*#__PURE__*/React.createElement("path", {
82
+ d: "M8 1.5 15 14H1L8 1.5Z",
83
+ strokeLinejoin: "miter"
84
+ }), /*#__PURE__*/React.createElement("path", {
85
+ d: "M8 6v3.5",
86
+ strokeLinecap: "square"
87
+ }), /*#__PURE__*/React.createElement("path", {
88
+ d: "M8 11.5v.5",
89
+ strokeLinecap: "square"
90
+ }));
91
+ }
92
+ function mount(origin, fontdueUrl) {
93
+ const container = document.createElement('div');
94
+ document.body.appendChild(container);
95
+ createRoot(container).render( /*#__PURE__*/React.createElement(ConnectionErrorToolbar, {
96
+ origin: origin,
97
+ fontdueUrl: fontdueUrl
98
+ }));
99
+ }
100
+ export function renderConnectionErrorToolbar(origin, fontdueUrl) {
101
+ if (document.body) {
102
+ mount(origin, fontdueUrl);
103
+ } else {
104
+ document.addEventListener('DOMContentLoaded', () => mount(origin, fontdueUrl));
105
+ }
106
+ }
@@ -0,0 +1,2 @@
1
+ import React from 'react';
2
+ export default function FontdueAdminToolbar(): React.JSX.Element | null;