@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
@@ -7,19 +7,8 @@
7
7
  "attributes": {
8
8
  "color": "color",
9
9
  "fontSize": "size",
10
- "fontWeight": [
11
- "normal",
12
- "bold",
13
- "100",
14
- "200",
15
- "300",
16
- "400",
17
- "500",
18
- "600",
19
- "700",
20
- "800",
21
- "900"
22
- ],
10
+ "fontFamily": "fontFamily",
11
+ "fontWeight": "fontWeight",
23
12
  "textAlign": ["left", "center", "right", "justify"],
24
13
  "adjustsFontSizeToFit": "boolean",
25
14
  "showEllipsis": "boolean"
@@ -45,33 +34,40 @@
45
34
  "sort": 2,
46
35
  "preferedScale": "s"
47
36
  },
37
+ "fontFamily": {
38
+ "label": "Font Family",
39
+ "description": "Font family used for the text.",
40
+ "category": "style",
41
+ "specialCategory": null,
42
+ "sort": 3
43
+ },
48
44
  "fontWeight": {
49
45
  "label": "Font Weight",
50
46
  "description": "Text weight.",
51
47
  "category": "style",
52
48
  "specialCategory": null,
53
- "sort": 3
49
+ "sort": 4
54
50
  },
55
51
  "textAlign": {
56
52
  "label": "Text Align",
57
53
  "description": "Text alignment.",
58
54
  "category": "style",
59
55
  "specialCategory": null,
60
- "sort": 4
56
+ "sort": 5
61
57
  },
62
58
  "adjustsFontSizeToFit": {
63
59
  "label": "Adjust Font Size To Fit",
64
60
  "description": "Automatically reduces font size to fit the available space.",
65
61
  "category": "style",
66
62
  "specialCategory": null,
67
- "sort": 5
63
+ "sort": 6
68
64
  },
69
65
  "showEllipsis": {
70
66
  "label": "Show Ellipsis",
71
67
  "description": "If text overflows, show ellipsis (…); applied as single-line truncation.",
72
68
  "category": "style",
73
69
  "specialCategory": null,
74
- "sort": 6
70
+ "sort": 7
75
71
  }
76
72
  }
77
73
  }
