fontdue-js 3.0.0-alpha9 → 3.0.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 (135) hide show
  1. package/CHANGELOG.md +14 -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 +4 -2
  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,299 @@
1
+ 'use client';
2
+
3
+ import _FontdueAdminToolbarTokenMutation from "../../__generated__/FontdueAdminToolbarTokenMutation.graphql.js";
4
+ import _FontdueAdminToolbarQuery from "../../__generated__/FontdueAdminToolbarQuery.graphql.js";
5
+ import React, { useContext, useEffect, useState } from 'react';
6
+ import { commitMutation, fetchQuery, graphql, useRelayEnvironment } from 'react-relay';
7
+ import ConfigContext from '../ConfigContext.js';
8
+ import { useFontdueUrl } from '../UrlContext.js';
9
+ import { PREVIEW_ENDPOINT, DEFAULT_PREVIEW_TTL_MS, setPreviewMarkerCookie } from '../../preview/constants.js';
10
+ import { readPreviewState, expiresAtFromTokenResult, isSessionExpiredError } from './previewState.js';
11
+ import { fontdueBaseUrl, version } from '../../relay/environment.js';
12
+ // Admin-only affordance for logged-in foundry admins: reveal unpublished
13
+ // ("hidden") fonts across the whole storefront. Storefront pages are
14
+ // server-rendered sessionless and cached, and the provider's own query is
15
+ // preloaded server-side without the session — so `adminUser` can't ride it.
16
+ // Admin presence is detected here, client-side, where the request carries
17
+ // the session; the public never sees this control.
18
+ //
19
+ // The reveal itself is not done here: entering preview brokers a short-lived
20
+ // admin token and hands it to the framework's preview route, which switches
21
+ // the whole app to a live, token-bearing render. Exiting clears it. Public
22
+ // renders never touch any of this and stay static and cached.
23
+ const adminQuery = (_FontdueAdminToolbarQuery.hash && _FontdueAdminToolbarQuery.hash !== "8660b45f086137d249eee82459ad648d" && console.error("The definition of 'FontdueAdminToolbarQuery' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _FontdueAdminToolbarQuery);
24
+
25
+ // createAdminToken mints a short-lived, stateless admin token from the admin
26
+ // session; it's handed to the preview route to enter draft mode.
27
+ const tokenMutation = (_FontdueAdminToolbarTokenMutation.hash && _FontdueAdminToolbarTokenMutation.hash !== "2b82f195747c86d50fd5884d76f3b709" && console.error("The definition of 'FontdueAdminToolbarTokenMutation' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _FontdueAdminToolbarTokenMutation);
28
+ export default function FontdueAdminToolbar() {
29
+ const environment = useRelayEnvironment();
30
+ const previewConfig = useContext(ConfigContext).preview;
31
+ // The preview entry/exit route is part of the portable preview contract (see
32
+ // fontdue-js/preview); it defaults to '/api/preview' but is configurable so
33
+ // non-Next apps can mount it where their router expects. The revalidate route
34
+ // is opt-in (no default) — see config.preview.revalidateEndpoint.
35
+ const previewEndpoint = (previewConfig === null || previewConfig === void 0 ? void 0 : previewConfig.endpoint) ?? PREVIEW_ENDPOINT;
36
+ const revalidateEndpoint = previewConfig === null || previewConfig === void 0 ? void 0 : previewConfig.revalidateEndpoint;
37
+ // Client-only embeds (the script-tag CDN bundle, a client SPA) have no server
38
+ // preview route to broker a token through, so they enter/exit preview by
39
+ // toggling the readable marker cookie directly — see setPreviewMarkerCookie.
40
+ const clientSidePreview = (previewConfig === null || previewConfig === void 0 ? void 0 : previewConfig.clientSide) ?? false;
41
+
42
+ // Where the admin is signed in: the configured Fontdue origin. Used for the
43
+ // "signed in at" line and the deep link back to the admin. Falls back to the
44
+ // URL the embed was initialized with (the script-tag bundle sets no
45
+ // FONTDUE_URL env); undefined (e.g. multi-tenant) drops both gracefully.
46
+ const contextUrl = useFontdueUrl();
47
+ const fontdueUrl = fontdueBaseUrl() ?? (contextUrl || undefined);
48
+ const fontdueHost = fontdueUrl ? safeHost(fontdueUrl) : null;
49
+ const adminUrl = fontdueUrl ? `${fontdueUrl.replace(/\/+$/, '')}/admin` : null;
50
+ const [adminName, setAdminName] = useState(null);
51
+ const [ready, setReady] = useState(false);
52
+ const [open, setOpen] = useState(false);
53
+ const [previewState, setPreviewState] = useState('off');
54
+ const [busy, setBusy] = useState(false);
55
+ const [error, setError] = useState(null);
56
+ const [notice, setNotice] = useState(null);
57
+ const previewing = previewState === 'active';
58
+ const expired = previewState === 'expired';
59
+ useEffect(() => {
60
+ setPreviewState(readPreviewState());
61
+ // network-only: the provider's preloaded query already wrote a sessionless
62
+ // `viewer` to the store, so we must go to the network (with the session)
63
+ // rather than read that cached, admin-blind copy.
64
+ const subscription = fetchQuery(environment, adminQuery, {}, {
65
+ fetchPolicy: 'network-only'
66
+ }).subscribe({
67
+ next: data => {
68
+ var _data$viewer;
69
+ const admin = (_data$viewer = data.viewer) === null || _data$viewer === void 0 ? void 0 : _data$viewer.adminUser;
70
+ if (admin) setAdminName(admin.name ?? 'admin');
71
+ },
72
+ error: () => setReady(true),
73
+ complete: () => setReady(true)
74
+ });
75
+ return () => subscription.unsubscribe();
76
+ }, [environment]);
77
+
78
+ // Re-derive the state when the tab regains focus or becomes visible. A tab
79
+ // left open past the token's expiry won't otherwise notice — there's no
80
+ // event when a cookie's encoded expiry passes — so the warning would never
81
+ // appear until the next reload. Recovery stays user-initiated; we only
82
+ // refresh what state is shown.
83
+ useEffect(() => {
84
+ if (typeof window === 'undefined') return;
85
+ const recheck = () => setPreviewState(readPreviewState());
86
+ window.addEventListener('focus', recheck);
87
+ document.addEventListener('visibilitychange', recheck);
88
+ return () => {
89
+ window.removeEventListener('focus', recheck);
90
+ document.removeEventListener('visibilitychange', recheck);
91
+ };
92
+ }, []);
93
+
94
+ // Hidden until we've confirmed an admin session. The public always lands here.
95
+ if (!ready || !adminName) return null;
96
+ async function startPreview(token, expiresAt) {
97
+ const res = await fetch(previewEndpoint, {
98
+ method: 'POST',
99
+ headers: {
100
+ 'content-type': 'application/json'
101
+ },
102
+ body: JSON.stringify({
103
+ token,
104
+ expiresAt
105
+ })
106
+ });
107
+ if (res.ok) {
108
+ window.location.reload();
109
+ } else {
110
+ setBusy(false);
111
+ setError('Could not start preview.');
112
+ }
113
+ }
114
+ function enterPreview() {
115
+ setBusy(true);
116
+ setError(null);
117
+ setNotice(null);
118
+ // Client-only: no server render needs a token, so just mark preview on and
119
+ // reload — the admin's session cookie rides the next fetches and reveals
120
+ // hidden fonts now that the marker makes the client send `fontdue-preview`.
121
+ // There's no token to mint, so the marker just gets the default 1h expiry.
122
+ if (clientSidePreview) {
123
+ setPreviewMarkerCookie(Date.now() + DEFAULT_PREVIEW_TTL_MS);
124
+ window.location.reload();
125
+ return;
126
+ }
127
+ commitMutation(environment, {
128
+ mutation: tokenMutation,
129
+ variables: {},
130
+ onCompleted: (res, errors) => {
131
+ var _res$createAdminToken;
132
+ const token = (_res$createAdminToken = res.createAdminToken) === null || _res$createAdminToken === void 0 ? void 0 : _res$createAdminToken.token;
133
+ if (errors && errors.length > 0 || !token) {
134
+ setBusy(false);
135
+ // createAdminToken is gated on the admin session (current_user); a
136
+ // "Not authorized" error means the session itself has lapsed, which
137
+ // needs a fresh sign-in, not a retry. Surface that distinctly and
138
+ // clear the now-meaningless marker so the toolbar resets.
139
+ if (isSessionExpiredError(errors)) {
140
+ setPreviewMarkerCookie(false);
141
+ setPreviewState('off');
142
+ setError('Your session expired — sign in again.');
143
+ } else {
144
+ setError('Could not start preview.');
145
+ }
146
+ return;
147
+ }
148
+ startPreview(token, expiresAtFromTokenResult(res.createAdminToken));
149
+ },
150
+ onError: () => {
151
+ setBusy(false);
152
+ setError('Could not start preview.');
153
+ }
154
+ });
155
+ }
156
+ async function exitPreview() {
157
+ setBusy(true);
158
+ setError(null);
159
+ setNotice(null);
160
+ if (clientSidePreview) {
161
+ setPreviewMarkerCookie(false);
162
+ } else {
163
+ await fetch(previewEndpoint, {
164
+ method: 'DELETE'
165
+ });
166
+ }
167
+ window.location.reload();
168
+ }
169
+
170
+ // Recover from the expired state: mint a fresh token and re-enter. This is the
171
+ // same enter flow — a successful POST overwrites the stale token + marker
172
+ // cookies and reloads, so there's nothing to clear first. If the session has
173
+ // also lapsed, enterPreview surfaces the distinct "sign in again" message.
174
+ function reEnterPreview() {
175
+ enterPreview();
176
+ }
177
+
178
+ // Purge the cached storefront so public visitors see freshly published (or
179
+ // newly hidden) content. Targets the configured revalidate route as-is.
180
+ async function refreshCache() {
181
+ if (!revalidateEndpoint) return;
182
+ setBusy(true);
183
+ setError(null);
184
+ setNotice(null);
185
+ try {
186
+ const res = await fetch(revalidateEndpoint, {
187
+ method: 'POST'
188
+ });
189
+ if (!res.ok) throw new Error(`status ${res.status}`);
190
+ setNotice('Public cache refreshed.');
191
+ } catch {
192
+ setError('Could not refresh the cache.');
193
+ } finally {
194
+ setBusy(false);
195
+ }
196
+ }
197
+ return /*#__PURE__*/React.createElement("div", {
198
+ className: "fontdue-admin-toolbar",
199
+ "data-previewing": previewing,
200
+ "data-preview-state": previewState,
201
+ "data-testid": "fontdue-admin-toolbar"
202
+ }, open && /*#__PURE__*/React.createElement("div", {
203
+ className: "fontdue-admin-toolbar__panel"
204
+ }, /*#__PURE__*/React.createElement("div", {
205
+ className: "fontdue-admin-toolbar__header"
206
+ }, /*#__PURE__*/React.createElement("span", {
207
+ className: "fontdue-admin-toolbar__title"
208
+ }, "Fontdue"), /*#__PURE__*/React.createElement("span", {
209
+ className: "fontdue-admin-toolbar__user"
210
+ }, adminName)), expired ? /*#__PURE__*/React.createElement("div", {
211
+ className: "fontdue-admin-toolbar__expired",
212
+ role: "status",
213
+ "data-testid": "preview-expired"
214
+ }, /*#__PURE__*/React.createElement("p", {
215
+ className: "fontdue-admin-toolbar__expired-text"
216
+ }, /*#__PURE__*/React.createElement(WarningIcon, null), "Preview expired. Hidden fonts are no longer shown."), /*#__PURE__*/React.createElement("button", {
217
+ type: "button",
218
+ className: "fontdue-admin-toolbar__action",
219
+ onClick: reEnterPreview,
220
+ disabled: busy,
221
+ "data-testid": "reenter-preview-button"
222
+ }, "Re-enter preview")) : /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("label", {
223
+ className: "fontdue-admin-toolbar__toggle"
224
+ }, /*#__PURE__*/React.createElement("input", {
225
+ type: "checkbox",
226
+ checked: previewing,
227
+ disabled: busy,
228
+ onChange: e => e.target.checked ? enterPreview() : exitPreview(),
229
+ "data-testid": "preview-toggle"
230
+ }), /*#__PURE__*/React.createElement("span", null, "Preview hidden fonts")), /*#__PURE__*/React.createElement("p", {
231
+ className: "fontdue-admin-toolbar__hint"
232
+ }, previewing ? 'Unpublished fonts are visible across the site — only to you.' : 'Reveal unpublished fonts everywhere, only for you.')), (revalidateEndpoint || adminUrl) && /*#__PURE__*/React.createElement("div", {
233
+ className: "fontdue-admin-toolbar__actions"
234
+ }, revalidateEndpoint && /*#__PURE__*/React.createElement("button", {
235
+ type: "button",
236
+ className: "fontdue-admin-toolbar__action",
237
+ onClick: refreshCache,
238
+ disabled: busy,
239
+ "data-testid": "revalidate-button"
240
+ }, "Refresh public cache"), adminUrl && /*#__PURE__*/React.createElement("a", {
241
+ className: "fontdue-admin-toolbar__action",
242
+ href: adminUrl,
243
+ target: "_blank",
244
+ rel: "noreferrer",
245
+ "data-testid": "open-admin-link"
246
+ }, "Open Fontdue admin \u2197")), notice && /*#__PURE__*/React.createElement("p", {
247
+ className: "fontdue-admin-toolbar__notice"
248
+ }, notice), error && /*#__PURE__*/React.createElement("p", {
249
+ className: "fontdue-admin-toolbar__error"
250
+ }, error), /*#__PURE__*/React.createElement("p", {
251
+ className: "fontdue-admin-toolbar__meta"
252
+ }, fontdueHost ? `Shown because you’re signed in to Fontdue at ${fontdueHost}.` : 'Shown because you’re signed in to Fontdue.', /*#__PURE__*/React.createElement("span", {
253
+ className: "fontdue-admin-toolbar__version"
254
+ }, "fontdue-js ", version))), /*#__PURE__*/React.createElement("button", {
255
+ type: "button",
256
+ className: "fontdue-admin-toolbar__button",
257
+ onClick: () => setOpen(v => !v),
258
+ "aria-expanded": open,
259
+ "data-testid": "fontdue-admin-toolbar-button"
260
+ }, expired ? /*#__PURE__*/React.createElement(WarningIcon, null) : /*#__PURE__*/React.createElement("span", {
261
+ className: "fontdue-admin-toolbar__dot",
262
+ "aria-hidden": "true"
263
+ }), "Fontdue", previewing ? ' · previewing' : '', expired ? ' · preview expired' : ''));
264
+ }
265
+
266
+ // Small inline warning glyph (a triangle with an exclamation). Inherits the
267
+ // surrounding text color so it stays monochrome with the toolbar chrome.
268
+ function WarningIcon() {
269
+ return /*#__PURE__*/React.createElement("svg", {
270
+ className: "fontdue-admin-toolbar__warning-icon",
271
+ width: "13",
272
+ height: "13",
273
+ viewBox: "0 0 16 16",
274
+ fill: "none",
275
+ stroke: "currentColor",
276
+ strokeWidth: "1.4",
277
+ "aria-hidden": "true",
278
+ focusable: "false"
279
+ }, /*#__PURE__*/React.createElement("path", {
280
+ d: "M8 1.5 15 14H1L8 1.5Z",
281
+ strokeLinejoin: "miter"
282
+ }), /*#__PURE__*/React.createElement("path", {
283
+ d: "M8 6v3.5",
284
+ strokeLinecap: "square"
285
+ }), /*#__PURE__*/React.createElement("path", {
286
+ d: "M8 11.5v.5",
287
+ strokeLinecap: "square"
288
+ }));
289
+ }
290
+
291
+ // Hostname of a base URL, or the raw value if it can't be parsed (so a
292
+ // misconfigured URL still shows something rather than throwing).
293
+ function safeHost(url) {
294
+ try {
295
+ return new URL(url).host;
296
+ } catch {
297
+ return url;
298
+ }
299
+ }
@@ -0,0 +1,7 @@
1
+ export type PreviewState = 'off' | 'active' | 'expired';
2
+ export declare function derivePreviewState(hasMarker: boolean, expiresAt: number | undefined, now?: number): PreviewState;
3
+ export declare function readPreviewState(): PreviewState;
4
+ export declare function expiresAtFromTokenResult(result: unknown): number;
5
+ export declare function isSessionExpiredError(errors: ReadonlyArray<{
6
+ message?: string | null;
7
+ }> | null | undefined): boolean;
@@ -0,0 +1,58 @@
1
+ // Pure preview-state logic for the admin toolbar, split out from the component
2
+ // so it can be unit-tested without a browser, a Relay environment, or the
3
+ // `graphql` Babel transform (importing the .tsx triggers the tag at load).
4
+ //
5
+ // The preview token is a stateless, time-only-expiring Phoenix.Token. There is
6
+ // no way to learn its validity client-side (it's httpOnly, and the toolbar's
7
+ // admin check rides the longer-lived *session* cookie, not the token), so the
8
+ // toolbar tracks the token's *expiry* instead and derives three states from the
9
+ // readable marker cookie:
10
+ //
11
+ // - 'off' — no marker cookie: not previewing.
12
+ // - 'active' — marker present and now < expiresAt: previewing.
13
+ // - 'expired' — marker present and now >= expiresAt: the token has lapsed,
14
+ // so server renders have silently fallen back to the public
15
+ // catalog. Show a warning and offer "re-enter".
16
+
17
+ import { DEFAULT_PREVIEW_TTL_MS, hasPreviewMarkerCookie, getPreviewExpiry } from '../../preview/constants.js';
18
+ // Pure derivation. `hasMarker` and `expiresAt` come from the marker cookie;
19
+ // `now` is injectable for tests.
20
+ export function derivePreviewState(hasMarker, expiresAt) {
21
+ let now = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : Date.now();
22
+ if (!hasMarker) return 'off';
23
+ // A marker with no/garbled expiry (e.g. a stale legacy `=1` cookie) is
24
+ // treated as expired: safer to prompt a re-enter than to imply it's live.
25
+ if (expiresAt === undefined || now >= expiresAt) return 'expired';
26
+ return 'active';
27
+ }
28
+
29
+ // Read the current preview state straight off the document's cookies.
30
+ export function readPreviewState() {
31
+ return derivePreviewState(hasPreviewMarkerCookie(), getPreviewExpiry());
32
+ }
33
+
34
+ // Pull an expiry out of the createAdminToken response. The backend will grow an
35
+ // `expiresAt` field (ISO-8601 or epoch-ms); until it does, the field is absent
36
+ // and we fall back to now + DEFAULT_PREVIEW_TTL_MS — the canonical 1h TTL lives
37
+ // in the backend `token.ex` `:admin_token` `max_age`. Read defensively so the
38
+ // client and backend halves can ship independently.
39
+ export function expiresAtFromTokenResult(result) {
40
+ const value = result === null || result === void 0 ? void 0 : result.expiresAt;
41
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
42
+ if (typeof value === 'string' && value !== '') {
43
+ const asNumber = Number(value);
44
+ if (Number.isFinite(asNumber)) return asNumber;
45
+ const asDate = Date.parse(value);
46
+ if (!Number.isNaN(asDate)) return asDate;
47
+ }
48
+ return Date.now() + DEFAULT_PREVIEW_TTL_MS;
49
+ }
50
+
51
+ // True when the createAdminToken mutation failed because the admin *session*
52
+ // (not the preview token) has expired. The resolver returns a "Not authorized"
53
+ // GraphQL error when current_user is nil, so we match that message rather than
54
+ // treating every failure as a recoverable retry.
55
+ export function isSessionExpiredError(errors) {
56
+ if (!errors) return false;
57
+ return errors.some(e => /not authoriz/i.test(e.message ?? ''));
58
+ }
@@ -8,7 +8,7 @@ import ConfigContext, { makeConfig, mergeConfig } from '../ConfigContext.js';
8
8
  import { createDefaultStore } from '../../reducer.js';
9
9
  import ComponentsContext from '../ComponentsContext.js';
10
10
  import UrlContext from '../UrlContext.js';
11
- import { setCorsModalEnabled } from '../../corsError.js';
11
+ import { setConnectionErrorUiEnabled } from '../../corsError.js';
12
12
  import TypeTesterFamiliesProvider from '../TypeTester/TypeTesterFamilies.js';
13
13
 
14
14
  // Marker context used by self-wrapping components to detect a parent
@@ -126,7 +126,9 @@ export default function FontdueContextProvider(_ref3) {
126
126
  stripeIntegration
127
127
  });
128
128
  const configValue = makeConfig(config);
129
- setCorsModalEnabled(configValue.corsErrorModal);
129
+ // `corsErrorModal` is the legacy config name; it now gates the toolbar's
130
+ // connection-error status rather than a modal.
131
+ setConnectionErrorUiEnabled(configValue.corsErrorModal);
130
132
  const sharedStore = useSharedStore(store, config);
131
133
  return /*#__PURE__*/React.createElement(FontdueContextMarker.Provider, {
132
134
  value: true
@@ -13,6 +13,11 @@ import retryImport from '../../retryImport.js';
13
13
  import useSerializablePreloadedQuery from '../../relay/useSerializablePreloadedQuery.js';
14
14
  const ConsentBanner = /*#__PURE__*/lazy(() => retryImport(() => import('../ConsentBanner/index.js')));
15
15
  const Tracking = /*#__PURE__*/lazy(() => retryImport(() => import('../Tracking/index.js')));
16
+ // Admin-only; self-gates to an admin session and renders nothing otherwise, so
17
+ // it's safe to mount for everyone. It detects admins client-side (storefront
18
+ // renders are sessionless), so it needs no fragment on the provider query.
19
+ // Lazy so it stays out of the provider's main chunk.
20
+ const FontdueAdminToolbar = /*#__PURE__*/lazy(() => retryImport(() => import('../FontdueAdminToolbar/index.js')));
16
21
 
17
22
  // Single layout-level query that spreads every aux UI component's fragment.
18
23
  // Components declare their own data needs via fragments — adding a new aux
@@ -72,6 +77,6 @@ function AuxUI(_ref4) {
72
77
  codeConfig: codeConfig
73
78
  }, /*#__PURE__*/React.createElement(Suspense, {
74
79
  fallback: null
75
- }, /*#__PURE__*/React.createElement(ConsentBanner, null), /*#__PURE__*/React.createElement(Tracking, null))));
80
+ }, /*#__PURE__*/React.createElement(ConsentBanner, null), /*#__PURE__*/React.createElement(Tracking, null), /*#__PURE__*/React.createElement(FontdueAdminToolbar, null))));
76
81
  }
