@developer_tribe/react-builder 1.0.9 → 1.2.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 (49) hide show
  1. package/dist/build-components/BIcon/BIconProps.generated.d.ts +2 -2
  2. package/dist/build-components/OnboardFooter/OnboardFooterProps.generated.d.ts +2 -2
  3. package/dist/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.d.ts +2 -2
  4. package/dist/build-components/OnboardTitle/OnboardTitleProps.generated.d.ts +2 -2
  5. package/dist/build-components/PaywallCloseButton/PaywallCloseButtonProps.generated.d.ts +2 -2
  6. package/dist/build-components/Text/TextProps.generated.d.ts +2 -2
  7. package/dist/build-components/patterns.generated.d.ts +78 -30
  8. package/dist/hooks/useProjectFonts.d.ts +13 -0
  9. package/dist/index.cjs.js +3 -3
  10. package/dist/index.cjs.js.map +1 -1
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.esm.js +3 -3
  13. package/dist/index.esm.js.map +1 -1
  14. package/dist/index.native.cjs.js +3 -3
  15. package/dist/index.native.cjs.js.map +1 -1
  16. package/dist/index.native.esm.js +3 -3
  17. package/dist/index.native.esm.js.map +1 -1
  18. package/dist/pages/ProjectPage.d.ts +4 -1
  19. package/dist/store.d.ts +11 -0
  20. package/dist/types/Fonts.d.ts +12 -0
  21. package/dist/utils/fontWeight.d.ts +3 -0
  22. package/dist/utils/fontsDebug.d.ts +12 -0
  23. package/dist/utils/loadFontFamily.d.ts +30 -0
  24. package/package.json +1 -1
  25. package/scripts/prebuild/utils/createGeneratedProps.js +9 -1
  26. package/scripts/prebuild/utils/validateAllComponentsOrThrow.js +2 -0
  27. package/src/AttributesEditor.tsx +237 -1
  28. package/src/RenderPage.tsx +15 -6
  29. package/src/attributes-editor/Field.tsx +18 -0
  30. package/src/build-components/BIcon/BIconProps.generated.ts +2 -13
  31. package/src/build-components/OnboardFooter/OnboardFooterProps.generated.ts +2 -13
  32. package/src/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.ts +2 -13
  33. package/src/build-components/OnboardTitle/OnboardTitleProps.generated.ts +2 -13
  34. package/src/build-components/PaywallCloseButton/PaywallCloseButtonProps.generated.ts +2 -13
  35. package/src/build-components/Text/TextProps.generated.ts +2 -13
  36. package/src/build-components/Text/pattern.json +13 -17
  37. package/src/build-components/patterns.generated.ts +78 -102
  38. package/src/components/BuilderButton.tsx +13 -11
  39. package/src/hooks/useProjectFonts.ts +130 -0
  40. package/src/index.ts +1 -0
  41. package/src/pages/ProjectPage.tsx +8 -0
  42. package/src/store.ts +46 -1
  43. package/src/types/Fonts.ts +16 -0
  44. package/src/utils/analyseNodeByPatterns.ts +9 -0
  45. package/src/utils/extractTextStyle.ts +98 -1
  46. package/src/utils/fontWeight.ts +29 -0
  47. package/src/utils/fontsDebug.ts +16 -0
  48. package/src/utils/loadFontFamily.ts +318 -0
  49. package/src/utils/patterns.ts +2 -0
package/src/store.ts CHANGED
@@ -16,7 +16,8 @@ import type {
16
16
  PaywallBenefits,
17
17
  PaywallBenefitValue,
18
18
  } from './paywall/types/benefits';
19
-
19
+ import type { Fonts } from './types/Fonts';
20
+ //TODO: project base store and general store should be separated. We can we use context
20
21
  type RenderStore = {
21
22
  projectName: string;
22
23
  setProjectName: (name: string) => void;
@@ -59,6 +60,22 @@ type RenderStore = {
59
60
  },
60
61
  ) => void;