@@ -10,19 +10,8 @@ export const patterns = [
10
10
  attributes: {
11
11
  color: 'color',
12
12
  fontSize: 'size',
13
- fontWeight: [
14
- 'normal',
15
- 'bold',
16
- '100',
17
- '200',
18
- '300',
19
- '400',
20
- '500',
21
- '600',
22
- '700',
23
- '800',
24
- '900',
25
- ],
13
+ fontFamily: 'fontFamily',
14
+ fontWeight: 'fontWeight',
26
15
  textAlign: ['left', 'center', 'right', 'justify'],
27
16
  adjustsFontSizeToFit: 'boolean',
28
17
  showEllipsis: 'boolean',
@@ -91,19 +80,26 @@ export const patterns = [
91
80
  sort: 2,
92
81
  preferedScale: 's',
93
82
  },
83
+ fontFamily: {
84
+ label: 'Font Family',
85
+ description: 'Font family used for the text.',
86
+ category: 'style',
87
+ specialCategory: null,
88
+ sort: 3,
89
+ },
94
90
  fontWeight: {
95
91
  label: 'Font Weight',
96
92
  description: 'Text weight.',
97
93
  category: 'style',
98
94
  specialCategory: null,
99
- sort: 3,
95
+ sort: 4,
100
96
  },
101
97
  textAlign: {
102
98
  label: 'Text Align',
103
99
  description: 'Text alignment.',
104
100
  category: 'style',
105
101
  specialCategory: null,
106
- sort: 4,
102
+ sort: 5,
107
103
  },
108
104
  adjustsFontSizeToFit: {
109
105
  label: 'Adjust Font Size To Fit',
@@ -111,7 +107,7 @@ export const patterns = [
111
107
  'Automatically reduces font size to fit the available space.',
112
108
  category: 'style',
113
109
  specialCategory: null,
114
- sort: 5,
110
+ sort: 6,
115
111
  },
116
112
  showEllipsis: {
117
113
  label: 'Show Ellipsis',
@@ -119,7 +115,7 @@ export const patterns = [
119
115
  'If text overflows, show ellipsis (…); applied as single-line truncation.',
120
116
  category: 'style',
121
117
  specialCategory: null,
122
- sort: 6,
118
+ sort: 7,
123
119
  },
124
120
  scrollable: {
125
121
  label: 'Scrollable',
@@ -5294,19 +5290,8 @@ export const patterns = [
5294
5290
  attributes: {
5295
5291
  color: 'color',
5296
5292
  fontSize: 'size',
5297
- fontWeight: [
5298
- 'normal',
5299
- 'bold',
5300
- '100',
5301
- '200',
5302
- '300',
5303
- '400',
5304
- '500',
5305
- '600',
5306
- '700',
5307
- '800',
5308
- '900',
5309
- ],
5293
+ fontFamily: 'fontFamily',
5294
+ fontWeight: 'fontWeight',
5310
5295
  textAlign: ['left', 'center', 'right', 'justify'],
5311
5296
  adjustsFontSizeToFit: 'boolean',
5312
5297
  showEllipsis: 'boolean',
@@ -5379,19 +5364,26 @@ export const patterns = [
5379
5364
  sort: 2,
5380
5365
  preferedScale: 's',
5381
5366
  },
5367
+ fontFamily: {
5368
+ label: 'Font Family',
5369
+ description: 'Font family used for the text.',
5370
+ category: 'style',
5371
+ specialCategory: null,
5372
+ sort: 3,
5373
+ },
5382
5374
  fontWeight: {
5383
5375
  label: 'Font Weight',
5384
5376
  description: 'Text weight.',
5385
5377
  category: 'style',
5386
5378
  specialCategory: null,
5387
- sort: 3,
5379
+ sort: 4,
5388
5380
  },
5389
5381
  textAlign: {
5390
5382
  label: 'Text Align',
5391
5383
  description: 'Text alignment.',
5392
5384
  category: 'style',
5393
5385
  specialCategory: null,
5394
- sort: 4,
5386
+ sort: 5,
5395
5387
  },
5396
5388
  adjustsFontSizeToFit: {
5397
5389
  label: 'Adjust Font Size To Fit',
@@ -5399,7 +5391,7 @@ export const patterns = [
5399
5391
  'Automatically reduces font size to fit the available space.',
5400
5392
  category: 'style',
5401
5393
  specialCategory: null,
5402
- sort: 5,
5394
+ sort: 6,
5403
5395
  },
5404
5396
  showEllipsis: {
5405
5397
  label: 'Show Ellipsis',
@@ -5407,7 +5399,7 @@ export const patterns = [
5407
5399
  'If text overflows, show ellipsis (…); applied as single-line truncation.',
5408
5400
  category: 'style',
5409
5401
  specialCategory: null,
5410
- sort: 6,
5402
+ sort: 7,
5411
5403
  },
5412
5404
  scrollable: {
5413
5405
  label: 'Scrollable',
@@ -6874,19 +6866,8 @@ export const patterns = [
6874
6866
  attributes: {
6875
6867
  color: 'color',
6876
6868
  fontSize: 'size',
6877
- fontWeight: [
6878
- 'normal',
6879
- 'bold',
6880
- '100',
6881
- '200',
6882
- '300',
6883
- '400',
6884
- '500',
6885
- '600',
6886
- '700',
6887
- '800',
6888
- '900',
6889
- ],
6869
+ fontFamily: 'fontFamily',
6870
+ fontWeight: 'fontWeight',
6890
6871
  textAlign: ['left', 'center', 'right', 'justify'],
6891
6872
  adjustsFontSizeToFit: 'boolean',
6892
6873
  showEllipsis: 'boolean',
@@ -6952,19 +6933,26 @@ export const patterns = [
6952
6933
  sort: 2,
6953
6934
  preferedScale: 's',
6954
6935
  },
6936
+ fontFamily: {
6937
+ label: 'Font Family',
6938
+ description: 'Font family used for the text.',
6939
+ category: 'style',
6940
+ specialCategory: null,
6941
+ sort: 3,
6942
+ },
6955
6943
  fontWeight: {
6956
6944
  label: 'Font Weight',
6957
6945
  description: 'Text weight.',
6958
6946
  category: 'style',
6959
6947
  specialCategory: null,
6960
- sort: 3,
6948
+ sort: 4,
6961
6949
  },
6962
6950
  textAlign: {
6963
6951
  label: 'Text Align',
6964
6952
  description: 'Text alignment.',
6965
6953
  category: 'style',
6966
6954
  specialCategory: null,
6967
- sort: 4,
6955
+ sort: 5,
6968
6956
  },
6969
6957
  adjustsFontSizeToFit: {
6970
6958
  label: 'Adjust Font Size To Fit',
@@ -6972,7 +6960,7 @@ export const patterns = [
6972
6960
  'Automatically reduces font size to fit the available space.',
6973
6961
  category: 'style',
6974
6962
  specialCategory: null,
6975
- sort: 5,
6963
+ sort: 6,
6976
6964
  },
6977
6965
  showEllipsis: {
6978
6966
  label: 'Show Ellipsis',
@@ -6980,7 +6968,7 @@ export const patterns = [
6980
6968
  'If text overflows, show ellipsis (…); applied as single-line truncation.',
6981
6969
  category: 'style',
6982
6970
  specialCategory: null,
6983
- sort: 6,
6971
+ sort: 7,
6984
6972
  },
6985
6973
  scrollable: {
6986
6974
  label: 'Scrollable',
@@ -7293,19 +7281,8 @@ export const patterns = [
7293
7281
  attributes: {
7294
7282
  color: 'color',
7295
7283
  fontSize: 'size',
7296
- fontWeight: [
7297
- 'normal',
7298
- 'bold',
7299
- '100',
7300
- '200',
7301
- '300',
7302
- '400',
7303
- '500',
7304
- '600',
7305
- '700',
7306
- '800',
7307
- '900',
7308
- ],
7284
+ fontFamily: 'fontFamily',
7285
+ fontWeight: 'fontWeight',
7309
7286
  textAlign: ['left', 'center', 'right', 'justify'],
7310
7287
  adjustsFontSizeToFit: 'boolean',
7311
7288
  showEllipsis: 'boolean',
@@ -7371,19 +7348,26 @@ export const patterns = [
7371
7348
  sort: 2,
7372
7349
  preferedScale: 's',
7373
7350
  },
7351
+ fontFamily: {
7352
+ label: 'Font Family',
7353
+ description: 'Font family used for the text.',
7354
+ category: 'style',
7355
+ specialCategory: null,
7356
+ sort: 3,
7357
+ },
7374
7358
  fontWeight: {
7375
7359
  label: 'Font Weight',
7376
7360
  description: 'Text weight.',
7377
7361
  category: 'style',
7378
7362
  specialCategory: null,
7379
- sort: 3,
7363
+ sort: 4,
7380
7364
  },
7381
7365
  textAlign: {
7382
7366
  label: 'Text Align',
7383
7367
  description: 'Text alignment.',
7384
7368
  category: 'style',
7385
7369
  specialCategory: null,
7386
- sort: 4,
7370
+ sort: 5,
7387
7371
  },
7388
7372
  adjustsFontSizeToFit: {
7389
7373
  label: 'Adjust Font Size To Fit',
@@ -7391,7 +7375,7 @@ export const patterns = [
7391
7375
  'Automatically reduces font size to fit the available space.',
7392
7376
  category: 'style',
7393
7377
  specialCategory: null,
7394
- sort: 5,
7378
+ sort: 6,
7395
7379
  },
7396
7380
  showEllipsis: {
7397
7381
  label: 'Show Ellipsis',
@@ -7399,7 +7383,7 @@ export const patterns = [
7399
7383
  'If text overflows, show ellipsis (…); applied as single-line truncation.',
7400
7384
  category: 'style',
7401
7385
  specialCategory: null,
7402
- sort: 6,
7386
+ sort: 7,
7403
7387
  },
7404
7388
  scrollable: {
7405
7389
  label: 'Scrollable',
@@ -8066,19 +8050,8 @@ export const patterns = [
8066
8050
  strokeWidth: 'number',
8067
8051
  color: 'color',
8068
8052
  fontSize: 'size',
8069
- fontWeight: [
8070
- 'normal',
8071
- 'bold',
8072
- '100',
8073
- '200',
8074
- '300',
8075
- '400',
8076
- '500',
8077
- '600',
8078
- '700',
8079
- '800',
8080
- '900',
8081
- ],
8053
+ fontFamily: 'fontFamily',
8054
+ fontWeight: 'fontWeight',
8082
8055
  textAlign: ['left', 'center', 'right', 'justify'],
8083
8056
  adjustsFontSizeToFit: 'boolean',
8084
8057
  showEllipsis: 'boolean',
@@ -8165,19 +8138,26 @@ export const patterns = [
8165
8138
  sort: 2,
8166
8139
  preferedScale: 's',
8167
8140
  },
8141
+ fontFamily: {
8142
+ label: 'Font Family',
8143
+ description: 'Font family used for the text.',
8144
+ category: 'style',
8145
+ specialCategory: null,
8146
+ sort: 3,
8147
+ },
8168
8148
  fontWeight: {
8169
8149
  label: 'Font Weight',
8170
8150
  description: 'Text weight.',
8171
8151
  category: 'style',
8172
8152
  specialCategory: null,
8173
- sort: 3,
8153
+ sort: 4,
8174
8154
  },
8175
8155
  textAlign: {
8176
8156
  label: 'Text Align',
8177
8157
  description: 'Text alignment.',
8178
8158
  category: 'style',
8179
8159
  specialCategory: null,
8180
- sort: 4,
8160
+ sort: 5,
8181
8161
  },
8182
8162
  adjustsFontSizeToFit: {
8183
8163
  label: 'Adjust Font Size To Fit',
@@ -8185,7 +8165,7 @@ export const patterns = [
8185
8165
  'Automatically reduces font size to fit the available space.',
8186
8166
  category: 'style',
8187
8167
  specialCategory: null,
8188
- sort: 5,
8168
+ sort: 6,
8189
8169
  },
8190
8170
  showEllipsis: {
8191
8171
  label: 'Show Ellipsis',
@@ -8193,7 +8173,7 @@ export const patterns = [
8193
8173
  'If text overflows, show ellipsis (…); applied as single-line truncation.',
8194
8174
  category: 'style',
8195
8175
  specialCategory: null,
8196
- sort: 6,
8176
+ sort: 7,
8197
8177
  },
8198
8178
  scrollable: {
8199
8179
  label: 'Scrollable',
@@ -10038,19 +10018,8 @@ export const patterns = [
10038
10018
  zIndex: 'number',
10039
10019
  color: 'color',
10040
10020
  fontSize: 'size',
10041
- fontWeight: [
10042
- 'normal',
10043
- 'bold',
10044
- '100',
10045
- '200',
10046
- '300',
10047
- '400',
10048
- '500',
10049
- '600',
10050
- '700',
10051
- '800',
10052
- '900',
10053
- ],
10021
+ fontFamily: 'fontFamily',
10022
+ fontWeight: 'fontWeight',
10054
10023
  textAlign: ['left', 'center', 'right', 'justify'],
10055
10024
  adjustsFontSizeToFit: 'boolean',
10056
10025
  showEllipsis: 'boolean',
@@ -10366,19 +10335,26 @@ export const patterns = [
10366
10335
  sort: 2,
10367
10336
  preferedScale: 's',
10368
10337
  },
10338
+ fontFamily: {
10339
+ label: 'Font Family',
10340
+ description: 'Font family used for the text.',
10341
+ category: 'style',
10342
+ specialCategory: null,
10343
+ sort: 3,
10344
+ },
10369
10345
  fontWeight: {
10370
10346
  label: 'Font Weight',
10371
10347
  description: 'Text weight.',
10372
10348
  category: 'style',
10373
10349
  specialCategory: null,
10374
- sort: 3,
10350
+ sort: 4,
10375
10351
  },
10376
10352
  textAlign: {
10377
10353
  label: 'Text Align',
10378
10354
  description: 'Text alignment.',
10379
10355
  category: 'style',
10380
10356
  specialCategory: null,
10381
- sort: 4,
10357
+ sort: 5,
10382
10358
  },
10383
10359
  adjustsFontSizeToFit: {
10384
10360
  label: 'Adjust Font Size To Fit',
@@ -10386,7 +10362,7 @@ export const patterns = [
10386
10362
  'Automatically reduces font size to fit the available space.',
10387
10363
  category: 'style',
10388
10364
  specialCategory: null,
10389
- sort: 5,
10365
+ sort: 6,
10390
10366
  },
10391
10367
  showEllipsis: {
10392
10368
  label: 'Show Ellipsis',
@@ -10394,7 +10370,7 @@ export const patterns = [
10394
10370
  'If text overflows, show ellipsis (…); applied as single-line truncation.',
10395
10371
  category: 'style',
10396
10372
  specialCategory: null,
10397
- sort: 6,
10373
+ sort: 7,
10398
10374
  },
10399
10375
  },
10400
10376
  },
@@ -19,14 +19,8 @@ export function BuilderButton({
19
19
  onMoveUp,
20
20
  onMoveDown,
21
21
  }: BuilderButtonProps) {
22
- if (isNodeNullOrUndefined(node)) {
23
- return <div className="builder__placeholder">Null or undefined</div>;
24
- }
25
- if (isNodeString(node)) {
26
- return <div className="builder__text">{node as string}</div>;
27
- }
28
- const nodeData = node as NodeData<NodeDefaultAttribute>;
29
-
22
+ // IMPORTANT: Hooks must be called unconditionally on every render.
23
+ // (Early returns before hooks can trigger React internal invariants.)
30
24
  const [isMenuOpen, setIsMenuOpen] = useState(false);
31
25
  const actionsRef = useRef<HTMLDivElement | null>(null);
32
26
  const menuId = useMemo(
@@ -35,9 +29,9 @@ export function BuilderButton({
35
29
  );
36
30
 
37
31
  const handleDelete = () => {
38
- if (onDelete) {
39
- onDelete(node);
40
- }
32
+ if (!onDelete) return;
33
+ if (isNodeNullOrUndefined(node) || isNodeString(node)) return;
34
+ onDelete(node);
41
35
  };
42
36
 
43
37
  // Copy/Paste intentionally removed for now.
@@ -66,6 +60,14 @@ export function BuilderButton({
66
60
  };
67
61
  }, [isMenuOpen]);
68
62
 
63
+ if (isNodeNullOrUndefined(node)) {
64
+ return <div className="builder__placeholder">Null or undefined</div>;
65
+ }
66
+ if (isNodeString(node)) {
67
+ return <div className="builder__text">{node as string}</div>;
68
+ }
69
+
70
+ const nodeData = node as NodeData<NodeDefaultAttribute>;
69
71
  let extra = '';
70
72
  if (nodeData.attributes?.condition) {
71
73
  extra = ` (${nodeData.attributes.condition} ${nodeData.attributes.conditionVariable})`;
@@ -0,0 +1,130 @@
1
+ import { useEffect } from 'react';
2
+ import type { Fonts } from '../types/Fonts';
3
+ import { useRenderStore } from '../store';
4
+ import { loadFontFamily } from '../utils/loadFontFamily';
5
+ import { fontsDebug } from '../utils/fontsDebug';
6
+
7
+ function sleep(ms: number): Promise<void> {
8
+ return new Promise((resolve) => setTimeout(resolve, ms));
9
+ }
10
+
11
+ async function loadWithDeadline<T>(
12
+ task: Promise<T>,
13
+ deadlineMs: number,
14
+ ): Promise<T> {
15
+ if (!Number.isFinite(deadlineMs) || deadlineMs <= 0) {
16
+ return await task;
17
+ }
18
+ return (await Promise.race([
19
+ task,
20
+ sleep(deadlineMs).then(() => {
21
+ throw new Error('timeout');
22
+ }),
23
+ ])) as T;
24
+ }
25
+
26
+ type UseProjectFontsParams = {
27
+ fonts: Fonts;
28
+ appFont: string | undefined;
29
+ };
30
+
31
+ /**
32
+ * Syncs project-provided fonts into the library store and ensures appFont is loaded (web).
33
+ * Loading behavior:
34
+ * - keep waiting while the load is in-flight
35
+ * - stop only when it loads, an error is thrown, or 10s deadline passes
36
+ */
37
+ export function useProjectFonts({ fonts, appFont }: UseProjectFontsParams) {
38
+ useEffect(() => {
39
+ fontsDebug.info('useProjectFonts: effect start', {
40
+ appFont,
41
+ fontsCount: Array.isArray(fonts) ? fonts.length : 'not-array',
42
+ });
43
+ const {
44
+ setFonts,
45
+ setAppFont,
46
+ setErrors,
47
+ addError,
48
+ loadedFonts,
49
+ markFontLoaded,
50
+ } = useRenderStore.getState();
51
+
52
+ fontsDebug.info('useProjectFonts: storing fonts/appFont in store', {
53
+ appFont,
54
+ fontsCount: Array.isArray(fonts) ? fonts.length : 'not-array',
55
+ });
56
+ setFonts(fonts);
57
+ setAppFont(appFont);
58
+ setErrors([]);
59
+
60
+ let cancelled = false;
61
+
62
+ const run = async () => {
63
+ const normalizedAppFont =
64
+ typeof appFont === 'string' ? appFont.trim() : '';
65
+ if (!normalizedAppFont) {
66
+ fontsDebug.warn('useProjectFonts: appFont missing/empty');
67
+ setErrors(['appFont is undefined']);
68
+ return;
69
+ }
70
+
71
+ if (cancelled) return;
72
+
73
+ const exists =
74
+ Array.isArray(fonts) &&
75
+ fonts.some(
76
+ (f) =>
77
+ f?.name === normalizedAppFont &&
78
+ f?.family &&
79
+ typeof f.family === 'object' &&
80
+ Object.keys(f.family).length > 0,
81
+ );
82
+
83
+ if (!exists) {
84
+ fontsDebug.warn('useProjectFonts: appFont not found in fonts', {
85
+ normalizedAppFont,
86
+ });
87
+ setErrors([`appFont "${normalizedAppFont}" not found in fonts`]);
88
+ return;
89
+ }
90
+
91
+ if (
92
+ Array.isArray(loadedFonts) &&
93
+ loadedFonts.includes(normalizedAppFont)
94
+ ) {
95
+ fontsDebug.info(
96
+ 'useProjectFonts: appFont already loaded (store cache)',
97
+ {
98
+ normalizedAppFont,
99
+ },
100
+ );
101
+ return;
102
+ }
103
+
104
+ fontsDebug.info('useProjectFonts: loading appFont', {
105
+ normalizedAppFont,
106
+ deadlineMs: 10_000,
107
+ });
108
+ await loadWithDeadline(
109
+ loadFontFamily(fonts, normalizedAppFont, { forceFetch: true }),
110
+ 10_000,
111
+ );
112
+ if (cancelled) return;
113
+ markFontLoaded(normalizedAppFont);
114
+ fontsDebug.info('useProjectFonts: appFont loaded', { normalizedAppFont });
115
+ };
116
+
117
+ void run().catch((e) => {
118
+ if (cancelled) return;
119
+ fontsDebug.compactError('useProjectFonts: failed', e, { appFont });
120
+ addError(
121
+ `Failed to initialize fonts: ${e instanceof Error ? e.message : String(e)}`,
122
+ );
123
+ });
124
+
125
+ return () => {
126
+ fontsDebug.info('useProjectFonts: cleanup');
127
+ cancelled = true;
128
+ };
129
+ }, [fonts, appFont]);
130
+ }
package/src/index.ts CHANGED
@@ -16,6 +16,7 @@ export { useLocalize } from './hooks/useLocalize';
16
16
  export type { TargetedScreenSize } from './types/TargetedScreenSize';
17
17
  export type { Node, NodeData, NodeDefaultAttribute } from './types/Node';
18
18
  export type { Project, ProjectColors } from './types/Project';
19
+ export type { Fonts, FontDefinition } from './types/Fonts';
19
20
  export {
20
21
  isNodeNullOrUndefined,
21
22
  isNodeString,
@@ -34,6 +34,8 @@ import {
34
34
  isNodeRecord,
35
35
  nodeHasChild,
36
36
  } from '../utils/nodeTree';
37
+ import type { Fonts } from '../types/Fonts';
38
+ import { useProjectFonts } from '../hooks/useProjectFonts';
37
39
  export type ProjectPageProps = {
38
40
  project: Project;
39
41
  onSaveProject: (project: Project) => void;
@@ -42,6 +44,9 @@ export type ProjectPageProps = {
42
44
  projectColors?: ProjectColors;
43
45
  onSaveProjectColors?: (colors: ProjectColors) => void;
44
46
  name?: string;
47
+ fonts: Fonts;
48
+ // NOTE: appFont is required as a prop to force the host to think about it.
49
+ appFont: string | undefined;
45
50
  };
46
51
 
47
52
  const MOBILE_BREAKPOINT = 1000;
@@ -54,9 +59,12 @@ export function ProjectPage({
54
59
  projectColors,
55
60
  onSaveProjectColors,
56
61
  name,
62
+ fonts,
63
+ appFont,
57
64
  }: ProjectPageProps) {
58
65
  useLogRender('ProjectPage');
59
66
  useSyncHtmlThemeClass();
67
+ useProjectFonts({ fonts, appFont });
60
68
  const resolvedName = name ?? project.name;
61
69
  const resolvedProjectColors = projectColors ?? project.projectColors;
62
70
  const isEmptyProjectData =