77
82
  export default FontdueProvider;
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import type { FontdueProvider_props } from './index.js';
3
+ import '../../next/registerSingleTenantResolver.js';
3
4
  export type { FontdueProvider_props } from './index.js';
4
5
  export type { FontdueProviderPreloadedQuery } from '../../loadFontdueProviderQuery.js';
5
6
  export type { FontdueServerConfig } from '../../relay/serverConfig.js';
@@ -3,6 +3,16 @@ import React from 'react';
3
3
  import FontdueProvider from './index.js';
4
4
  import loadFontdueProviderQueryImpl from '../../loadFontdueProviderQuery.js';
5
5
  import { setFontdueServerConfig } from '../../relay/serverConfig.js';
6
+ // Side-effect import: registers the single-tenant ambient config resolver so a
7
+ // single-tenant foundry that mounts <FontdueProvider> never has to call a
8
+ // per-render setup function. The module statically pulls in only browser-safe
9
+ // code and lazy-loads the Next request APIs (next/headers, next/navigation)
10
+ // behind a dynamic import that runs only when the resolver resolves config —
11
+ // keeping next/* off this react-server entrypoint's eager graph. The resolver
12
+ // no-ops in multi-tenant mode (which drives config through the slot) and is
13
+ // inert in the default build Astro/RR7 use. See
14
+ // ../../next/registerSingleTenantResolver.ts.
15
+ import '../../next/registerSingleTenantResolver.js';
6
16
  // Stub for the RSC export condition. The default `<FontdueProvider>` server
