fontdue-js 3.0.3 → 3.0.5

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,12 @@
1
+ ## 3.0.5
2
+
3
+ - Fixed `StoreModal` showing a missing-glyph box or an oversized gap between the `Aa` previews of variable-font instances and bundle styles. The previews were separated by a thin space (U+2009) rendered in the foundry’s own font — fonts with no glyph for that character showed a `.notdef` box between every preview, and monospaced fonts rendered it at a full advance width. The previews are now spaced in CSS.
4
+ - The `Consent Granted` event that `ConsentBanner` posts on “Accept all” now carries the entry page’s attribution — its URL, path, referrer, title, and any `utm_*`, `fbclid` or `gclid` parameters, plus a `context` object — captured when the page first loaded rather than at the moment consent was granted. Campaign attribution now survives a visitor navigating around the site before they accept.
5
+
6
+ ## 3.0.4
7
+
8
+ - Fixed the `CharacterViewer` breaking its grid layout when a character set contains glyphs that need an OpenType feature. Each character set is wrapped in its own element so it can carry its own `font-feature-settings`; those wrappers are now `display: contents` so their cells flow through the block's single grid, rather than each wrapper being its own grid and forcing a row break at every feature boundary — an inserted `ss01`/`salt` alternate no longer lands alone on the next row.
9
+
1
10
  ## 3.0.3
2
11
 
3
12
  - Fixed misaligned style names in `StoreModal`. The feature-glyphs preview is no longer rendered when a style has nothing to show — a variable font with no named instances, or a non-variable style with no representative glyph for its languages — so its empty wrapper no longer indents the style name and rows in a family now line up.