61
62
  clearLogs: () => void;
63
+
64
+ // Fonts (provided by host app)
65
+ fonts: Fonts;
66
+ setFonts: (fonts: Fonts) => void;
67
+ // NOTE: appFont is intentionally non-optional as a prop in `ProjectPage`,
68
+ // but the value itself can be undefined when the host can't determine it.
69
+ appFont: string | undefined;
70
+ setAppFont: (appFont: string | undefined) => void;
71
+ // Validation/runtime errors surfaced to the host UI
72
+ errors: string[];
73
+ setErrors: (errors: string[]) => void;
74
+ addError: (error: string) => void;
75
+ clearErrors: () => void;
76
+ // Cache loaded font families to avoid repeated loads
77
+ loadedFonts: string[];
78
+ markFontLoaded: (fontFamily: string) => void;
62
79
  };
63
80
 
64
81
  export const useRenderStore = createWithEqualityFn<RenderStore>()(
@@ -188,6 +205,34 @@ export const useRenderStore = createWithEqualityFn<RenderStore>()(
188
205
  return { logs: [...state.logs, newEntry] };
189
206
  }),
190
207
  clearLogs: () => set({ logs: [] }),
208
+
209
+ fonts: [],
210
+ setFonts: (fonts) => set({ fonts: Array.isArray(fonts) ? fonts : [] }),
211
+ appFont: undefined,
212
+ setAppFont: (appFont) => set({ appFont }),
213
+ errors: [],
214
+ setErrors: (errors) =>
215
+ set({ errors: Array.isArray(errors) ? errors : [] }),
216
+ addError: (error) =>
217
+ set((state) => ({
218
+ errors: [
219
+ ...(Array.isArray(state.errors) ? state.errors : []),
220
+ String(error),
221
+ ],
222
+ })),
223
+ clearErrors: () => set({ errors: [] }),
224
+ loadedFonts: [],
225
+ markFontLoaded: (fontFamily) =>
226
+ set((state) => {
227
+ const family =
228
+ typeof fontFamily === 'string' ? fontFamily.trim() : '';
229
+ if (!family) return state;
230
+ const prev = Array.isArray(state.loadedFonts)
231
+ ? state.loadedFonts
232
+ : [];
233
+ if (prev.includes(family)) return state;
234
+ return { loadedFonts: [...prev, family] };
235
+ }),
191
236
  }),
