@developer_tribe/react-builder 1.2.23 → 1.2.24

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.
@@ -7,3 +7,15 @@ import type { NodeDefaultAttribute } from '../types/Node';
7
7
  export declare function getStyleBag(attributes: NodeDefaultAttribute | undefined): Record<string, unknown> | undefined;
8
8
  /** Safe indexed access to attributes. Use for reading style/direct props. */
9
9
  export declare function toAttributeRecord(attributes: unknown): Record<string, unknown>;
10
+ /**
11
+ * All attribute keys that represent visual style properties.
12
+ * Used to separate style keys from non-style (behavioral/content) keys.
13
+ *
14
+ * Keep in sync with ViewStyleGenerated, TextStyleGenerated, and ImageStyleGenerated.
15
+ */
16
+ declare const STYLE_ATTR_KEYS_LIST: readonly ["style", "styles", "flexDirection", "flexWrap", "alignItems", "justifyContent", "gap", "padding", "paddingHorizontal", "paddingVertical", "paddingTop", "paddingBottom", "paddingLeft", "paddingRight", "margin", "marginHorizontal", "marginVertical", "marginTop", "marginBottom", "marginLeft", "marginRight", "backgroundColor", "borderRadius", "width", "minWidth", "maxWidth", "height", "minHeight", "maxHeight", "flex", "position", "top", "bottom", "left", "right", "zIndex", "color", "fontSize", "fontFamily", "fontWeight", "textAlign", "resizeMode"];
17
+ /** Type-level union of all style attribute keys. Use with `Omit<T, StyleAttrKey>`. */
18
+ export type StyleAttrKey = (typeof STYLE_ATTR_KEYS_LIST)[number];
19
+ /** Strips all visual-style keys from an attributes record, returning only non-style keys. */
20
+ export declare function stripStyleKeys(attrs: Record<string, unknown>): Record<string, unknown>;
21
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@developer_tribe/react-builder",
3
- "version": "1.2.23",
3
+ "version": "1.2.24",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "restricted": true,
@@ -73,6 +73,7 @@
73
73
  "lottie-react": "^2.4.1",
74
74
  "prettier": "^3.6.2",
75
75
  "react": "^18.3.1",
76
+ "react-native": "^0.83.1",
76
77
  "rimraf": "^6.0.1",
77
78
  "rollup": "^4.52.2",
78
79
  "rollup-plugin-peer-deps-external": "^2.2.4",
@@ -90,12 +91,16 @@
90
91
  "peerDependencies": {
91
92
  "react": ">=17",
92
93
  "react-dom": ">=17",
94
+ "react-native": ">=0.70",
93
95
  "react-router-dom": ">=6.0.0"
94
96
  },
95
97
  "peerDependenciesMeta": {
96
98
  "react-dom": {
97
99
  "optional": true
98
100
  },
101
+ "react-native": {
102
+ "optional": true
103
+ },
99
104
  "react-router-dom": {
100
105
  "optional": true
101
106
  }
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "supportedProjectVersion": "1.1.2",
3
- "reactBuilderVersion": "1.2.22"
3
+ "reactBuilderVersion": "1.2.23"
4
4
  }
@@ -146,7 +146,7 @@
146
146
  "children": null
147
147
  },