7
17
  // entrypoint (this file) awaits the query for consumers, so RSC users should
8
18
  // never call it manually. Re-exporting a throwing stub makes the mistake
@@ -1,9 +1,9 @@
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 { NewsletterSignupQuery } from '../../__generated__/NewsletterSignupQuery.graphql.js';
4
4
  import { Config } from '../ConfigContext.js';
5
5
  export type NewsletterSignupPreloadedQuery = SerializablePreloadedQuery<NewsletterSignupQuery>;
6
- export declare function loadNewsletterSignupQuery(): Promise<NewsletterSignupPreloadedQuery>;
6
+ export declare function loadNewsletterSignupQuery(options?: LoadQueryOptions): Promise<NewsletterSignupPreloadedQuery>;
7
7
  export interface NewsletterSignup_props {
8
8
  optInLabel?: string;
9
9
  successLabel?: string;
@@ -13,8 +13,8 @@ import useSerializablePreloadedQuery from '../../relay/useSerializablePreloadedQ
13
13
  import NewsletterSignupQueryNode from '../../__generated__/NewsletterSignupQuery.graphql.js';
14
14
  import { EnsureFontdueContext } from '../FontdueContextProvider/index.js';
15
15
  const updateCustomerMutation = (_NewsletterSignupUpdateCustomerMutation.hash && _NewsletterSignupUpdateCustomerMutation.hash !== "769087891b6f263122bbb630b3f2ca6c" && console.error("The definition of 'NewsletterSignupUpdateCustomerMutation' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _NewsletterSignupUpdateCustomerMutation);
16
- export async function loadNewsletterSignupQuery() {
17
- return loadSerializableQuery(NewsletterSignupQueryNode, {});
16
+ export async function loadNewsletterSignupQuery(options) {
17
+ return loadSerializableQuery(NewsletterSignupQueryNode, {}, options);
18
18
  }
19
19
  const query = (_NewsletterSignupQuery.hash && _NewsletterSignupQuery.hash !== "24b303198a6038318723fc0124548862" && console.error("The definition of 'NewsletterSignupQuery' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _NewsletterSignupQuery);
20
20
  function NewsletterSignupComponent(_ref) {
@@ -71,11 +71,25 @@ const getInitialElements = () => {
71
71
  return map;
72
72
  };
73
73
  const Root = props => {
74
+ var _props$config;
75
+ // The script-tag integration is client-only — there is no server render to
76
+ // forward a preview token to — so the admin toolbar must enter/exit preview
77
+ // by toggling the marker cookie in the browser. Force it on here, where the
78
+ // integration is unambiguously client-side. (The admin's Fontdue session
79
+ // rides the credentialed cross-origin GraphQL fetch and gates the reveal.)
80
+ const config = {
81
+ ...props.config,
82
+ preview: {
83
+ ...((_props$config = props.config) === null || _props$config === void 0 ? void 0 : _props$config.preview),
84
+ clientSide: true
85
+ }
86
+ };
87
+
74
88
  // use a Map here instead of an array in case we register the same element
75
89
  // more than once (otherwise we run into issues rendering the component
76
90
  // twice into the same element). this also gives us a way to set unique keys.
77
91
  const [elements, setElements] = useState(getInitialElements());
78
- const store = useRef(createDefaultStore(props.config));
92
+ const store = useRef(createDefaultStore(config));
79
93
  useEffect(() => {
80
94
  // watch for any new or removed fontdue elements
81
95
 
@@ -190,7 +204,7 @@ const Root = props => {
190
204
  return /*#__PURE__*/React.createElement(FontdueProvider, {
191
205
  url: props.url,
192
206
  stripeIntegration: props.stripeIntegration,
193
- config: props.config,
207
+ config: config,
194
208
  components: props.components,
195
209
  store: store.current
196
210
  }, /*#__PURE__*/React.createElement(React.Fragment, null, Array.from(elements).map(_ref => {
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { TestFontsForm_Query } from '../../__generated__/TestFontsForm_Query.graphql.js';
3
- import { SerializablePreloadedQuery } from '../../relay/loadSerializableQuery.js';
3
+ import { SerializablePreloadedQuery, type LoadQueryOptions } from '../../relay/loadSerializableQuery.js';
4
4
  import { Config } from '../ConfigContext.js';
5
5
  export interface TestFontsForm_props {
6
6
  agreementLabel?: string;
@@ -8,7 +8,7 @@ export interface TestFontsForm_props {
8
8
  newsletterCheckboxChecked?: boolean;
9
9
  }
10
10
  export type TestFontsFormPreloadedQuery = SerializablePreloadedQuery<TestFontsForm_Query>;
11
- export declare function loadTestFontsFormQuery(): Promise<TestFontsFormPreloadedQuery>;
11
+ export declare function loadTestFontsFormQuery(options?: LoadQueryOptions): Promise<TestFontsFormPreloadedQuery>;
12
12
  export declare function TestFontsFormPreloadedQueryRenderer({ preloadedQuery, ...rest }: TestFontsForm_props & {
13
13
  preloadedQuery: TestFontsFormPreloadedQuery;
14
14
  }): React.JSX.Element;
@@ -194,8 +194,8 @@ const TestFontsFormComponent = _ref2 => {
194
194
  }));
195
195
  };
196
196
  const query = (_TestFontsForm_Query.hash && _TestFontsForm_Query.hash !== "cd43f0cacc4dcf01cf94fb1ff97197ca" && 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);
197
- export async function loadTestFontsFormQuery() {
198
- return loadSerializableQuery(TestFontsFormQueryNode, {});
197
+ export async function loadTestFontsFormQuery(options) {
198
+ return loadSerializableQuery(TestFontsFormQueryNode, {}, options);
199
199
  }
200
200
  export function TestFontsFormPreloadedQueryRenderer(_ref4) {
201
201
  let {
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { Alignment, Direction, FeaturesProp } from './types.js';
3
- import { SerializablePreloadedQuery } from '../../relay/loadSerializableQuery.js';
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
6
  interface TypeTesterStandaloneComponent_props {
@@ -30,7 +30,7 @@ export interface LoadTypeTesterQueryVariables {
30
30
  familyName: string;
31
31
  styleName: string;
32
32
  }
33
- export declare function loadTypeTesterQuery(variables: LoadTypeTesterQueryVariables): Promise<TypeTesterPreloadedQuery>;
33
+ export declare function loadTypeTesterQuery(variables: LoadTypeTesterQueryVariables, options?: LoadQueryOptions): Promise<TypeTesterPreloadedQuery>;
34
34
  export declare function TypeTesterStandalonePreloadedQueryRenderer({ preloadedQuery, ...rest }: TypeTesterStandaloneComponent_props & {
35
35
  preloadedQuery: TypeTesterPreloadedQuery;
36
36
  }): React.JSX.Element;
@@ -100,11 +100,11 @@ function TypeTesterStandaloneComponent(_ref) {
100
100
  }));
101
101
  }
102
102
  const query = (_TypeTesterStandaloneQuery.hash && _TypeTesterStandaloneQuery.hash !== "951214eacb2370ea3ca0b19bebe7fbe7" && console.error("The definition of 'TypeTesterStandaloneQuery' appears to have changed. Run `relay-compiler` to update the generated files to receive the expected data."), _TypeTesterStandaloneQuery);
103
- export async function loadTypeTesterQuery(variables) {
103
+ export async function loadTypeTesterQuery(variables, options) {
104
104
  return loadSerializableQuery(TypeTesterStandaloneQueryNode, {
105
105
  familyName: variables.familyName,
106
106
  styleName: variables.styleName
107
- });
107
+ }, options);
108
108
  }
109
109
  export function TypeTesterStandalonePreloadedQueryRenderer(_ref3) {
110
110
  let {
@@ -3,7 +3,7 @@ import { TypeTestersIDQuery } from '../../__generated__/TypeTestersIDQuery.graph
3
3
  import { TypeTestersSlugQuery } from '../../__generated__/TypeTestersSlugQuery.graphql.js';
4
4
  import { Config } from '../ConfigContext.js';
5
5
  import { FeaturesProp } from '../TypeTester/types.js';
6
- import { SerializablePreloadedQuery } from '../../relay/loadSerializableQuery.js';
6
+ import { SerializablePreloadedQuery, type LoadQueryOptions } from '../../relay/loadSerializableQuery.js';
7
7
  export interface TypeTesters_props {
8
8
  collectionId?: string;
9
9
  collectionSlug?: string;
@@ -33,7 +33,7 @@ export type LoadTypeTestersQueryVariables = ({
33
33
  tags?: string[] | null;
34
34
  excludeTags?: string[] | null;
35
35
  };
36
- export declare function loadTypeTestersQuery(variables: LoadTypeTestersQueryVariables): Promise<TypeTestersPreloadedQuery>;
36
+ export declare function loadTypeTestersQuery(variables: LoadTypeTestersQueryVariables, options?: LoadQueryOptions): Promise<TypeTestersPreloadedQuery>;
37
37
  export type TypeTesters_unifiedProps = TypeTesters_props & {
38
38
  preloadedQuery?: TypeTestersPreloadedQuery;
39
39
  config?: Config;
@@ -251,7 +251,7 @@ function TypeTestersSlugQueryRenderer(_ref6) {
251
251
  excludeTags: excludeTags
252
252
  }));
253
253
  }
254
- export async function loadTypeTestersQuery(variables) {
254
+ export async function loadTypeTestersQuery(variables, options) {
255
255
  const tags = variables.tags ?? null;
256
256
  const excludeTags = variables.excludeTags ?? null;
257
257
  if (variables.collectionId) {
@@ -259,14 +259,14 @@ export async function loadTypeTestersQuery(variables) {
259
259
  collectionId: variables.collectionId,
260
260
  tags,
261
261
  excludeTags
262
- });
262
+ }, options);
263
263
  }
264
264
  if (variables.collectionSlug) {
265
265
  return loadSerializableQuery(TypeTestersSlugQueryNode, {
266
266
  collectionSlug: variables.collectionSlug,
267
267
  tags,
268
268
  excludeTags
269
- });
269
+ }, options);
270
270
  }
271
271
  throw new Error('loadTypeTestersQuery expected either collectionId or collectionSlug');
272
272
  }
@@ -8,6 +8,7 @@ export type VerticalMetrics = {
8
8
  readonly ascender: number;
9
9
  readonly descender: number;
10
10
  readonly lineGap: number | null;
11
+ readonly avgCharWidth: number | null;
11
12
  };
12
13
  declare const useFontStyle: ({ fontStyle: fontStyleKey, }: UseFontStyle_props) => {
13
14
  style: React.CSSProperties;