@developer_tribe/react-builder 1.2.1 → 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/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/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
|
@@ -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';
|
|
@@ -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';
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { ViewPropsGenerated } from '../../build-components/View/ViewProps.generated';
|
|
2
|
+
import type { NodeData } from '../../types/Node';
|
|
3
|
+
import type { AppConfig } from '../../types/PreviewConfig';
|
|
4
|
+
import type { ProjectColors } from '../../types/Project';
|
|
5
|
+
import { parseSize } from '../../size-matters';
|
|
6
|
+
import { parseColor } from '../parseColor';
|
|
7
|
+
|
|
8
|
+
export type ExtractViewStyleOptions = {
|
|
9
|
+
appConfig?: AppConfig;
|
|
10
|
+
projectColors?: ProjectColors;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function extractViewStyle<T extends ViewPropsGenerated['attributes']>(
|
|
14
|
+
node: NodeData<T>,
|
|
15
|
+
options: ExtractViewStyleOptions = {},
|
|
16
|
+
) {
|
|
17
|
+
const attributes = node.attributes;
|
|
18
|
+
const styleBag = (attributes as any)?.style as
|
|
19
|
+
| Record<string, unknown>
|
|
20
|
+
| undefined;
|
|
21
|
+
const get = <K extends keyof ViewPropsGenerated['attributes']>(
|
|
22
|
+
key: K,
|
|
23
|
+
): ViewPropsGenerated['attributes'][K] | undefined => {
|
|
24
|
+
const direct = (attributes as any)?.[key];
|
|
25
|
+
if (direct !== undefined && direct !== null) return direct;
|
|
26
|
+
return styleBag?.[key as unknown as string] as any;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const scrollable = (get('scrollable') as any) ?? false;
|
|
30
|
+
const style: React.CSSProperties = {
|
|
31
|
+
display: 'flex',
|
|
32
|
+
flexDirection: 'column',
|
|
33
|
+
};
|
|
34
|
+
if (!attributes) return style;
|
|
35
|
+
const isEmptySizeValue = (value: unknown) =>
|
|
36
|
+
value === undefined ||
|
|
37
|
+
value === null ||
|
|
38
|
+
(typeof value === 'string' && value.trim() === '');
|
|
39
|
+
if (scrollable) {
|
|
40
|
+
// Important for flex children: allow the element to shrink so overflow scroll can work.
|
|
41
|
+
style.minWidth = 0;
|
|
42
|
+
style.minHeight = 0;
|
|
43
|
+
if (get('flexDirection') === 'row') {
|
|
44
|
+
style.overflowX = 'auto';
|
|
45
|
+
style.overflowY = 'hidden';
|
|
46
|
+
style.maxWidth = '100%';
|
|
47
|
+
style.maxHeight = '100%';
|
|
48
|
+
} else {
|
|
49
|
+
style.overflowY = 'auto';
|
|
50
|
+
style.overflowX = 'hidden';
|
|
51
|
+
style.maxHeight = '100%';
|
|
52
|
+
style.maxWidth = '100%';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const flexDirection = get('flexDirection');
|
|
56
|
+
if (flexDirection) style.flexDirection = flexDirection as any;
|
|
57
|
+
const alignItems = get('alignItems');
|
|
58
|
+
if (alignItems)
|
|
59
|
+
style.alignItems = alignItems as React.CSSProperties['alignItems'];
|
|
60
|
+
const justifyContent = get('justifyContent');
|
|
61
|
+
if (justifyContent)
|
|
62
|
+
style.justifyContent =
|
|
63
|
+
justifyContent as React.CSSProperties['justifyContent'];
|
|
64
|
+
const setParsedSize = <K extends keyof React.CSSProperties>(
|
|
65
|
+
property: K,
|
|
66
|
+
rawValue: string | number | undefined,
|
|
67
|
+
) => {
|
|
68
|
+
if (isEmptySizeValue(rawValue)) return;
|
|
69
|
+
const parsed = parseSize(rawValue);
|
|
70
|
+
style[property] = parsed as React.CSSProperties[K];
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
setParsedSize('gap', get('gap') as any);
|
|
74
|
+
setParsedSize('padding', get('padding') as any);
|
|
75
|
+
setParsedSize('margin', get('margin') as any);
|
|
76
|
+
|
|
77
|
+
const paddingHorizontal = get('paddingHorizontal') as any;
|
|
78
|
+
if (!isEmptySizeValue(paddingHorizontal)) {
|
|
79
|
+
const parsed = parseSize(paddingHorizontal);
|
|
80
|
+
style.paddingLeft = parsed as React.CSSProperties['paddingLeft'];
|
|
81
|
+
style.paddingRight = parsed as React.CSSProperties['paddingRight'];
|
|
82
|
+
}
|
|
83
|
+
const paddingVertical = get('paddingVertical') as any;
|
|
84
|
+
if (!isEmptySizeValue(paddingVertical)) {
|
|
85
|
+
const parsed = parseSize(paddingVertical);
|
|
86
|
+
style.paddingTop = parsed as React.CSSProperties['paddingTop'];
|
|
87
|
+
style.paddingBottom = parsed as React.CSSProperties['paddingBottom'];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Explicit per-side paddings should override paddingHorizontal/paddingVertical.
|
|
91
|
+
setParsedSize('paddingTop', get('paddingTop') as any);
|
|
92
|
+
setParsedSize('paddingBottom', get('paddingBottom') as any);
|
|
93
|
+
setParsedSize('paddingLeft', get('paddingLeft') as any);
|
|
94
|
+
setParsedSize('paddingRight', get('paddingRight') as any);
|
|
95
|
+
|
|
96
|
+
const marginHorizontalRaw =
|
|
97
|
+
((attributes as Record<string, unknown>).marginHorizontal as
|
|
98
|
+
| string
|
|
99
|
+
| number
|
|
100
|
+
| undefined) ?? (styleBag?.marginHorizontal as any);
|
|
101
|
+
if (!isEmptySizeValue(marginHorizontalRaw)) {
|
|
102
|
+
const parsed = parseSize(marginHorizontalRaw);
|
|
103
|
+
style.marginLeft = parsed as React.CSSProperties['marginLeft'];
|
|
104
|
+
style.marginRight = parsed as React.CSSProperties['marginRight'];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const marginVertical = get('marginVertical') as any;
|
|
108
|
+
if (!isEmptySizeValue(marginVertical)) {
|
|
109
|
+
const parsed = parseSize(marginVertical);
|
|
110
|
+
style.marginTop = parsed as React.CSSProperties['marginTop'];
|
|
111
|
+
style.marginBottom = parsed as React.CSSProperties['marginBottom'];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
setParsedSize('marginTop', get('marginTop') as any);
|
|
115
|
+
setParsedSize('marginBottom', get('marginBottom') as any);
|
|
116
|
+
setParsedSize('marginLeft', get('marginLeft') as any);
|
|
117
|
+
setParsedSize('marginRight', get('marginRight') as any);
|
|
118
|
+
const backgroundColor = get('backgroundColor') as any;
|
|
119
|
+
if (backgroundColor) {
|
|
120
|
+
style.backgroundColor =
|
|
121
|
+
parseColor(backgroundColor, {
|
|
122
|
+
projectColors: options.projectColors,
|
|
123
|
+
appConfig: options.appConfig,
|
|
124
|
+
}) ?? backgroundColor;
|
|
125
|
+
}
|
|
126
|
+
setParsedSize('borderRadius', get('borderRadius') as any);
|
|
127
|
+
setParsedSize('width', get('width') as any);
|
|
128
|
+
setParsedSize('minWidth', get('minWidth') as any);
|
|
129
|
+
setParsedSize('maxWidth', get('maxWidth') as any);
|
|
130
|
+
setParsedSize('height', get('height') as any);
|
|
131
|
+
setParsedSize('minHeight', get('minHeight') as any);
|
|
132
|
+
setParsedSize('maxHeight', get('maxHeight') as any);
|
|
133
|
+
const flex = get('flex') as any;
|
|
134
|
+
if (flex !== undefined) style.flex = flex as React.CSSProperties['flex'];
|
|
135
|
+
const position = get('position') as any;
|
|
136
|
+
if (position) style.position = position as React.CSSProperties['position'];
|
|
137
|
+
setParsedSize('top', get('top') as any);
|
|
138
|
+
setParsedSize('bottom', get('bottom') as any);
|
|
139
|
+
setParsedSize('left', get('left') as any);
|
|
140
|
+
setParsedSize('right', get('right') as any);
|
|
141
|
+
const zIndex = get('zIndex') as any;
|
|
142
|
+
if (zIndex !== undefined)
|
|
143
|
+
style.zIndex = zIndex as React.CSSProperties['zIndex'];
|
|
144
|
+
return style;
|
|
145
|
+
}
|