@@ -207,9 +207,11 @@ function CharacterViewerComponent(_ref2) {
207
207
  const [cellWidth, setCellWidth] = useState(null);
208
208
  const measureCell = useCallback(() => {
209
209
  var _blocksRef$current;
210
- const list = (_blocksRef$current = blocksRef.current) === null || _blocksRef$current === void 0 ? void 0 : _blocksRef$current.querySelector('.character-viewer__block__character-list');
211
- if (!list) return;
212
- const firstTrack = getComputedStyle(list).gridTemplateColumns.split(' ')[0];
210
+ // The grid container is the per-block characters element; the
211
+ // character-list wrappers inside it are display: contents (FD-810).
212
+ const grid = (_blocksRef$current = blocksRef.current) === null || _blocksRef$current === void 0 ? void 0 : _blocksRef$current.querySelector('.character-viewer__block__characters');
213
+ if (!grid) return;
214
+ const firstTrack = getComputedStyle(grid).gridTemplateColumns.split(' ')[0];
213
215
  const width = parseFloat(firstTrack);
214
216
  if (!Number.isNaN(width)) setCellWidth(width);
215
217
  }, []);
@@ -1,5 +1,20 @@
1
1
  export declare function setClientAnonymousId(id: string): void;
2
2
  export declare function getClientAnonymousId(): string | undefined;
3
+ /**
4
+ * The tracker captures the entry page view once on mount and stores its
5
+ * attribution here (url, referrer, fbclid, utm campaign). The consent banner
6
+ * reads it when the visitor grants consent so the `Consent Granted` event can
7
+ * carry the arrival attribution — preserving it even if the visitor has since
8
+ * navigated away from the landing URL. The server forwards that event; each
9
+ * destination decides how to represent it (Meta reads it as the entry
10
+ * PageView). Undefined until the tracker has mounted.
11
+ */
12
+ export type EntryAttribution = {
13
+ properties: Record<string, unknown>;
14
+ context: Record<string, unknown>;
15
+ };
16
+ export declare function setEntryAttribution(body: EntryAttribution): void;
17
+ export declare function getEntryAttribution(): EntryAttribution | undefined;
3
18
  /** Parse the _fontdue_cc cookie and check if it contains the given category. */
4
19
  export declare function hasConsent(category: string): boolean;
5
20
  /** Set the _fontdue_cc cookie with the given categories. */
@@ -11,6 +11,24 @@ export function getClientAnonymousId() {
11
11
  return clientAnonymousId;
12
12
  }
13
13
 
14
+ /**
15
+ * The tracker captures the entry page view once on mount and stores its
16
+ * attribution here (url, referrer, fbclid, utm campaign). The consent banner
17
+ * reads it when the visitor grants consent so the `Consent Granted` event can
18
+ * carry the arrival attribution — preserving it even if the visitor has since
19
+ * navigated away from the landing URL. The server forwards that event; each
20
+ * destination decides how to represent it (Meta reads it as the entry
21
+ * PageView). Undefined until the tracker has mounted.
22
+ */
23
+
24
+ let entryAttribution;
25
+ export function setEntryAttribution(body) {
26
+ entryAttribution = body;
27
+ }
28
+ export function getEntryAttribution() {
29
+ return entryAttribution;
30
+ }
31
+
14
32
  /** Parse the _fontdue_cc cookie and check if it contains the given category. */
15
33
  export function hasConsent(category) {
16
34
  const match = document.cookie.match(new RegExp('(?:^|;\\s*)' + CONSENT_COOKIE + '=([^;]*)'));
@@ -1,7 +1,7 @@
1
1
  import React, { useContext, useEffect, useState } from 'react';
2
2
  import ConfigContext from '../ConfigContext.js';
3
3
  import { useFontdueUrl } from '../UrlContext.js';
4
- import { setConsent, activateScripts, getClientAnonymousId } from './consent.js';
4
+ import { setConsent, activateScripts, getClientAnonymousId, getEntryAttribution } from './consent.js';
5
5
  const DEFAULT_MESSAGE = 'We use cookies to analyze site usage and improve your\u00A0experience.';
6
6
  const ConsentBanner = () => {
7
7
  const config = useContext(ConfigContext);
@@ -19,15 +19,21 @@ const ConsentBanner = () => {
19
19
  if (dismissed) return null;
20
20
  const message = config.tracking.consentMessage || DEFAULT_MESSAGE;
21
21
  const sendConsentEvent = categories => {
22
- // Fire a tracking event so the server sets the _fontdue_state cookie
23
- // on this response (rather than waiting for the next page load).
24
- // Include the client anonymous_id so it gets synced to the cookie.
22
+ // Fire a normal tracking event on the existing pipeline. Because the consent
23
+ // cookie was just written, the server is now permitted to forward, sets the
24
+ // durable _fontdue_state cookie on this response (rather than waiting for the
25
+ // next page load), and syncs the client anonymous_id into it. Carry the
26
+ // captured entry-page attribution so destinations that read the grant as a
27
+ // page view (Meta) attribute it to the visitor's arrival (referrer, fbclid).
28
+ const entry = getEntryAttribution();
25
29
  const body = {
26
30
  event: 'Consent Granted',
27
31
  properties: {
32
+ ...((entry === null || entry === void 0 ? void 0 : entry.properties) ?? {}),
28
33
  categories: categories.join(',')
29
34
  }
30
35
  };
36
+ if (entry !== null && entry !== void 0 && entry.context) body.context = entry.context;
31
37
  const anonId = getClientAnonymousId();
32
38
  if (anonId) body.anonymous_id = anonId;
33
39
  fetch(`${url}/api/track`, {
@@ -40,6 +46,8 @@ const ConsentBanner = () => {
40
46
  }).catch(() => {});
41
47
  };
42
48
  const handleAcceptAll = () => {
49
+ // Write the consent cookie first, so the tracking request carries it and the
50
+ // server is permitted to forward.
43
51
  setConsent(['necessary', 'analytics']);
44
52
  activateScripts('analytics');
45
53
  if (config.tracking.enabled) {
@@ -8,7 +8,6 @@ import { PriceFormat } from '../Price/index.js';
8
8
  import FontStyle from '../FontStyle/index.js';
9
9
  import { isSelected } from '../../reducer.js';
10
10
  import StoreModalStyleButtonElement from '../elements/StoreModalStyleButton/index.js';
11
- const THIN_SPACE = '\u2009';
12
11
  export default function StoreModalBundleButton(_ref) {
13
12
  var _bundle$sku;
14
13
  let {
@@ -27,11 +26,11 @@ export default function StoreModalBundleButton(_ref) {
27
26
  onClick: handleSelect,
28
27
  selected: selected
29
28
  }), {
30
- featureGlyphs: bundle.fontStyles.map((fontStyle, i) => /*#__PURE__*/React.createElement(FontStyle, {
29
+ featureGlyphs: bundle.fontStyles.map(fontStyle => /*#__PURE__*/React.createElement(FontStyle, {
31
30
  key: fontStyle.id,
32
31
  fontStyle: fontStyle,
33
32
  Component: "span"
34
- }, i !== 0 && THIN_SPACE, "Aa")),
33
+ }, "Aa")),
35
34
  name: bundle.name,
36
35
  price: /*#__PURE__*/React.createElement(SKUPrice, {
37
36
  sku: bundle.sku,
@@ -9,7 +9,6 @@ import FontStyle from '../FontStyle/index.js';
9
9
  import { isSelected } from '../../reducer.js';
10
10
  import StoreModalStyleButtonElement from '../elements/StoreModalStyleButton/index.js';
11
11
  import { getFeatureGlyphs, variableInstanceCSS } from '../../utils.js';
12
- const THIN_SPACE = '\u2009';
13
12
  export default function StoreModalStyleButton(_ref) {
14
13
  var _fontStyle$variableIn, _fontStyle$variableIn2;
15
14
  let {
@@ -32,10 +31,10 @@ export default function StoreModalStyleButton(_ref) {
32
31
  featureGlyphs: hasFeatureGlyphs ? /*#__PURE__*/React.createElement(FontStyle, {
33
32
  fontStyle: fontStyle,
34
33
  Component: "span"
35
- }, isVariableFont ? (_fontStyle$variableIn2 = fontStyle.variableInstances) === null || _fontStyle$variableIn2 === void 0 ? void 0 : _fontStyle$variableIn2.map((instance, i) => /*#__PURE__*/React.createElement("span", {
34
+ }, isVariableFont ? (_fontStyle$variableIn2 = fontStyle.variableInstances) === null || _fontStyle$variableIn2 === void 0 ? void 0 : _fontStyle$variableIn2.map(instance => /*#__PURE__*/React.createElement("span", {
36
35
  key: instance.name,
37
36
  style: variableInstanceCSS(instance)
38
- }, i !== 0 && THIN_SPACE, getFeatureGlyphs(fontStyle.supportedLanguages))) : getFeatureGlyphs(fontStyle.supportedLanguages)) : null,
37
+ }, getFeatureGlyphs(fontStyle.supportedLanguages))) : getFeatureGlyphs(fontStyle.supportedLanguages)) : null,
39
38
  // for variable fonts, only show the name of the style if there are
40
39
  // multiple styles
41
40
  name: isVariableFont ? fontStyle.sku ? fontStyle.name : undefined : fontStyle.name,
@@ -3,7 +3,7 @@
3
3
  import { useContext, useEffect } from 'react';
4
4
  import ConfigContext from '../ConfigContext.js';
5
5
  import { useFontdueUrl } from '../UrlContext.js';
6
- import { hasConsent, onConsent, setClientAnonymousId } from '../ConsentBanner/consent.js';
6
+ import { hasConsent, onConsent, setClientAnonymousId, setEntryAttribution } from '../ConsentBanner/consent.js';
7
7
  function getCampaignDataFromUrl() {
8
8
  const urlParams = new URLSearchParams(window.location.search);
9
9
  const campaign = {};
@@ -37,32 +37,41 @@ function getCampaignDataFromUrl() {
37
37
  properties: hasProperties ? properties : null
38
38
  };
39
39
  }
40
- async function sendPageView(baseUrl, anonymousId) {
40
+ // Build the page-view payload from the current document. Captured once for the
41
+ // entry page view so its attribution (referrer, fbclid, utm) can travel with the
42
+ // consent grant, preserving arrival attribution even if the visitor has since
43
+ // navigated.
44
+ function buildPageViewBody() {
45
+ const {
46
+ campaign,
47
+ properties: clickIdProperties
48
+ } = getCampaignDataFromUrl();
49
+ const properties = {
50
+ url: window.location.href,
51
+ path: window.location.pathname,
52
+ referrer: document.referrer,
53
+ title: document.title,
54
+ search: window.location.search,
55
+ ...clickIdProperties
56
+ };
57
+ const context = {
58
+ userAgent: navigator.userAgent
59
+ };
60
+ if (campaign) {
61
+ context.campaign = campaign;
62
+ }
63
+ return {
64
+ properties,
65
+ context
66
+ };
67
+ }
68
+ async function postPageView(baseUrl, body, anonymousId) {
41
69
  try {
42
- const {
43
- campaign,
44
- properties: clickIdProperties
45
- } = getCampaignDataFromUrl();
46
- const properties = {
47
- url: window.location.href,
48
- path: window.location.pathname,
49
- referrer: document.referrer,
50
- title: document.title,
51
- search: window.location.search,
52
- ...clickIdProperties
53
- };
54
- const context = {
55
- userAgent: navigator.userAgent
56
- };
57
- if (campaign) {
58
- context.campaign = campaign;
59
- }
60
- const body = {
61
- properties,
62
- context
70
+ const payload = {
71
+ ...body
63
72
  };
64
73
  if (anonymousId) {
65
- body.anonymous_id = anonymousId;
74
+ payload.anonymous_id = anonymousId;
66
75
  }
67
76
  const response = await fetch(`${baseUrl}/api/track/page`, {
68
77
  method: 'POST',
@@ -70,7 +79,7 @@ async function sendPageView(baseUrl, anonymousId) {
70
79
  'Content-Type': 'application/json'
71
80
  },
72
81
  credentials: 'include',
73
- body: JSON.stringify(body)
82
+ body: JSON.stringify(payload)
74
83
  });
75
84
  const data = await response.json();
76
85
  return data.anonymous_id || null;
@@ -79,6 +88,9 @@ async function sendPageView(baseUrl, anonymousId) {
79
88
  return null;
80
89
  }
81
90
  }
91
+ async function sendPageView(baseUrl, anonymousId) {
92
+ return postPageView(baseUrl, buildPageViewBody(), anonymousId);
93
+ }
82
94
  const Tracking = () => {
83
95
  const config = useContext(ConfigContext);
84
96
  const url = useFontdueUrl();
@@ -96,8 +108,20 @@ const Tracking = () => {
96
108
  const analyticsConsent = hasConsent('analytics');
97
109
  const effectiveAnonymousId = clientAnonymousId && config.tracking.consentRequired && !analyticsConsent ? clientAnonymousId : undefined;
98
110
 
99
- // Send initial page view
100
- const pageViewPromise = sendPageView(url, effectiveAnonymousId);
111
+ // Capture the entry page view once, then send it. Before consent the server
112
+ // records but does not forward it.
113
+ const entryBody = buildPageViewBody();
114
+ const pageViewPromise = postPageView(url, entryBody, effectiveAnonymousId);
115
+
116
+ // Publish this captured entry attribution so the consent banner can attach
117
+ // it to the `Consent Granted` event when the visitor grants consent. That
118
+ // event is a normal tracking event on the existing pipeline; the server
119
+ // forwards it and each destination decides how to represent it (Meta reads
120
+ // it as the attributed entry page view). Capturing once preserves the
121
+ // arrival attribution even if the visitor has since navigated.
122
+ if (config.tracking.consentRequired) {
123
+ setEntryAttribution(entryBody);
124
+ }
101
125
 
102
126
  // Segment SDK for device-mode integrations (e.g. Google Tag Manager)
103
127
  // The analytics factory is provided by the entry point — it calls AnalyticsBrowser.load()
package/dist/fontdue.css CHANGED
@@ -1075,11 +1075,12 @@ fontdue-type-tester {
1075
1075
  .character-viewer__block__characters {
1076
1076
  font-size: 30px;
1077
1077
  margin-left: 1px;
1078
+ display: grid;
1079
+ grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
1078
1080
  }
1079
1081
 
1080
1082
  .character-viewer__block__character-list {
1081
- display: grid;
1082
- grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
1083
+ display: contents;
1083
1084
  }
1084
1085
  .character-viewer__block__character-list > div {
1085
1086
  min-height: var(--cell-width, 60px);
@@ -2353,6 +2354,12 @@ input:checked + .checkbox__icon {
2353
2354
  margin-right: 10px;
2354
2355
  word-break: break-word;
2355
2356
  }
2357
+ .store-modal__style-button__feature-glyphs span {
2358
+ display: inline-block;
2359
+ }
2360
+ .store-modal__style-button__feature-glyphs span:not(:last-child) {
2361
+ margin-right: 0.2em;
2362
+ }
2356
2363
 
2357
2364
  .store-modal__style-button__price,
2358
2365
  .store-modal__style-button__checked {
@@ -7,7 +7,7 @@ import { PREVIEW_HEADER, hasPreviewMarkerCookie } from '../preview/constants.js'
7
7
  // (defineVersionPlugin in .babelrc.cjs) with the literal package.json#version.
8
8
  // Exported so UI (the admin toolbar) can surface it without re-reading the
9
9
  // build-time global in a 'use client' module.
10
- export const version = "3.0.3";
10
+ export const version = "3.0.5";
11
11
  const IS_SERVER = typeof window === typeof undefined;
12
12
 
13
13
  // Opt server fetches into Next's data cache only in production; dev stays
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fontdue-js",
3
- "version": "3.0.3",
3
+ "version": "3.0.5",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "build": "npm run relay && run-p build-js build-css build-ts",