192
237
  {
193
238
  name: 'render-store',
@@ -0,0 +1,16 @@
1
+ /** CSS font-weight key (usually "100".."900"). */
2
+ export type FontWeightKey = string;
3
+
4
+ /** URL to a font file (ttf/otf/woff/woff2). */
5
+ export type FontFileUrl = string;
6
+
7
+ /** Map of font-weight -> font file URL. */
8
+ export type FontFamilySources = Record<FontWeightKey, FontFileUrl>;
9
+
10
+ export type FontDefinition = {
11
+ name: string;
12
+ family: FontFamilySources;
13
+ projects?: string[];
14
+ };
15
+
16
+ export type Fonts = FontDefinition[];
@@ -7,6 +7,7 @@ import {
7
7
  isPrimitiveType,
8
8
  normalizeComponentType,
9
9
  } from './patterns';
10
+ import { FONT_WEIGHT_OPTIONS, normalizeFontWeight } from './fontWeight';
10
11
  import {
11
12
  isEmptyObject,
12
13
  isNodeArray,
@@ -233,6 +234,14 @@ function validatePrimitiveValue(
233
234
  return typeof value === 'number' || typeof value === 'string'
234
235
  ? ok()
235
236
  : fail(`Expected size (string or number)`, path);
237
+ case 'fontWeight': {
238
+ const normalized = normalizeFontWeight(value);
239
+ if (normalized) return ok();
240
+ return fail(
241
+ `Expected fontWeight (${FONT_WEIGHT_OPTIONS.join(', ')})`,
242
+ path,
243
+ );
244
+ }
236
245
  case 'iconType':
237
246
  return typeof value === 'string'
238
247
  ? ok()
@@ -6,6 +6,68 @@ import type { ProjectColors } from '../types/Project';
6
6
  import { fs, parseSize } from '../size-matters';
7
7
  import { parseColor } from './parseColor';
8
8
  import { extractViewStyle } from './extractViewStyle';
9
+ import { normalizeFontWeight } from './fontWeight';
10
+ import { useRenderStore } from '../store';
11
+ import {
12
+ findFontDefinition,
13
+ loadFontFamily,
14
+ resolveClosestFontWeightKey,
15
+ } from './loadFontFamily';
16
+ import { fontsDebug } from './fontsDebug';
17
+
18
+ const inFlightFontLoads: Map<string, Promise<void>> = new Map();
19
+
20
+ function weightToNumericKey(weight: unknown): string | undefined {
21
+ const normalized = normalizeFontWeight(weight);
22
+ if (!normalized) return undefined;
23
+ if (normalized === 'normal') return '400';
24
+ if (normalized === 'bold') return '700';
25
+ // already '100'..'900'
26
+ return normalized;
27
+ }
28
+
29
+ function ensureFontWeightLoaded(familyName: string, weightKey?: string) {
30
+ if (typeof document === 'undefined') return;
31
+ const name = familyName.trim();
32
+ if (!name) return;
33
+ const weight = weightKey?.trim() || '400';
34
+ const cacheKey = `${name}@${weight}`;
35
+ if (inFlightFontLoads.has(cacheKey)) return;
36
+
37
+ fontsDebug.info('extractTextStyle: ensureFontWeightLoaded', {
38
+ familyName: name,
39
+ weight,
40
+ });
41
+
42
+ const { fonts, markFontLoaded, addError } = useRenderStore.getState();
43
+ const promise = loadFontFamily(fonts, name, {
44
+ preferWeight: weight,
45
+ forceFetch: true,
46
+ })
47
+ .then(() => {
48
+ fontsDebug.info('extractTextStyle: font weight loaded', {
49
+ familyName: name,
50
+ weight,
51
+ });
52
+ markFontLoaded(name);
53
+ })
54
+ .catch((e) => {
55
+ fontsDebug.compactError('extractTextStyle: font weight load failed', e, {
56
+ familyName: name,
57
+ weight,
58
+ });
59
+ addError(
60
+ `Failed to load font "${name}" (weight ${weight}): ${
61
+ e instanceof Error ? e.message : String(e)
62
+ }`,
63
+ );
64
+ })
65
+ .finally(() => {
66
+ inFlightFontLoads.delete(cacheKey);
67
+ });
68
+
69
+ inFlightFontLoads.set(cacheKey, promise);
70
+ }
9
71
 
10
72
  type ExtractTextStyleOptions = {
11
73
  appConfig?: AppConfig;
@@ -48,8 +110,43 @@ export function extractTextStyle<T extends TextPropsGenerated['attributes']>(
48
110
  } else {
49
111
  style.fontSize = fs(14);
50
112
  }
113
+ const fontFamily = get('fontFamily') as any;
51
114
  const fontWeight = get('fontWeight') as any;
52
- if (fontWeight) style.fontWeight = fontWeight;
115
+ const requestedWeight = weightToNumericKey(fontWeight);
116
+ const normalizedFontFamily =
117
+ typeof fontFamily === 'string' && fontFamily.trim().length > 0
118
+ ? fontFamily.trim()
119
+ : undefined;
120
+ if (normalizedFontFamily) {
121
+ // Resolve "closest available" weight for this family (e.g. if requested 100 but family starts at 300).
122
+ const { fonts } = useRenderStore.getState();
123
+ const def = findFontDefinition(fonts, normalizedFontFamily);
124
+ const resolvedWeightKey =
125
+ def?.family && typeof def.family === 'object'
126
+ ? resolveClosestFontWeightKey(
127
+ def.family as Record<string, string>,
128
+ requestedWeight,
129
+ )
130
+ : requestedWeight;
131
+
132
+ fontsDebug.info('extractTextStyle: resolved font family/weight', {
133
+ fontFamily: normalizedFontFamily,
134
+ requestedWeight,
135
+ resolvedWeightKey,
136
+ });
137
+
138
+ style.fontFamily = `"${normalizedFontFamily}"`;
139
+ // Ensure the correct weight file is available (lazy-load per weight).
140
+ ensureFontWeightLoaded(normalizedFontFamily, resolvedWeightKey);
141
+ // Important: set fontWeight to the actual weight we loaded so CSS requests match loaded face.
142
+ if (resolvedWeightKey) {
143
+ style.fontWeight = resolvedWeightKey;
144
+ }
145
+ }
146
+ const normalizedFontWeight = normalizeFontWeight(fontWeight);
147
+ // If no fontFamily is set, keep previous behavior.
148
+ if (!normalizedFontFamily && normalizedFontWeight)
149
+ style.fontWeight = normalizedFontWeight;
53
150
  const resolvedTextColor = parseColor(get('color') as any, {
54
151
  projectColors: options.projectColors,
55
152
  appConfig: resolvedAppConfig,
@@ -0,0 +1,29 @@
1
+ export const FONT_WEIGHT_OPTIONS = [
2
+ 'normal',
3
+ 'bold',
4
+ '100',
5
+ '200',
6
+ '300',
7
+ '400',
8
+ '500',
9
+ '600',
10
+ '700',
11
+ '800',
12
+ '900',
13
+ ] as const;
14
+
15
+ export type FontWeightOption = (typeof FONT_WEIGHT_OPTIONS)[number];
16
+
17
+ export function normalizeFontWeight(
18
+ value: unknown,
19
+ ): FontWeightOption | undefined {
20
+ if (typeof value === 'number' && Number.isFinite(value)) {
21
+ const asString = String(value) as FontWeightOption;
22
+ return FONT_WEIGHT_OPTIONS.includes(asString) ? asString : undefined;
23
+ }
24
+ if (typeof value === 'string') {
25
+ const trimmed = value.trim() as FontWeightOption;
26
+ return FONT_WEIGHT_OPTIONS.includes(trimmed) ? trimmed : undefined;
27
+ }
28
+ return undefined;
29
+ }
@@ -0,0 +1,16 @@
1
+ type Payload = unknown;
2
+
3
+ /**
4
+ * Intentionally silent logger for font pipeline.
5
+ * (We keep the interface so call sites don't need to change.)
6
+ */
7
+ export const fontsDebug = {
8
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
9
+ info(_message: string, _payload?: Payload) {},
10
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
11
+ warn(_message: string, _payload?: Payload) {},
12
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
13
+ error(_message: string, _payload?: Payload) {},
14
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
15
+ compactError(_message: string, _error: unknown, _payload?: Payload) {},
16
+ };
@@ -0,0 +1,318 @@
1
+ import type { Fonts, FontDefinition } from '../types/Fonts';
2
+ import { fontsDebug } from './fontsDebug';
3
+
4
+ function normalizeFamilyName(value: unknown): string {
5
+ return typeof value === 'string' ? value.trim() : '';
6
+ }
7
+
8
+ export function findFontDefinition(
9
+ fonts: Fonts,
10
+ familyName: string,
11
+ ): FontDefinition | undefined {
12
+ const name = normalizeFamilyName(familyName);
13
+ if (!name) return undefined;
14
+ if (!Array.isArray(fonts)) return undefined;
15
+ return fonts.find((f) => f?.name === name);
16
+ }
17
+
18
+ function parseWeightKey(value: unknown): number | null {
19
+ if (typeof value !== 'string') return null;
20
+ const trimmed = value.trim();
21
+ if (!trimmed) return null;
22
+ const n = Number(trimmed);
23
+ return Number.isFinite(n) ? n : null;
24
+ }
25
+
26
+ /**
27
+ * Given a family sources map (weightKey -> url) and a requested weight,
28
+ * returns the closest available weight key (or a sensible fallback).
29
+ */
30
+ export function resolveClosestFontWeightKey(
31
+ family: Record<string, string>,
32
+ requestedWeight?: string,
33
+ ): string | undefined {
34
+ const entries = Object.entries(family ?? {}).filter(
35
+ ([k, v]) =>
36
+ typeof k === 'string' &&
37
+ k.trim().length > 0 &&
38
+ typeof v === 'string' &&
39
+ v.trim().length > 0,
40
+ );
41
+ if (entries.length === 0) return undefined;
42
+
43
+ const reqKey = normalizeFamilyName(requestedWeight);
44
+ if (reqKey && entries.some(([k]) => k === reqKey)) return reqKey;
45
+
46
+ // Prefer 400 if nothing exact matches.
47
+ if (entries.some(([k]) => k === '400')) return '400';
48
+
49
+ const reqNum = parseWeightKey(reqKey);
50
+ const numeric = entries
51
+ .map(([k]) => ({ k, n: parseWeightKey(k) }))
52
+ .filter((x): x is { k: string; n: number } => typeof x.n === 'number');
53
+ if (numeric.length === 0) return entries[0][0];
54
+
55
+ if (typeof reqNum !== 'number') {
56
+ // No requested weight: pick the smallest numeric weight as a stable default.
57
+ numeric.sort((a, b) => a.n - b.n);
58
+ return numeric[0].k;
59
+ }
60
+
61
+ // Pick closest numeric weight.
62
+ let best = numeric[0];
63
+ let bestDiff = Math.abs(best.n - reqNum);
64
+ for (const cand of numeric) {
65
+ const diff = Math.abs(cand.n - reqNum);
66
+ if (diff < bestDiff) {
67
+ best = cand;
68
+ bestDiff = diff;
69
+ }
70
+ }
71
+ return best.k;
72
+ }
73
+
74
+ export function resolveFontSourceForWeight(
75
+ family: Record<string, string>,
76
+ requestedWeight?: string,
77
+ ): { weight: string; url: string } | undefined {
78
+ const weight = resolveClosestFontWeightKey(family, requestedWeight);
79
+ if (!weight) return undefined;
80
+ const url = family?.[weight];
81
+ if (typeof url !== 'string' || !url.trim()) return undefined;
82
+ return { weight, url: url.trim() };
83
+ }
84
+
85
+ type LoadFontFamilyOptions = {
86
+ preferWeight?: string;
87
+ /**
88
+ * If true (default), fetch the font file via `fetch()` first so:
89
+ * - the request is visible in DevTools Network (Fetch/XHR)
90
+ * - we can log response status and surface CORS/404 clearly
91
+ *
92
+ * If false, falls back to `new FontFace(name, 'url(...)')`.
93
+ */
94
+ forceFetch?: boolean;
95
+ };
96
+
97
+ /**
98
+ * Web-only font loader.
99
+ * - Uses the `FontFace` API when available.
100
+ * - Loads a single preferred weight (default: 400) to keep it "lazy".
101
+ * - Caller is responsible for caching (e.g. via `loadedFonts` in the store).
102
+ */
103
+ export async function loadFontFamily(
104
+ fonts: Fonts,
105
+ familyName: string,
106
+ options: LoadFontFamilyOptions = {},
107
+ ): Promise<void> {
108
+ const name = normalizeFamilyName(familyName);
109
+ if (!name) throw new Error('fontFamily is empty');
110
+
111
+ fontsDebug.info('loadFontFamily: start', {
112
+ familyName: name,
113
+ preferWeight: options.preferWeight,
114
+ });
115
+
116
+ const def = findFontDefinition(fonts, name);
117
+ if (!def) throw new Error(`fontFamily "${name}" not found in fonts`);
118
+
119
+ const family = def.family;
120
+ if (!family || typeof family !== 'object') {
121
+ throw new Error(`fontFamily "${name}" has no "family" sources`);
122
+ }
123
+
124
+ // Not a browser environment (SSR / RN): do nothing.
125
+ if (typeof document === 'undefined') return;
126
+
127
+ const fontsApi = (document as any).fonts as FontFaceSet | undefined;
128
+ if (!fontsApi || typeof (globalThis as any).FontFace !== 'function') {
129
+ throw new Error('Font loading is not supported in this environment');
130
+ }
131
+ const safeFontsApi = fontsApi as FontFaceSet;
132
+
133
+ function hasLoadedFace(familyName: string, weight: string): boolean {
134
+ try {
135
+ const set = safeFontsApi as unknown as Iterable<FontFace>;
136
+ for (const face of set) {
137
+ const f = face as unknown as {
138
+ family?: string;
139
+ weight?: string;
140
+ status?: string;
141
+ };
142
+ const fam =
143
+ typeof f.family === 'string' ? f.family.replace(/['"]/g, '') : '';
144
+ const w = typeof f.weight === 'string' ? f.weight.trim() : '';
145
+ if (fam === familyName && w === weight && f.status === 'loaded') {
146
+ return true;
147
+ }
148
+ }
149
+ } catch {
150
+ // ignore
151
+ }
152
+ return false;
153
+ }
154
+
155
+ const resolved = resolveFontSourceForWeight(
156
+ family as Record<string, string>,
157
+ normalizeFamilyName(options.preferWeight),
158
+ );
159
+ if (!resolved)
160
+ throw new Error(`fontFamily "${name}" has no usable source URLs`);
161
+ const { weight: preferWeight, url } = resolved;
162
+
163
+ fontsDebug.info('loadFontFamily: resolved source', {
164
+ familyName: name,
165
+ weight: preferWeight,
166
+ url,
167
+ });
168
+
169
+ // If the browser already has it (e.g. via global CSS), skip work.
170
+ try {
171
+ // Use a weight-aware check so we can lazily load additional weights on demand.
172
+ const checkQuery = `${preferWeight} 16px "${name}"`;
173
+ const already = fontsApi.check(checkQuery);
174
+ fontsDebug.info('loadFontFamily: document.fonts.check', {
175
+ familyName: name,
176
+ query: checkQuery,
177
+ result: already,
178
+ });
179
+ const loadedFace = hasLoadedFace(name, preferWeight);
180
+ fontsDebug.info('loadFontFamily: hasLoadedFace', {
181
+ familyName: name,
182
+ weight: preferWeight,
183
+ result: loadedFace,
184
+ });
185
+ // Only skip when the face is actually loaded.
186
+ if (loadedFace) {
187
+ fontsDebug.info(
188
+ 'loadFontFamily: already available (document.fonts.check)',
189
+ {
190
+ familyName: name,
191
+ weight: preferWeight,
192
+ },
193
+ );
194
+ return;
195
+ }
196
+ } catch {
197
+ // ignore check failures; we'll try to load.
198
+ }
199
+
200
+ const forceFetch = options.forceFetch !== false;
201
+ let source: string | ArrayBuffer = `url(${url})`;
202
+ const urlSource = `url(${url})`;
203
+
204
+ if (forceFetch && typeof fetch === 'function') {
205
+ try {
206
+ fontsDebug.info('loadFontFamily: fetching font file', {
207
+ url,
208
+ familyName: name,
209
+ weight: preferWeight,
210
+ });
211
+ const res = await fetch(url, {
212
+ mode: 'cors',
213
+ cache: 'no-store',
214
+ credentials: 'omit',
215
+ });
216
+ fontsDebug.info('loadFontFamily: font fetch response', {
217
+ url,
218
+ ok: res.ok,
219
+ status: res.status,
220
+ statusText: res.statusText,
221
+ });
222
+ if (!res.ok) {
223
+ throw new Error(`Font fetch failed: ${res.status} ${res.statusText}`);
224
+ }
225
+ source = await res.arrayBuffer();
226
+ fontsDebug.info('loadFontFamily: font file downloaded', {
227
+ url,
228
+ bytes: (source as ArrayBuffer).byteLength,
229
+ familyName: name,
230
+ weight: preferWeight,
231
+ });
232
+ } catch (e) {
233
+ // Fallback to native font loading. This can still succeed in some environments where fetch is blocked.
234
+ fontsDebug.compactError(
235
+ 'loadFontFamily: fetch failed; falling back to url()',
236
+ e,
237
+ {
238
+ url,
239
+ familyName: name,
240
+ weight: preferWeight,
241
+ },
242
+ );
243
+ source = urlSource;
244
+ }
245
+ } else {
246
+ fontsDebug.info('loadFontFamily: using url() source (no fetch)', {
247
+ url,
248
+ familyName: name,
249
+ weight: preferWeight,
250
+ });
251
+ }
252
+
253
+ async function loadAndAdd(src: string | ArrayBuffer, label: string) {
254
+ const face = new (globalThis as any).FontFace(name, src as any, {
255
+ weight: preferWeight,
256
+ display: 'swap',
257
+ }) as FontFace;
258
+ fontsDebug.info('loadFontFamily: FontFace.load() begin', {
259
+ familyName: name,
260
+ weight: preferWeight,
261
+ source: label,
262
+ });
263
+ const loaded = await face.load();
264
+ safeFontsApi.add(loaded);
265
+ fontsDebug.info('loadFontFamily: loaded + added to document.fonts', {
266
+ familyName: name,
267
+ weight: preferWeight,
268
+ source: label,
269
+ });
270
+ }
271
+
272
+ try {
273
+ // Preferred path (buffer if we fetched, else url()).
274
+ await loadAndAdd(
275
+ source,
276
+ typeof source === 'string' ? 'url()' : 'arrayBuffer',
277
+ );
278
+ } catch (e) {
279
+ // Some browsers can be picky about ArrayBuffer sources. Retry using url() as a last resort.
280
+ fontsDebug.compactError('loadFontFamily: FontFace.load failed', e, {
281
+ familyName: name,
282
+ weight: preferWeight,
283
+ tried: typeof source === 'string' ? 'url()' : 'arrayBuffer',
284
+ });
285
+ if (typeof source !== 'string') {
286
+ fontsDebug.info('loadFontFamily: retrying with url() source', {
287
+ familyName: name,
288
+ weight: preferWeight,
289
+ url,
290
+ });
291
+ await loadAndAdd(urlSource, 'url() retry');
292
+ } else {
293
+ throw e;
294
+ }
295
+ }
296
+
297
+ try {
298
+ const afterQuery = `${preferWeight} 16px "${name}"`;
299
+ const after = safeFontsApi.check(afterQuery);
300
+ fontsDebug.info('loadFontFamily: document.fonts.check after add', {
301
+ familyName: name,
302
+ weight: preferWeight,
303
+ result: after,
304
+ });
305
+ // Force the browser to "activate" the face for this query (and reveal errors if any).
306
+ if (typeof (safeFontsApi as any).load === 'function') {
307
+ fontsDebug.info('loadFontFamily: document.fonts.load begin', {
308
+ query: afterQuery,
309
+ });
310
+ await (safeFontsApi as any).load(afterQuery);
311
+ fontsDebug.info('loadFontFamily: document.fonts.load done', {
312
+ query: afterQuery,
313
+ });
314
+ }
315
+ } catch {
316
+ // ignore
317
+ }
318
+ }
@@ -161,6 +161,8 @@ export function isPrimitiveType(typeName: string): boolean {
161
161
  typeName === 'boolean' ||
162
162
  typeName === 'color' ||
163
163
  typeName === 'size' ||
164
+ typeName === 'fontFamily' ||
165
+ typeName === 'fontWeight' ||
164
166
  typeName === 'iconType'
165
167
  );
166
168
  }