148
148
  {
149
- "type": "text",
149
+ "type": "Text",
150
150
  "attributes": {
151
151
  "description": "Metin öğesi. (#1)",
152
152
  "title": "text 1",
@@ -190,7 +190,7 @@
190
190
  "children": null
191
191
  },
192
192
  {
193
- "type": "text",
193
+ "type": "Text",
194
194
  "attributes": {
195
195
  "description": "Metin öğesi. (#2)",
196
196
  "title": "text 2",
@@ -234,7 +234,7 @@
234
234
  "children": null
235
235
  },
236
236
  {
237
- "type": "text",
237
+ "type": "Text",
238
238
  "attributes": {
239
239
  "description": "Metin öğesi. (#3)",
240
240
  "title": "text 3",
@@ -273,7 +273,7 @@
273
273
  "children": null
274
274
  },
275
275
  {
276
- "type": "text",
276
+ "type": "Text",
277
277
  "attributes": {
278
278
  "description": "Metin öğesi. (#4)",
279
279
  "title": "Product Desc(s)",
@@ -284,7 +284,7 @@
284
284
  "children": "@productDescription — Unlock all premium features for a month."
285
285
  },
286
286
  {
287
- "type": "text",
287
+ "type": "Text",
288
288
  "attributes": {
289
289
  "description": "Metin öğesi. (#5)",
290
290
  "title": "Product Price(s)",
@@ -144,7 +144,7 @@
144
144
  "children": null
145
145
  },
146
146
  {
147
- "type": "text",
147
+ "type": "Text",
148
148
  "attributes": {
149
149
  "description": "Text item.",
150
150
  "title": "text 1",
@@ -188,7 +188,7 @@
188
188
  "children": null
189
189
  },
190
190
  {
191
- "type": "text",
191
+ "type": "Text",
192
192
  "attributes": {
193
193
  "description": "Text item.",
194
194
  "title": "text 2",
@@ -232,7 +232,7 @@
232
232
  "children": null
233
233
  },
234
234
  {
235
- "type": "text",
235
+ "type": "Text",
236
236
  "attributes": {
237
237
  "description": "Text item.",
238
238
  "title": "text 3",
@@ -271,7 +271,7 @@
271
271
  "children": null
272
272
  },
273
273
  {
274
- "type": "text",
274
+ "type": "Text",
275
275
  "attributes": {
276
276
  "description": "Product description.",
277
277
  "title": "Product Desc(s)",
@@ -282,7 +282,7 @@
282
282
  "children": "@productDescription — Unlock all premium features for a month."
283
283
  },
284
284
  {
285
- "type": "text",
285
+ "type": "Text",
286
286
  "attributes": {
287
287
  "description": "Product price.",
288
288
  "title": "Product Price(s)",
@@ -1,4 +1,5 @@
1
1
  import { useMemo } from 'react';
2
+ import type { ImageStyle } from 'react-native';
2
3
  import type { NodeData } from '../../../types/Node';
3
4
  import type {
4
5
  ImagePropsGenerated,
@@ -7,11 +8,21 @@ import type {
7
8
  import { useBuilderParams } from '../../../components/BuilderProvider';
8
9
  import { extractImageStyleNative } from '../../../utils/extractImageStyle';
9
10
  import { defaultAppConfig } from '../../../types/PreviewConfig';
10
- import { getStyleBag } from '../../../utils/attributeStyle';
11
+ import {
12
+ getStyleBag,
13
+ toAttributeRecord,
14
+ stripStyleKeys,
15
+ type StyleAttrKey,
16
+ } from '../../../utils/attributeStyle';
11
17
 
12
18
  export function useExtractImageStyle<
13
19
  T extends ImagePropsGenerated['attributes'],
14
- >(node: NodeData<T>) {
20
+ >(
21
+ node: NodeData<T>,
22
+ ): {
23
+ style: ImageStyle;
24
+ other: Omit<T, StyleAttrKey> & { resizeMode?: ResizeModeOptionType };
25
+ } {
15
26
  const { appConfig, projectColors: builderProjectColors } = useBuilderParams();
16
27
  const theme = appConfig?.theme ?? defaultAppConfig.theme;
17
28
  const projectColors = builderProjectColors;
@@ -23,26 +34,19 @@ export function useExtractImageStyle<
23
34
  extractImageStyleNative(node, { theme, projectColors });
24
35
 
25
36
  const attrs = node.attributes;
26
-
27
- // Prefer the typed resizeMode from attributes, fall back to extracted style value.
37
+ const stripped = stripStyleKeys(toAttributeRecord(attrs));
28
38
  const imgStylesBag = getStyleBag(attrs);
39
+
40
+ // Prefer the typed resizeMode from style bag, fall back to extracted style value.
29
41
  const resizeMode = ((imgStylesBag?.resizeMode as string | undefined) ??
30
42
  resizeModeFromStyle) as ResizeModeOptionType | undefined;
31
43
 
32
- // Forward all non-style attributes; style bag is already consumed above.
33
- //Optimzation trade off by readability: fromEntries+filter avoids generic-to-Record double assertion.
34
- const forwardedAttrs = Object.fromEntries(
35
- Object.entries(attrs ?? {}).filter(
36
- ([key]) => key !== 'style' && key !== 'styles',
37
- ),
38
- );
39
-
40
44
  return {
41
- style,
45
+ style: style as ImageStyle,
42
46
  other: {
43
- ...forwardedAttrs,
47
+ ...stripped,
44
48
  resizeMode,
45
- },
49
+ } as Omit<T, StyleAttrKey> & { resizeMode?: ResizeModeOptionType },
46
50
  };
47
51
  }, [node, theme, projectColors]);
48
52
  }
@@ -1,14 +1,26 @@
1
1
  import { useMemo } from 'react';
2
+ import type { TextStyle } from 'react-native';
2
3
  import type { NodeData } from '../../../types/Node';
3
4
  import type { TextPropsGenerated } from '../../../build-components/Text/TextProps.generated';
4
5
  import { defaultAppConfig } from '../../../types/PreviewConfig';
5
6
  import { useBuilderParams } from '../../../components/BuilderProvider';
6
7
  import { extractTextStyleNative } from '../../../utils/extractTextStyle';
7
- import { getStyleBag } from '../../../utils/attributeStyle';
8
+ import {
9
+ getStyleBag,
10
+ toAttributeRecord,
11
+ stripStyleKeys,
12
+ type StyleAttrKey,
13
+ } from '../../../utils/attributeStyle';
8
14
 
9
15
  export function useExtractTextStyle<T extends TextPropsGenerated['attributes']>(
10
16
  node: NodeData<T>,
11
- ) {
17
+ ): {
18
+ style: TextStyle;
19
+ other: Omit<T, StyleAttrKey> & {
20
+ adjustsFontSizeToFit?: boolean;
21
+ showEllipsis?: boolean;
22
+ };
23
+ } {
12
24
  const {
13
25
  appConfig: builderAppConfig,
14
26
  projectColors: builderProjectColors,
@@ -27,25 +39,23 @@ export function useExtractTextStyle<T extends TextPropsGenerated['attributes']>(
27
39
  fonts,
28
40
  });
29
41
 
30
- const attrs = node.attributes ?? {};
42
+ const attrs = node.attributes;
43
+ const stripped = stripStyleKeys(toAttributeRecord(attrs));
31
44
  const styleBag = getStyleBag(attrs);
32
- const {
33
- style: _style,
34
- styles: _styles,
35
- ...rest
36
- } = attrs as Record<string, unknown>;
37
- void _style;
38
- void _styles;
39
45
 
40
46
  return {
41
- style,
47
+ style: style as TextStyle,
42
48
  other: {
43
- ...rest,
44
- // These are "behavior" flags stored under style in the schema.
45
- adjustsFontSizeToFit: styleBag?.adjustsFontSizeToFit as
49
+ ...stripped,
50
+ // These are "behavior" flags that may reside in the style bag.
51
+ adjustsFontSizeToFit: (stripped.adjustsFontSizeToFit ??
52
+ styleBag?.adjustsFontSizeToFit) as boolean | undefined,
53
+ showEllipsis: (stripped.showEllipsis ?? styleBag?.showEllipsis) as
46
54
  | boolean
47
55
  | undefined,
48
- showEllipsis: styleBag?.showEllipsis as boolean | undefined,
56
+ } as Omit<T, StyleAttrKey> & {
57
+ adjustsFontSizeToFit?: boolean;
58
+ showEllipsis?: boolean;
49
59
  },
50
60
  };
51
61
  },
@@ -1,44 +1,42 @@
1
1
  import { useMemo } from 'react';
2
+ import type { ViewStyle } from 'react-native';
2
3
  import type { NodeData } from '../../../types/Node';
3
4
  import type { ViewPropsGenerated } from '../../../build-components/View/ViewProps.generated';
4
5
  import { useBuilderParams } from '../../../components/BuilderProvider';
5
6
  import { extractViewStyleNative } from '../../../utils/extractViewStyle';
6
7
  import { defaultAppConfig } from '../../../types/PreviewConfig';
7
- import { getStyleBag, toAttributeRecord } from '../../../utils/attributeStyle';
8
+ import {
9
+ getStyleBag,
10
+ toAttributeRecord,
11
+ stripStyleKeys,
12
+ type StyleAttrKey,
13
+ } from '../../../utils/attributeStyle';
8
14
 
9
15
  export function useExtractViewStyle<T extends ViewPropsGenerated['attributes']>(
10
16
  node: NodeData<T>,
11
- ) {
17
+ ): {
18
+ style: ViewStyle;
19
+ other: Omit<T, StyleAttrKey> & { scrollable?: boolean };
20
+ } {
12
21
  const { appConfig, projectColors: builderProjectColors } = useBuilderParams();
13
22
  const theme = appConfig?.theme ?? defaultAppConfig.theme;
14
23
  const projectColors = builderProjectColors;
15
24
 
16
25
  return useMemo(() => {
17
26
  const style = extractViewStyleNative(node, { theme, projectColors });
18
-
19
27
  const attrs = node.attributes;
20
-
21
- // Forward all non-style attributes; style bag is already consumed by extractViewStyleNative.
22
- //Optimzation trade off by readability: fromEntries+filter avoids generic-to-Record double assertion.
23
- const forwardedAttrs = Object.fromEntries(
24
- Object.entries(attrs ?? {}).filter(
25
- ([key]) => key !== 'style' && key !== 'styles',
26
- ),
27
- );
28
-
29
- // scrollable may reside inside the styles bag at runtime (not on ViewStyleGenerated).
30
- const attrRecord = toAttributeRecord(attrs);
28
+ const stripped = stripStyleKeys(toAttributeRecord(attrs));
31
29
  const styleBag = getStyleBag(attrs);
32
- const scrollable = (attrRecord.scrollable ?? styleBag?.scrollable) as
33
- | boolean
34
- | undefined;
35
30
 
36
31
  return {
37
- style,
32
+ style: style as ViewStyle,
38
33
  other: {
39
- ...forwardedAttrs,
40
- scrollable,
41
- },
34
+ ...stripped,
35
+ // scrollable may reside inside the styles bag at runtime.
36
+ scrollable: (stripped.scrollable ?? styleBag?.scrollable) as
37
+ | boolean
38
+ | undefined,
39
+ } as Omit<T, StyleAttrKey> & { scrollable?: boolean },
42
40
  };
43
41
  }, [node, theme, projectColors]);
44
42
  }
@@ -11,6 +11,9 @@
11
11
  "style": {
12
12
  "resizeMode": ["cover", "contain", "stretch", "center"]
13
13
  }
14
+ },
15
+ "defaults": {
16
+ "resizeMode": "contain"
14
17
  }
15
18
  },
16
19
  "meta": {
@@ -10,13 +10,16 @@ import { useLogRender } from '../../utils/useLogRender';
10
10
  import { useExtractViewStyle } from '../../attribute-analyser/style/web/useExtractViewStyle';
11
11
  import { useMockOSContext, useMockPermission } from '../../mockOS';
12
12
  import { useLocalize } from '../../hooks/useLocalize';
13
+ import { parseColor } from '../../utils/parseColor';
14
+ import { getStyleBag, toAttributeRecord } from '../../utils/attributeStyle';
13
15
 
14
16
  function OnboardButton({ node }: OnboardButtonComponentProps) {
15
17
  useLogRender('OnboardButton');
16
18
  node = useNode(node);
17
19
  const attributeName = node.sourceType ?? node.type ?? 'OnboardButton';
18
20
  const { emblaApi } = useContext(onboardContext) ?? {};
19
- const { appConfig } = useBuilderParams();
21
+ const { appConfig, projectColors } = useBuilderParams();
22
+ const theme = appConfig?.theme;
20
23
 
21
24
  const context = useMockOSContext();
22
25
  const mockPermissionManager = useMockPermission(context);
@@ -25,14 +28,25 @@ function OnboardButton({ node }: OnboardButtonComponentProps) {
25
28
  const attributeKey = node.key ?? generatedId;
26
29
 
27
30
  const attrs = node.attributes;
28
- const styleBag = attrs?.style;
31
+ const attrRecord = toAttributeRecord(attrs);
32
+ const styleBag = getStyleBag(attrs);
29
33
  const labelRaw = attrs?.labelKey ?? '';
30
34
  const localize = useLocalize({ appConfig });
31
35
  const label = localize(labelRaw);
32
36
 
33
- const flex = styleBag?.flex ?? 1;
34
- const textColor = attrs?.button_text_color ?? '#FFFFFF';
35
- const backgroundColor = attrs?.button_background_color ?? '#0066FF';
37
+ const flex = (attrRecord.flex ?? styleBag?.flex ?? 1) as number;
38
+
39
+ // The editor saves color attrs inside `styles` (meta category === 'style'),
40
+ // but legacy JSON may have them at the top level.
41
+ const rawTextColor = (attrRecord.button_text_color ??
42
+ styleBag?.button_text_color) as string | undefined;
43
+ const rawBgColor = (attrRecord.button_background_color ??
44
+ styleBag?.button_background_color) as string | undefined;
45
+
46
+ const textColor =
47
+ parseColor(rawTextColor, { projectColors, theme }) ?? '#FFFFFF';
48
+ const backgroundColor =
49
+ parseColor(rawBgColor, { projectColors, theme }) ?? '#0066FF';
36
50
  const viewStyle = useExtractViewStyle(node);
37
51
 
38
52
  const handleClick = () => {
@@ -9,6 +9,7 @@ import { isNodeSelected, SELECTED_OUTLINE_STYLE } from '../../utils/selection';
9
9
  import { useMergedStyle } from '../../utils/useMergedStyle';
10
10
  import { defaultAppConfig } from '../../types/PreviewConfig';
11
11
  import { parseColor } from '../../utils/parseColor';
12
+ import { getStyleBag, toAttributeRecord } from '../../utils/attributeStyle';
12
13
 
13
14
  type Segment =
14
15
  | { type: 'text'; value: string }
@@ -107,26 +108,36 @@ function OnboardFooter({ node }: OnboardFooterComponentProps) {
107
108
  key ? (localication?.[defaultLanguage ?? 'en']?.[key] ?? key) : '';
108
109
 
109
110
  const attrs = node?.attributes;
111
+ const attrRecord = toAttributeRecord(attrs);
112
+ const styleBag = getStyleBag(attrs);
110
113
  const text = t(attrs?.textLocalizationKey);
111
114
  const textStyle = useExtractTextStyle(node, true);
112
115
  const viewStyle = useExtractViewStyle(node);
113
116
 
117
+ // Read linked-word colors from both top-level attributes and the styles bag.
118
+ // The editor saves them inside `styles` (meta category === 'style'),
119
+ // but legacy JSON may have them at the top level.
120
+ const rawFirstColor = (attrRecord.linkedWordFirstColor ??
121
+ styleBag?.linkedWordFirstColor) as string | undefined;
122
+ const rawSecondColor = (attrRecord.linkedWordSecondColor ??
123
+ styleBag?.linkedWordSecondColor) as string | undefined;
124
+
114
125
  // Parse colors for linked words
115
126
  const parsedFirstColor = useMemo(
116
127
  () =>
117
- parseColor(attrs?.linkedWordFirstColor, {
128
+ parseColor(rawFirstColor, {
118
129
  projectColors,
119
130
  theme,
120
131
  }),
121
- [attrs?.linkedWordFirstColor, projectColors, theme],
132
+ [rawFirstColor, projectColors, theme],
122
133
  );
123
134
  const parsedSecondColor = useMemo(
124
135
  () =>
125
- parseColor(attrs?.linkedWordSecondColor, {
136
+ parseColor(rawSecondColor, {
126
137
  projectColors,
127
138
  theme,
128
139
  }),
129
- [attrs?.linkedWordSecondColor, projectColors, theme],
140
+ [rawSecondColor, projectColors, theme],
130
141
  );
131
142
 
132
143
  const mergedTextStyle = useMergedStyle(textStyle, {
@@ -4205,6 +4205,7 @@ export const patterns = [
4205
4205
  description: 'description',
4206
4206
  src: 'string',
4207
4207
  },
4208
+ defaults: { resizeMode: 'contain' },
4208
4209
  },
4209
4210
  meta: {
4210
4211
  desiredParent: ['all'],
@@ -4527,6 +4528,7 @@ export const patterns = [
4527
4528
  },
4528
4529
  },
4529
4530
  defaults: {
4531
+ resizeMode: 'contain',
4530
4532
  style: {
4531
4533
  flexDirection: 'column',
4532
4534
  position: 'relative',
@@ -7589,6 +7591,7 @@ export const patterns = [
7589
7591
  video_url: 'string',
7590
7592
  lottie: 'string',
7591
7593
  },
7594
+ defaults: { resizeMode: 'contain' },
7592
7595
  },
7593
7596
  meta: {
7594
7597
  desiredParent: ['>OnboardProvider', '>OnboardItem', '!=Onboard'],
@@ -7925,6 +7928,7 @@ export const patterns = [
7925
7928
  },
7926
7929
  },
7927
7930
  defaults: {
7931
+ resizeMode: 'contain',
7928
7932
  style: {
7929
7933
  flexDirection: 'column',
7930
7934
  position: 'relative',
@@ -43,7 +43,7 @@ type BuilderProviderProps = {
43
43
  children: React.ReactNode;
44
44
  };
45
45
 
46
- const BuilderContext = createContext<BuilderProviderParams | undefined>(
46
+ const builderContext = createContext<BuilderProviderParams | undefined>(
47
47
  undefined,
48
48
  );
49
49
 
@@ -91,7 +91,7 @@ export function BuilderProvider({ params, children }: BuilderProviderProps) {
91
91
  );
92
92
 
93
93
  return (
94
- <BuilderContext.Provider value={value}>{children}</BuilderContext.Provider>
94
+ <builderContext.Provider value={value}>{children}</builderContext.Provider>
95
95
  );
96
96
  }
97
97
 
@@ -119,10 +119,10 @@ const defaultProjectColors: Readonly<ProjectColors> = {
119
119
  FOOTER_TEXT: '#81838F',
120
120
  },
121
121
  dark: {
122
- TEXT: '#161827',
123
- BACKGROUND: '#F4F5FF',
122
+ TEXT: '#E9EBF9',
123
+ BACKGROUND: '#080A17',
124
124
  ICON: '#0450E2',
125
- LINE: '#E9EBF9',
125
+ LINE: '#161827',
126
126
  ONBOARD_TITLE: '#FDFDFD',
127
127
  ONBOARD_SUBTITLE: '#C7C7C7',
128
128
  BUTTON_SECONDARY_TEXT: '#A9AAAC',
@@ -133,7 +133,7 @@ const defaultProjectColors: Readonly<ProjectColors> = {
133
133
 
134
134
  export function useBuilderParams(): Readonly<BuilderProviderParams> {
135
135
  return (
136
- useContext(BuilderContext) ?? {
136
+ useContext(builderContext) ?? {
137
137
  products: [],
138
138
  benefits: {},
139
139
  platform: 'web',
package/src/index.ts CHANGED
@@ -91,3 +91,6 @@ export {
91
91
  } from './assets/samples/getSamples';
92
92
  export { getDefaultProject } from './utils/getDefaultProject';
93
93
  export type { EventObjectGenerated } from './build-components/OnboardButton/OnboardButtonProps.generated';
94
+
95
+ export { parseColor } from './utils/parseColor';
96
+ export type { ParseColorOptions } from './utils/parseColor';
@@ -24,3 +24,81 @@ export function toAttributeRecord(
24
24
  function isPlainObject(value: unknown): value is Record<string, unknown> {
25
25
  return typeof value === 'object' && value !== null && !Array.isArray(value);
26
26
  }
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Style-key filtering – separates visual style keys from non-style attributes
30
+ // ---------------------------------------------------------------------------
31
+
32
+ /**
33
+ * All attribute keys that represent visual style properties.
34
+ * Used to separate style keys from non-style (behavioral/content) keys.
35
+ *
36
+ * Keep in sync with ViewStyleGenerated, TextStyleGenerated, and ImageStyleGenerated.
37
+ */
38
+ const STYLE_ATTR_KEYS_LIST = [
39
+ // Style bag containers
40
+ 'style',
41
+ 'styles',
42
+ // Layout
43
+ 'flexDirection',
44
+ 'flexWrap',
45
+ 'alignItems',
46
+ 'justifyContent',
47
+ 'gap',
48
+ // Padding
49
+ 'padding',
50
+ 'paddingHorizontal',
51
+ 'paddingVertical',
52
+ 'paddingTop',
53
+ 'paddingBottom',
54
+ 'paddingLeft',
55
+ 'paddingRight',
56
+ // Margin
57
+ 'margin',
58
+ 'marginHorizontal',
59
+ 'marginVertical',
60
+ 'marginTop',
61
+ 'marginBottom',
62
+ 'marginLeft',
63
+ 'marginRight',
64
+ // Background & border
65
+ 'backgroundColor',
66
+ 'borderRadius',
67
+ // Sizing
68
+ 'width',
69
+ 'minWidth',
70
+ 'maxWidth',
71
+ 'height',
72
+ 'minHeight',
73
+ 'maxHeight',
74
+ // Flex & position
75
+ 'flex',
76
+ 'position',
77
+ 'top',
78
+ 'bottom',
79
+ 'left',
80
+ 'right',
81
+ 'zIndex',
82
+ // Text
83
+ 'color',
84
+ 'fontSize',
85
+ 'fontFamily',
86
+ 'fontWeight',
87
+ 'textAlign',
88
+ // Image
89
+ 'resizeMode',
90
+ ] as const;
91
+
92
+ /** Type-level union of all style attribute keys. Use with `Omit<T, StyleAttrKey>`. */
93
+ export type StyleAttrKey = (typeof STYLE_ATTR_KEYS_LIST)[number];
94
+
95
+ const STYLE_ATTR_KEYS: ReadonlySet<string> = new Set(STYLE_ATTR_KEYS_LIST);
96
+
97
+ /** Strips all visual-style keys from an attributes record, returning only non-style keys. */
98
+ export function stripStyleKeys(
99
+ attrs: Record<string, unknown>,
100
+ ): Record<string, unknown> {
101
+ return Object.fromEntries(
102
+ Object.entries(attrs).filter(([key]) => !STYLE_ATTR_KEYS.has(key)),
103
+ );
104
+ }