@developer_tribe/react-builder 1.2.0 → 1.2.2
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/dist/hooks/useExtractTextStyle.d.ts +9 -0
- package/dist/hooks/useExtractViewStyle.d.ts +9 -0
- package/dist/index.cjs.js +2 -2
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.esm.js +2 -2
- package/dist/index.esm.js.map +1 -1
- package/dist/index.native.cjs.js +1 -1
- package/dist/index.native.cjs.js.map +1 -1
- package/dist/index.native.d.ts +1 -0
- package/dist/index.native.esm.js +1 -1
- package/dist/index.native.esm.js.map +1 -1
- package/dist/pages/ProjectPage.d.ts +10 -3
- package/dist/types/Fonts.d.ts +5 -1
- package/dist/utils/extractTextStyle/extractTextStyle.d.ts +9 -0
- package/dist/utils/extractTextStyle.d.ts +2 -10
- package/dist/utils/extractViewStyle/extractViewStyle.d.ts +9 -0
- package/dist/utils/extractViewStyle.d.ts +2 -9
- package/package.json +1 -1
- package/src/build-components/BIcon/BIcon.tsx +5 -6
- package/src/build-components/BackgroundImage/BackgroundImage.tsx +2 -5
- package/src/build-components/Button/Button.tsx +20 -15
- package/src/build-components/Carousel/Carousel.tsx +5 -5
- package/src/build-components/CarouselButtons/CarouselButtons.tsx +3 -6
- package/src/build-components/CarouselDots/CarouselDots.tsx +3 -5
- package/src/build-components/CarouselItem/CarouselItem.tsx +3 -6
- package/src/build-components/CarouselProvider/CarouselProvider.tsx +2 -5
- package/src/build-components/Main/Main.tsx +5 -9
- package/src/build-components/OnboardButton/OnboardButton.tsx +5 -5
- package/src/build-components/OnboardButtons/OnboardButtons.tsx +6 -6
- package/src/build-components/OnboardDot/OnboardDot.tsx +8 -4
- package/src/build-components/OnboardFooter/OnboardFooter.tsx +10 -10
- package/src/build-components/OnboardImage/OnboardImage.tsx +6 -6
- package/src/build-components/OnboardItem/OnboardItem.tsx +5 -6
- package/src/build-components/OnboardProvider/OnboardProvider.tsx +13 -10
- package/src/build-components/PaywallBackground/PaywallBackground.tsx +2 -5
- package/src/build-components/PaywallCloseButton/PaywallCloseButton.tsx +5 -5
- package/src/build-components/PaywallOptions/PaywallOptionButton.tsx +2 -5
- package/src/build-components/PaywallProvider/PaywallProvider.tsx +5 -5
- package/src/build-components/PaywallSubscribeButton/PaywallSubscribeButton.tsx +5 -5
- package/src/build-components/RadioButton/RadioButton.tsx +5 -5
- package/src/build-components/Text/Text.tsx +2 -5
- package/src/build-components/View/View.tsx +3 -6
- package/src/hooks/useExtractTextStyle.ts +30 -0
- package/src/hooks/useExtractViewStyle.ts +28 -0
- package/src/index.native.ts +3 -0
- package/src/index.ts +2 -0
- package/src/pages/ProjectPage.tsx +15 -5
- package/src/types/Fonts.ts +5 -1
- package/src/utils/extractTextStyle/extractTextStyle.ts +160 -0
- package/src/utils/extractTextStyle.ts +2 -160
- package/src/utils/extractViewStyle/extractViewStyle.ts +145 -0
- package/src/utils/extractViewStyle.ts +2 -145
|
@@ -4,7 +4,7 @@ import RenderNode from '../RenderNode.generated';
|
|
|
4
4
|
import type { Node } from '../../types/Node';
|
|
5
5
|
import useNode from '../useNode';
|
|
6
6
|
import { useRenderStore } from '../../store';
|
|
7
|
-
import {
|
|
7
|
+
import { useExtractViewStyle } from '../../hooks/useExtractViewStyle';
|
|
8
8
|
import { useLogRender } from '../../utils/useLogRender';
|
|
9
9
|
import { isNodeSelected, SELECTED_OUTLINE_STYLE } from '../../utils/selection';
|
|
10
10
|
import { useMergedStyle } from '../../utils/useMergedStyle';
|
|
@@ -55,10 +55,10 @@ function PaywallProvider({ node }: PaywallProviderComponentProps) {
|
|
|
55
55
|
return params;
|
|
56
56
|
}, [benefits]);
|
|
57
57
|
|
|
58
|
-
const baseStyle =
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
);
|
|
58
|
+
const baseStyle = useExtractViewStyle(node as any, {
|
|
59
|
+
appConfig,
|
|
60
|
+
projectColors,
|
|
61
|
+
});
|
|
62
62
|
|
|
63
63
|
const isSelected = isNodeSelected({ previewMode, current, node });
|
|
64
64
|
const style = useMergedStyle(
|
|
@@ -3,7 +3,7 @@ import type { PaywallSubscribeButtonComponentProps } from './PaywallSubscribeBut
|
|
|
3
3
|
import useNode from '../useNode';
|
|
4
4
|
import { useRenderStore } from '../../store';
|
|
5
5
|
import { useLogRender } from '../../utils/useLogRender';
|
|
6
|
-
import {
|
|
6
|
+
import { useExtractTextStyle } from '../../hooks/useExtractTextStyle';
|
|
7
7
|
import { isNodeSelected, SELECTED_OUTLINE_STYLE } from '../../utils/selection';
|
|
8
8
|
import { useMergedStyle } from '../../utils/useMergedStyle';
|
|
9
9
|
import { useLocalize } from '../../hooks/useLocalize';
|
|
@@ -30,10 +30,10 @@ function PaywallSubscribeButton({
|
|
|
30
30
|
}),
|
|
31
31
|
);
|
|
32
32
|
|
|
33
|
-
const baseStyle =
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
);
|
|
33
|
+
const baseStyle = useExtractTextStyle(node as any, {
|
|
34
|
+
appConfig,
|
|
35
|
+
projectColors,
|
|
36
|
+
});
|
|
37
37
|
|
|
38
38
|
const isSelected = isNodeSelected({ previewMode, current, node });
|
|
39
39
|
const style = useMergedStyle(
|
|
@@ -2,7 +2,7 @@ import React, { useContext, useId, useMemo } from 'react';
|
|
|
2
2
|
import type { RadioButtonComponentProps } from './RadioButtonProps.generated';
|
|
3
3
|
import useNode from '../useNode';
|
|
4
4
|
import { useRenderStore } from '../../store';
|
|
5
|
-
import {
|
|
5
|
+
import { useExtractViewStyle } from '../../hooks/useExtractViewStyle';
|
|
6
6
|
import { useLogRender } from '../../utils/useLogRender';
|
|
7
7
|
import { isNodeSelected, SELECTED_OUTLINE_STYLE } from '../../utils/selection';
|
|
8
8
|
import { useMergedStyle } from '../../utils/useMergedStyle';
|
|
@@ -91,10 +91,10 @@ function RadioButton({ node }: RadioButtonComponentProps) {
|
|
|
91
91
|
}),
|
|
92
92
|
);
|
|
93
93
|
|
|
94
|
-
const baseStyle =
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
);
|
|
94
|
+
const baseStyle = useExtractViewStyle(node as any, {
|
|
95
|
+
appConfig,
|
|
96
|
+
projectColors,
|
|
97
|
+
});
|
|
98
98
|
|
|
99
99
|
const isSelected = isNodeSelected({ previewMode, current, node });
|
|
100
100
|
const style = useMergedStyle(
|
|
@@ -8,7 +8,7 @@ import React, {
|
|
|
8
8
|
import type { TextComponentProps } from './TextProps.generated';
|
|
9
9
|
import useNode from '../useNode';
|
|
10
10
|
import { useRenderStore } from '../../store';
|
|
11
|
-
import {
|
|
11
|
+
import { useExtractTextStyle } from '../../hooks/useExtractTextStyle';
|
|
12
12
|
import { useLogRender } from '../../utils/useLogRender';
|
|
13
13
|
import { isNodeSelected, SELECTED_OUTLINE_STYLE } from '../../utils/selection';
|
|
14
14
|
import { useMergedStyle } from '../../utils/useMergedStyle';
|
|
@@ -31,10 +31,7 @@ function Text({ node }: TextComponentProps) {
|
|
|
31
31
|
}),
|
|
32
32
|
);
|
|
33
33
|
const keyOrText: string = node.children as string;
|
|
34
|
-
const textStyle =
|
|
35
|
-
() => extractTextStyle(node, { appConfig, projectColors }),
|
|
36
|
-
[node, appConfig, projectColors],
|
|
37
|
-
);
|
|
34
|
+
const textStyle = useExtractTextStyle(node, { appConfig, projectColors });
|
|
38
35
|
const isSelected = isNodeSelected({ previewMode, current, node });
|
|
39
36
|
const localize = useLocalize();
|
|
40
37
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import React, { useId
|
|
1
|
+
import React, { useId } from 'react';
|
|
2
2
|
import type { ViewComponentProps } from './ViewProps.generated';
|
|
3
3
|
import RenderNode from '../RenderNode.generated';
|
|
4
4
|
import { Node } from '../../types/Node';
|
|
5
5
|
import useNode from '../useNode';
|
|
6
6
|
import { useRenderStore } from '../../store';
|
|
7
|
-
import {
|
|
7
|
+
import { useExtractViewStyle } from '../../hooks/useExtractViewStyle';
|
|
8
8
|
import { useLogRender } from '../../utils/useLogRender';
|
|
9
9
|
import { isNodeSelected, SELECTED_OUTLINE_STYLE } from '../../utils/selection';
|
|
10
10
|
import { useMergedStyle } from '../../utils/useMergedStyle';
|
|
@@ -23,10 +23,7 @@ export function View({ node }: ViewComponentProps) {
|
|
|
23
23
|
projectColors: s.projectColors,
|
|
24
24
|
}),
|
|
25
25
|
);
|
|
26
|
-
const baseStyle =
|
|
27
|
-
() => extractViewStyle(node, { appConfig, projectColors }),
|
|
28
|
-
[node, appConfig, projectColors],
|
|
29
|
-
);
|
|
26
|
+
const baseStyle = useExtractViewStyle(node, { appConfig, projectColors });
|
|
30
27
|
const isSelected = isNodeSelected({ previewMode, current, node });
|
|
31
28
|
const viewStyle = useMergedStyle(
|
|
32
29
|
baseStyle,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import type { NodeData } from '../types/Node';
|
|
3
|
+
import type { TextPropsGenerated } from '../build-components/Text/TextProps.generated';
|
|
4
|
+
import type { AppConfig } from '../types/PreviewConfig';
|
|
5
|
+
import type { ProjectColors } from '../types/Project';
|
|
6
|
+
import { useRenderStore } from '../store';
|
|
7
|
+
import { extractTextStyle } from '../utils/extractTextStyle';
|
|
8
|
+
|
|
9
|
+
export type UseExtractTextStyleOptions = {
|
|
10
|
+
appConfig?: AppConfig;
|
|
11
|
+
projectColors?: ProjectColors;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function useExtractTextStyle<T extends TextPropsGenerated['attributes']>(
|
|
15
|
+
node: NodeData<T>,
|
|
16
|
+
options: UseExtractTextStyleOptions = {},
|
|
17
|
+
) {
|
|
18
|
+
const storeAppConfig = useRenderStore((s) => s.appConfig);
|
|
19
|
+
const storeProjectColors = useRenderStore((s) => s.projectColors);
|
|
20
|
+
const fonts = useRenderStore((s) => s.fonts);
|
|
21
|
+
|
|
22
|
+
const appConfig = options.appConfig ?? storeAppConfig;
|
|
23
|
+
const projectColors = options.projectColors ?? storeProjectColors;
|
|
24
|
+
|
|
25
|
+
return useMemo(
|
|
26
|
+
() => extractTextStyle(node, { appConfig, projectColors }),
|
|
27
|
+
// fonts is intentionally included: extractTextStyle resolves weights via store-provided font definitions.
|
|
28
|
+
[node, appConfig, projectColors, fonts],
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import type { NodeData } from '../types/Node';
|
|
3
|
+
import type { ViewPropsGenerated } from '../build-components/View/ViewProps.generated';
|
|
4
|
+
import type { AppConfig } from '../types/PreviewConfig';
|
|
5
|
+
import type { ProjectColors } from '../types/Project';
|
|
6
|
+
import { useRenderStore } from '../store';
|
|
7
|
+
import { extractViewStyle } from '../utils/extractViewStyle';
|
|
8
|
+
|
|
9
|
+
export type UseExtractViewStyleOptions = {
|
|
10
|
+
appConfig?: AppConfig;
|
|
11
|
+
projectColors?: ProjectColors;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function useExtractViewStyle<T extends ViewPropsGenerated['attributes']>(
|
|
15
|
+
node: NodeData<T>,
|
|
16
|
+
options: UseExtractViewStyleOptions = {},
|
|
17
|
+
) {
|
|
18
|
+
const storeAppConfig = useRenderStore((s) => s.appConfig);
|
|
19
|
+
const storeProjectColors = useRenderStore((s) => s.projectColors);
|
|
20
|
+
|
|
21
|
+
const appConfig = options.appConfig ?? storeAppConfig;
|
|
22
|
+
const projectColors = options.projectColors ?? storeProjectColors;
|
|
23
|
+
|
|
24
|
+
return useMemo(
|
|
25
|
+
() => extractViewStyle(node, { appConfig, projectColors }),
|
|
26
|
+
[node, appConfig, projectColors],
|
|
27
|
+
);
|
|
28
|
+
}
|
package/src/index.native.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -12,6 +12,8 @@ export { RenderErrorBoundary } from './components/RenderErrorBoundary';
|
|
|
12
12
|
export { useParams } from './hooks/useParams';
|
|
13
13
|
export { useLocalizationParams } from './hooks/useLocalizationParams';
|
|
14
14
|
export { useLocalize } from './hooks/useLocalize';
|
|
15
|
+
export { useExtractTextStyle } from './hooks/useExtractTextStyle';
|
|
16
|
+
export { useExtractViewStyle } from './hooks/useExtractViewStyle';
|
|
15
17
|
|
|
16
18
|
export type { TargetedScreenSize } from './types/TargetedScreenSize';
|
|
17
19
|
export type { Node, NodeData, NodeDefaultAttribute } from './types/Node';
|
|
@@ -44,9 +44,15 @@ export type ProjectPageProps = {
|
|
|
44
44
|
projectColors?: ProjectColors;
|
|
45
45
|
onSaveProjectColors?: (colors: ProjectColors) => void;
|
|
46
46
|
name?: string;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
typography: {
|
|
48
|
+
fonts: Fonts;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Optional. If omitted, `ProjectPage` will infer it from `typography.fonts`:
|
|
52
|
+
* - first font with `isMain: true`
|
|
53
|
+
* - else first font in the list
|
|
54
|
+
*/
|
|
55
|
+
appFont?: string | undefined;
|
|
50
56
|
};
|
|
51
57
|
|
|
52
58
|
const MOBILE_BREAKPOINT = 1000;
|
|
@@ -59,12 +65,16 @@ export function ProjectPage({
|
|
|
59
65
|
projectColors,
|
|
60
66
|
onSaveProjectColors,
|
|
61
67
|
name,
|
|
62
|
-
|
|
68
|
+
typography,
|
|
63
69
|
appFont,
|
|
64
70
|
}: ProjectPageProps) {
|
|
65
71
|
useLogRender('ProjectPage');
|
|
66
72
|
useSyncHtmlThemeClass();
|
|
67
|
-
|
|
73
|
+
const resolvedAppFont =
|
|
74
|
+
appFont ??
|
|
75
|
+
typography.fonts.find((f) => f?.isMain)?.name ??
|
|
76
|
+
typography.fonts[0]?.name;
|
|
77
|
+
useProjectFonts({ fonts: typography.fonts, appFont: resolvedAppFont });
|
|
68
78
|
const resolvedName = name ?? project.name;
|
|
69
79
|
const resolvedProjectColors = projectColors ?? project.projectColors;
|
|
70
80
|
const isEmptyProjectData =
|
package/src/types/Fonts.ts
CHANGED
|
@@ -5,12 +5,16 @@ export type FontWeightKey = string;
|
|
|
5
5
|
export type FontFileUrl = string;
|
|
6
6
|
|
|
7
7
|
/** Map of font-weight -> font file URL. */
|
|
8
|
-
export type FontFamilySources = Record<FontWeightKey, FontFileUrl>;
|
|
8
|
+
export type FontFamilySources = Record<FontWeightKey, FontFileUrl | undefined>;
|
|
9
9
|
|
|
10
10
|
export type FontDefinition = {
|
|
11
11
|
name: string;
|
|
12
12
|
family: FontFamilySources;
|
|
13
13
|
projects?: string[];
|
|
14
|
+
/** Optional language target for this font (e.g. "ar"). */
|
|
15
|
+
targetLanguage?: string;
|
|
16
|
+
/** Optional marker to pick a default app font. */
|
|
17
|
+
isMain?: boolean;
|
|
14
18
|
};
|
|
15
19
|
|
|
16
20
|
export type Fonts = FontDefinition[];
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { NodeData } from '../../types/Node';
|
|
2
|
+
import type { TextPropsGenerated } from '../../build-components/Text/TextProps.generated';
|
|
3
|
+
import type { AppConfig } from '../../types/PreviewConfig';
|
|
4
|
+
import { defaultAppConfig } from '../../types/PreviewConfig';
|
|
5
|
+
import type { ProjectColors } from '../../types/Project';
|
|
6
|
+
import { fs, parseSize } from '../../size-matters';
|
|
7
|
+
import { parseColor } from '../parseColor';
|
|
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
|
+
}
|
|
71
|
+
|
|
72
|
+
export type ExtractTextStyleOptions = {
|
|
73
|
+
appConfig?: AppConfig;
|
|
74
|
+
projectColors?: ProjectColors;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export function extractTextStyle<T extends TextPropsGenerated['attributes']>(
|
|
78
|
+
node: NodeData<T>,
|
|
79
|
+
options: ExtractTextStyleOptions = {},
|
|
80
|
+
) {
|
|
81
|
+
const attributes = node.attributes;
|
|
82
|
+
const styleBag = (attributes as any)?.style as
|
|
83
|
+
| Record<string, unknown>
|
|
84
|
+
| undefined;
|
|
85
|
+
const get = <K extends keyof TextPropsGenerated['attributes']>(
|
|
86
|
+
key: K,
|
|
87
|
+
): TextPropsGenerated['attributes'][K] | undefined => {
|
|
88
|
+
const direct = (attributes as any)?.[key];
|
|
89
|
+
if (direct !== undefined && direct !== null) return direct;
|
|
90
|
+
return styleBag?.[key as unknown as string] as any;
|
|
91
|
+
};
|
|
92
|
+
const resolvedAppConfig = options.appConfig ?? defaultAppConfig;
|
|
93
|
+
const { screenStyle, theme } = resolvedAppConfig;
|
|
94
|
+
const fallbackColor =
|
|
95
|
+
theme === 'light' ? screenStyle.light.color : screenStyle.dark.color;
|
|
96
|
+
|
|
97
|
+
const style: React.CSSProperties = {};
|
|
98
|
+
|
|
99
|
+
if (!attributes) {
|
|
100
|
+
style.fontSize = fs(14);
|
|
101
|
+
style.color = fallbackColor;
|
|
102
|
+
return style;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Typography
|
|
106
|
+
const fontSize = get('fontSize') as any;
|
|
107
|
+
if (fontSize !== undefined) {
|
|
108
|
+
const parsed = parseSize(fontSize);
|
|
109
|
+
style.fontSize = parsed as React.CSSProperties['fontSize'];
|
|
110
|
+
} else {
|
|
111
|
+
style.fontSize = fs(14);
|
|
112
|
+
}
|
|
113
|
+
const fontFamily = get('fontFamily') as any;
|
|
114
|
+
const fontWeight = get('fontWeight') as any;
|
|
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;
|
|
150
|
+
const resolvedTextColor = parseColor(get('color') as any, {
|
|
151
|
+
projectColors: options.projectColors,
|
|
152
|
+
appConfig: resolvedAppConfig,
|
|
153
|
+
});
|
|
154
|
+
style.color = resolvedTextColor ?? fallbackColor;
|
|
155
|
+
const textAlign = get('textAlign') as any;
|
|
156
|
+
if (textAlign)
|
|
157
|
+
style.textAlign = textAlign as React.CSSProperties['textAlign'];
|
|
158
|
+
|
|
159
|
+
return { ...extractViewStyle(node, options), ...style };
|
|
160
|
+
}
|
|
@@ -1,160 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import type { AppConfig } from '../types/PreviewConfig';
|
|
4
|
-
import { defaultAppConfig } from '../types/PreviewConfig';
|
|
5
|
-
import type { ProjectColors } from '../types/Project';
|
|
6
|
-
import { fs, parseSize } from '../size-matters';
|
|
7
|
-
import { parseColor } from './parseColor';
|
|
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
|
-
}
|
|
71
|
-
|
|
72
|
-
type ExtractTextStyleOptions = {
|
|
73
|
-
appConfig?: AppConfig;
|
|
74
|
-
projectColors?: ProjectColors;
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export function extractTextStyle<T extends TextPropsGenerated['attributes']>(
|
|
78
|
-
node: NodeData<T>,
|
|
79
|
-
options: ExtractTextStyleOptions = {},
|
|
80
|
-
) {
|
|
81
|
-
const attributes = node.attributes;
|
|
82
|
-
const styleBag = (attributes as any)?.style as
|
|
83
|
-
| Record<string, unknown>
|
|
84
|
-
| undefined;
|
|
85
|
-
const get = <K extends keyof TextPropsGenerated['attributes']>(
|
|
86
|
-
key: K,
|
|
87
|
-
): TextPropsGenerated['attributes'][K] | undefined => {
|
|
88
|
-
const direct = (attributes as any)?.[key];
|
|
89
|
-
if (direct !== undefined && direct !== null) return direct;
|
|
90
|
-
return styleBag?.[key as unknown as string] as any;
|
|
91
|
-
};
|
|
92
|
-
const resolvedAppConfig = options.appConfig ?? defaultAppConfig;
|
|
93
|
-
const { screenStyle, theme } = resolvedAppConfig;
|
|
94
|
-
const fallbackColor =
|
|
95
|
-
theme === 'light' ? screenStyle.light.color : screenStyle.dark.color;
|
|
96
|
-
|
|
97
|
-
const style: React.CSSProperties = {};
|
|
98
|
-
|
|
99
|
-
if (!attributes) {
|
|
100
|
-
style.fontSize = fs(14);
|
|
101
|
-
style.color = fallbackColor;
|
|
102
|
-
return style;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Typography
|
|
106
|
-
const fontSize = get('fontSize') as any;
|
|
107
|
-
if (fontSize !== undefined) {
|
|
108
|
-
const parsed = parseSize(fontSize);
|
|
109
|
-
style.fontSize = parsed as React.CSSProperties['fontSize'];
|
|
110
|
-
} else {
|
|
111
|
-
style.fontSize = fs(14);
|
|
112
|
-
}
|
|
113
|
-
const fontFamily = get('fontFamily') as any;
|
|
114
|
-
const fontWeight = get('fontWeight') as any;
|
|
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;
|
|
150
|
-
const resolvedTextColor = parseColor(get('color') as any, {
|
|
151
|
-
projectColors: options.projectColors,
|
|
152
|
-
appConfig: resolvedAppConfig,
|
|
153
|
-
});
|
|
154
|
-
style.color = resolvedTextColor ?? fallbackColor;
|
|
155
|
-
const textAlign = get('textAlign') as any;
|
|
156
|
-
if (textAlign)
|
|
157
|
-
style.textAlign = textAlign as React.CSSProperties['textAlign'];
|
|
158
|
-
|
|
159
|
-
return { ...extractViewStyle(node, options), ...style };
|
|
160
|
-
}
|
|
1
|
+
export type { ExtractTextStyleOptions } from './extractTextStyle/extractTextStyle';
|
|
2
|
+
export { extractTextStyle } from './extractTextStyle/extractTextStyle';
|