flowboard-react 0.6.7 → 0.6.10

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/LICENSE +2 -3
  2. package/README.md +16 -0
  3. package/lib/module/components/FlowboardFlow.js +24 -57
  4. package/lib/module/components/FlowboardFlow.js.map +1 -1
  5. package/lib/module/components/FlowboardRenderer.js +590 -196
  6. package/lib/module/components/FlowboardRenderer.js.map +1 -1
  7. package/lib/module/components/layout/stackOverlayModel.js +156 -0
  8. package/lib/module/components/layout/stackOverlayModel.js.map +1 -0
  9. package/lib/module/fonts/fontLoader.js +285 -0
  10. package/lib/module/fonts/fontLoader.js.map +1 -0
  11. package/lib/module/fonts/fontResolver.js +38 -0
  12. package/lib/module/fonts/fontResolver.js.map +1 -0
  13. package/lib/module/fonts/google-fonts-meta.json +1 -0
  14. package/lib/module/fonts/googleFontCatalog.js +56 -0
  15. package/lib/module/fonts/googleFontCatalog.js.map +1 -0
  16. package/lib/module/fonts/googleFontLoader.js +101 -0
  17. package/lib/module/fonts/googleFontLoader.js.map +1 -0
  18. package/lib/module/fonts/googleFontsLoader.js +68 -0
  19. package/lib/module/fonts/googleFontsLoader.js.map +1 -0
  20. package/lib/module/index.js +2 -0
  21. package/lib/module/index.js.map +1 -1
  22. package/lib/typescript/src/components/FlowboardFlow.d.ts.map +1 -1
  23. package/lib/typescript/src/components/FlowboardRenderer.d.ts +11 -4
  24. package/lib/typescript/src/components/FlowboardRenderer.d.ts.map +1 -1
  25. package/lib/typescript/src/components/layout/stackOverlayModel.d.ts +13 -0
  26. package/lib/typescript/src/components/layout/stackOverlayModel.d.ts.map +1 -0
  27. package/lib/typescript/src/fonts/fontLoader.d.ts +17 -0
  28. package/lib/typescript/src/fonts/fontLoader.d.ts.map +1 -0
  29. package/lib/typescript/src/fonts/fontResolver.d.ts +11 -0
  30. package/lib/typescript/src/fonts/fontResolver.d.ts.map +1 -0
  31. package/lib/typescript/src/fonts/googleFontCatalog.d.ts +4 -0
  32. package/lib/typescript/src/fonts/googleFontCatalog.d.ts.map +1 -0
  33. package/lib/typescript/src/fonts/googleFontLoader.d.ts +7 -0
  34. package/lib/typescript/src/fonts/googleFontLoader.d.ts.map +1 -0
  35. package/lib/typescript/src/fonts/googleFontsLoader.d.ts +18 -0
  36. package/lib/typescript/src/fonts/googleFontsLoader.d.ts.map +1 -0
  37. package/lib/typescript/src/index.d.ts +2 -0
  38. package/lib/typescript/src/index.d.ts.map +1 -1
  39. package/package.json +14 -2
  40. package/src/components/FlowboardFlow.tsx +33 -67
  41. package/src/components/FlowboardRenderer.tsx +852 -210
  42. package/src/components/layout/stackOverlayModel.ts +185 -0
  43. package/src/fonts/fontLoader.ts +426 -0
  44. package/src/fonts/fontResolver.ts +69 -0
  45. package/src/fonts/google-fonts-meta.json +1 -0
  46. package/src/fonts/googleFontCatalog.ts +77 -0
  47. package/src/fonts/googleFontLoader.ts +136 -0
  48. package/src/fonts/googleFontsLoader.ts +124 -0
  49. package/src/index.tsx +13 -0
@@ -1,4 +1,4 @@
1
- import React, { useMemo } from 'react';
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
2
  import {
3
3
  Animated,
4
4
  KeyboardAvoidingView,
@@ -42,6 +42,26 @@ import {
42
42
  } from '../utils/flowboardUtils';
43
43
  import { resolveFontAwesomeIcon, FontAwesome6 } from '../core/fontAwesome';
44
44
  import { useSliderRegistry } from './widgets/sliderRegistry';
45
+ import {
46
+ isOverlayStack,
47
+ isPositionedChild,
48
+ resolveAlignmentToCss,
49
+ resolveClipBehavior,
50
+ resolveFitBehavior,
51
+ resolveOverlayAlignmentValue,
52
+ resolvePositionedStyle,
53
+ } from './layout/stackOverlayModel';
54
+ import {
55
+ ensureGoogleFontLoaded as ensureWebGoogleFontLoaded,
56
+ resolveFontFamily,
57
+ } from '../fonts/googleFontLoader';
58
+ import {
59
+ ensureFontLoaded as ensureNativeFontLoaded,
60
+ isFontLoaded,
61
+ resolveAppliedFontFamily,
62
+ subscribeFontLoad,
63
+ } from '../fonts/fontLoader';
64
+ import { resolveFontSpec } from '../fonts/fontResolver';
45
65
 
46
66
  const styles = StyleSheet.create({
47
67
  root: { flex: 1, backgroundColor: '#ffffff' },
@@ -91,6 +111,175 @@ function getAxisBoundsForPageScroll(pageScroll: PageScrollAxis): AxisBounds {
91
111
  return { widthBounded: true, heightBounded: true };
92
112
  }
93
113
 
114
+ const LEGACY_SLIDE_DROP_KEYS = new Set(['badge']);
115
+
116
+ function isDevelopmentMode(): boolean {
117
+ return process.env.NODE_ENV !== 'production';
118
+ }
119
+
120
+ function warnLegacySlideMigration(
121
+ contextPath: string,
122
+ componentId: string | undefined,
123
+ droppedKeys: string[]
124
+ ): void {
125
+ if (!isDevelopmentMode() || droppedKeys.length === 0) return;
126
+ const idSuffix = componentId ? ` (id: ${componentId})` : '';
127
+ console.warn(
128
+ `[slider-migration] Migrated legacy slide to stack at "${contextPath}"${idSuffix}. Dropped unsupported keys: ${droppedKeys.join(
129
+ ', '
130
+ )}`
131
+ );
132
+ }
133
+
134
+ function warnLegacySliderChildWrap(
135
+ contextPath: string,
136
+ childType: string | undefined
137
+ ): void {
138
+ if (!isDevelopmentMode()) return;
139
+ console.warn(
140
+ `[slider-migration] Wrapped non-stack slider child at "${contextPath}" as a stack page${
141
+ childType ? ` (type: ${childType})` : ''
142
+ }.`
143
+ );
144
+ }
145
+
146
+ function normalizeLegacySlidePropsToStackProps(
147
+ input: Record<string, any> | undefined,
148
+ contextPath: string,
149
+ componentId?: string
150
+ ): Record<string, any> {
151
+ const props =
152
+ input && typeof input === 'object'
153
+ ? { ...input }
154
+ : ({} as Record<string, any>);
155
+ const droppedKeys: string[] = [];
156
+
157
+ if (props.fill && typeof props.fill === 'object') {
158
+ const normalizedFill = { ...(props.fill as Record<string, any>) };
159
+ const fillType = String(normalizedFill.type ?? '').toLowerCase();
160
+ if (fillType === 'color') {
161
+ normalizedFill.type = 'solid';
162
+ }
163
+ props.fill = normalizedFill;
164
+ }
165
+
166
+ for (const key of LEGACY_SLIDE_DROP_KEYS) {
167
+ if (key in props) {
168
+ droppedKeys.push(key);
169
+ delete props[key];
170
+ }
171
+ }
172
+
173
+ warnLegacySlideMigration(contextPath, componentId, droppedKeys);
174
+ return props;
175
+ }
176
+
177
+ function createStackPageWrapper(
178
+ child: Record<string, any> | null,
179
+ pageIndex: number
180
+ ): Record<string, any> {
181
+ return {
182
+ id: child?.id ? `${String(child.id)}-page` : `legacy-page-${pageIndex + 1}`,
183
+ type: 'stack',
184
+ properties: {
185
+ axis: 'vertical',
186
+ alignment: 'start',
187
+ distribution: 'start',
188
+ childSpacing: 0,
189
+ size: { width: 'fill', height: 'fit' },
190
+ layout: {
191
+ margin: { top: 0, right: 0, bottom: 0, left: 0 },
192
+ },
193
+ appearance: {
194
+ shape: 'rectangle',
195
+ cornerRadius: 0,
196
+ },
197
+ },
198
+ children: child ? [child] : [],
199
+ };
200
+ }
201
+
202
+ function migrateLegacySlideNodes(
203
+ input: unknown,
204
+ contextPath = 'screen'
205
+ ): unknown {
206
+ if (Array.isArray(input)) {
207
+ return input.map((item, index) =>
208
+ migrateLegacySlideNodes(item, `${contextPath}[${index}]`)
209
+ );
210
+ }
211
+ if (!input || typeof input !== 'object') return input;
212
+
213
+ const node = { ...(input as Record<string, any>) };
214
+ const rawType = String(node.type ?? '');
215
+
216
+ if (Array.isArray(node.children)) {
217
+ node.children = node.children.map((child, index) =>
218
+ migrateLegacySlideNodes(child, `${contextPath}.children[${index}]`)
219
+ );
220
+ }
221
+ if (node.child && typeof node.child === 'object') {
222
+ node.child = migrateLegacySlideNodes(node.child, `${contextPath}.child`);
223
+ }
224
+ if (Array.isArray(node.options)) {
225
+ node.options = node.options.map((option, index) => {
226
+ if (!option || typeof option !== 'object') return option;
227
+ const optionNode = { ...(option as Record<string, any>) };
228
+ if (optionNode.child && typeof optionNode.child === 'object') {
229
+ optionNode.child = migrateLegacySlideNodes(
230
+ optionNode.child,
231
+ `${contextPath}.options[${index}].child`
232
+ );
233
+ }
234
+ return optionNode;
235
+ });
236
+ }
237
+
238
+ if (rawType === 'slide') {
239
+ node.type = 'stack';
240
+ node.properties = normalizeLegacySlidePropsToStackProps(
241
+ (node.properties ?? {}) as Record<string, any>,
242
+ contextPath,
243
+ node.id ? String(node.id) : undefined
244
+ );
245
+ }
246
+
247
+ if (String(node.type ?? '') === 'slider') {
248
+ const children = Array.isArray(node.children) ? node.children : [];
249
+ const normalizedPages = children.map((child, index) => {
250
+ if (!child || typeof child !== 'object') {
251
+ warnLegacySliderChildWrap(
252
+ `${contextPath}.children[${index}]`,
253
+ undefined
254
+ );
255
+ return createStackPageWrapper(null, index);
256
+ }
257
+ const pageNode = child as Record<string, any>;
258
+ const childType = String(pageNode.type ?? '');
259
+ if (childType === 'stack') return pageNode;
260
+ warnLegacySliderChildWrap(`${contextPath}.children[${index}]`, childType);
261
+ return createStackPageWrapper(pageNode, index);
262
+ });
263
+ node.children =
264
+ normalizedPages.length > 0
265
+ ? normalizedPages
266
+ : [createStackPageWrapper(null, 0)];
267
+ }
268
+
269
+ return node;
270
+ }
271
+
272
+ function useGoogleFontLoadSignal(): void {
273
+ const [, setVersion] = useState(0);
274
+ useEffect(
275
+ () =>
276
+ subscribeFontLoad(() => {
277
+ setVersion((version) => version + 1);
278
+ }),
279
+ []
280
+ );
281
+ }
282
+
94
283
  export default function FlowboardRenderer(props: FlowboardRendererProps) {
95
284
  const {
96
285
  screenData,
@@ -101,27 +290,37 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
101
290
  currentIndex = 0,
102
291
  totalScreens = 1,
103
292
  } = props;
293
+ useGoogleFontLoadSignal();
104
294
 
105
- const childrenData: any[] = Array.isArray(screenData.children)
106
- ? screenData.children
295
+ const migratedScreenData = useMemo(
296
+ () =>
297
+ (migrateLegacySlideNodes(screenData, 'screen') as Record<string, any>) ??
298
+ {},
299
+ [screenData]
300
+ );
301
+
302
+ const childrenData: any[] = Array.isArray(migratedScreenData.children)
303
+ ? migratedScreenData.children
107
304
  : [];
108
305
 
109
- const backgroundData = screenData.background;
110
- const bgColorCode = screenData.backgroundColor;
306
+ const backgroundData = migratedScreenData.background;
307
+ const bgColorCode = migratedScreenData.backgroundColor;
111
308
 
112
- const showProgress = screenData.showProgress === true;
113
- const progressColor = parseColor(screenData.progressColor ?? '0xFF000000');
114
- const progressThickness = Number(screenData.progressThickness ?? 4);
115
- const progressRadius = Number(screenData.progressRadius ?? 0);
116
- const progressStyle = screenData.progressStyle ?? 'linear';
117
- const pageScroll = resolvePageScrollAxis(screenData);
309
+ const showProgress = migratedScreenData.showProgress === true;
310
+ const progressColor = parseColor(
311
+ migratedScreenData.progressColor ?? '0xFF000000'
312
+ );
313
+ const progressThickness = Number(migratedScreenData.progressThickness ?? 4);
314
+ const progressRadius = Number(migratedScreenData.progressRadius ?? 0);
315
+ const progressStyle = migratedScreenData.progressStyle ?? 'linear';
316
+ const pageScroll = resolvePageScrollAxis(migratedScreenData);
118
317
  const pageAxisBounds = getAxisBoundsForPageScroll(pageScroll);
119
318
  const scrollable = pageScroll !== 'none';
120
- const safeArea = screenData.safeArea !== false;
319
+ const safeArea = migratedScreenData.safeArea !== false;
121
320
  const safeAreaInsets = useSafeAreaInsets();
122
321
  const rootAxis: 'vertical' = 'vertical';
123
322
 
124
- const padding = parseInsets(screenData.padding);
323
+ const padding = parseInsets(migratedScreenData.padding);
125
324
  const contentPaddingStyle = insetsToStyle(padding);
126
325
  const progressPaddingStyle = {
127
326
  paddingTop: padding.top,
@@ -129,7 +328,7 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
129
328
  paddingLeft: padding.left,
130
329
  };
131
330
  const rootCrossAxisAlignment =
132
- screenData.crossAxisAlignment ?? screenData.crossAxis;
331
+ migratedScreenData.crossAxisAlignment ?? migratedScreenData.crossAxis;
133
332
  const safeAreaPaddingStyle = safeArea
134
333
  ? {
135
334
  paddingTop: safeAreaInsets.top,
@@ -162,6 +361,7 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
162
361
  Platform.OS === 'ios' ? 'interactive' : 'on-drag'
163
362
  }
164
363
  contentContainerStyle={{
364
+ flexGrow: 1,
165
365
  ...contentPaddingStyle,
166
366
  paddingTop: showProgress ? 0 : padding.top,
167
367
  }}
@@ -170,9 +370,11 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
170
370
  style={{
171
371
  flexGrow: 1,
172
372
  flexDirection: 'column',
173
- justifyContent: parseFlexAlignment(screenData.mainAxisAlignment),
373
+ justifyContent: parseFlexAlignment(
374
+ migratedScreenData.mainAxisAlignment
375
+ ),
174
376
  alignItems: parseRootCrossAlignment(rootCrossAxisAlignment),
175
- minHeight: 0,
377
+ minHeight: '100%',
176
378
  minWidth: 0,
177
379
  }}
178
380
  >
@@ -185,7 +387,7 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
185
387
  parentFlexAxis: rootAxis,
186
388
  parentMainAxisBounded: pageAxisBounds.heightBounded,
187
389
  pageAxisBounds,
188
- screenData,
390
+ screenData: migratedScreenData,
189
391
  enableFontAwesomeIcons,
190
392
  key: `child-${index}`,
191
393
  })
@@ -198,7 +400,9 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
198
400
  flex: 1,
199
401
  ...contentPaddingStyle,
200
402
  paddingTop: showProgress ? 0 : padding.top,
201
- justifyContent: parseFlexAlignment(screenData.mainAxisAlignment),
403
+ justifyContent: parseFlexAlignment(
404
+ migratedScreenData.mainAxisAlignment
405
+ ),
202
406
  alignItems: parseRootCrossAlignment(rootCrossAxisAlignment),
203
407
  }}
204
408
  >
@@ -211,7 +415,7 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
211
415
  parentFlexAxis: rootAxis,
212
416
  parentMainAxisBounded: true,
213
417
  pageAxisBounds,
214
- screenData,
418
+ screenData: migratedScreenData,
215
419
  enableFontAwesomeIcons,
216
420
  key: `child-${index}`,
217
421
  })
@@ -487,10 +691,7 @@ function normalizeStackAxis(type: string, props: Record<string, any>) {
487
691
  if (type === 'layout') {
488
692
  return props.direction === 'horizontal' ? 'horizontal' : 'vertical';
489
693
  }
490
- if (
491
- type === 'stack' &&
492
- (props.fit !== undefined || isLegacyOverlayAlignment(props.alignment))
493
- ) {
694
+ if (type === 'stack' && isLegacyOverlayAlignment(props.alignment)) {
494
695
  return 'overlay';
495
696
  }
496
697
  return 'vertical';
@@ -548,7 +749,7 @@ function normalizeOverlayAlignment(value: unknown) {
548
749
  case 'bottomRight':
549
750
  return 'bottomEnd';
550
751
  default:
551
- return 'center';
752
+ return 'topStart';
552
753
  }
553
754
  }
554
755
 
@@ -589,6 +790,67 @@ function normalizeStackSizeMode(
589
790
  return undefined;
590
791
  }
591
792
 
793
+ export function resolveFillSizeStyle(
794
+ size: Record<string, any> | null | undefined,
795
+ options?: { allowWidthFill?: boolean; allowHeightFill?: boolean }
796
+ ): ViewStyle {
797
+ const allowWidthFill = options?.allowWidthFill !== false;
798
+ const allowHeightFill = options?.allowHeightFill !== false;
799
+ const fillWidth =
800
+ allowWidthFill && normalizeStackSizeMode(size?.width) === 'fill';
801
+ const fillHeight =
802
+ allowHeightFill && normalizeStackSizeMode(size?.height) === 'fill';
803
+ const style: Record<string, any> = {};
804
+
805
+ if (fillWidth) {
806
+ style.width = '100%';
807
+ style.alignSelf = 'stretch';
808
+ style.minWidth = 0;
809
+ }
810
+ if (fillHeight) {
811
+ style.height = '100%';
812
+ style.flexGrow = 1;
813
+ style.flexShrink = 1;
814
+ style.flexBasis = 'auto';
815
+ style.minHeight = 0;
816
+ }
817
+ if (fillWidth && fillHeight) {
818
+ style.flexGrow = 1;
819
+ style.flexShrink = 1;
820
+ style.flexBasis = 'auto';
821
+ }
822
+
823
+ return style;
824
+ }
825
+
826
+ function resolveOverlayContainerFitStyle(
827
+ props: Record<string, any>,
828
+ axisBounds: AxisBounds
829
+ ): ViewStyle {
830
+ if (resolveFitBehavior(props.fit) !== 'expand') return {};
831
+
832
+ const widthMode = normalizeStackSizeMode(props.size?.width);
833
+ const heightMode = normalizeStackSizeMode(props.size?.height);
834
+ const style: Record<string, any> = {
835
+ minWidth: 0,
836
+ minHeight: 0,
837
+ };
838
+
839
+ if (axisBounds.widthBounded && widthMode !== 'fixed') {
840
+ style.width = '100%';
841
+ style.alignSelf = 'stretch';
842
+ }
843
+
844
+ if (axisBounds.heightBounded && heightMode !== 'fixed') {
845
+ style.height = '100%';
846
+ style.flexGrow = 1;
847
+ style.flexShrink = 1;
848
+ style.flexBasis = 'auto';
849
+ }
850
+
851
+ return style;
852
+ }
853
+
592
854
  function wantsFillWidth(props: Record<string, any>) {
593
855
  return (
594
856
  normalizeStackSizeMode(props.size?.width) === 'fill' ||
@@ -636,18 +898,36 @@ function normalizeStackFill(props: Record<string, any>) {
636
898
  if (props.fill && typeof props.fill === 'object') {
637
899
  const next = { ...props.fill };
638
900
  if (!next.type && next.color) next.type = 'solid';
901
+ if (String(next.type ?? '').toLowerCase() === 'color') {
902
+ next.type = 'solid';
903
+ }
904
+ const fillType = String(next.type ?? '').toLowerCase();
905
+ if (
906
+ (fillType === '' || fillType === 'solid') &&
907
+ (typeof next.color !== 'string' || next.color.trim().length === 0)
908
+ ) {
909
+ next.type = 'solid';
910
+ next.color = '0x00FFFFFF';
911
+ }
639
912
  return next;
640
913
  }
641
914
  if (props.background && typeof props.background === 'object') {
642
915
  if (props.background.type === 'color') {
643
- return { type: 'solid', color: props.background.color };
916
+ return {
917
+ type: 'solid',
918
+ color:
919
+ typeof props.background.color === 'string' &&
920
+ props.background.color.trim().length > 0
921
+ ? props.background.color
922
+ : '0x00FFFFFF',
923
+ };
644
924
  }
645
925
  return { ...props.background };
646
926
  }
647
927
  if (props.backgroundColor) {
648
928
  return { type: 'solid', color: props.backgroundColor };
649
929
  }
650
- return undefined;
930
+ return { type: 'solid', color: '0x00FFFFFF' };
651
931
  }
652
932
 
653
933
  function normalizeStackBorder(props: Record<string, any>) {
@@ -746,9 +1026,28 @@ function mergeStackProps(
746
1026
  },
747
1027
  };
748
1028
 
749
- if (axis === 'overlay') {
1029
+ if (
1030
+ axis === 'overlay' ||
1031
+ childProps.overlayAlignment !== undefined ||
1032
+ parentProps.overlayAlignment !== undefined ||
1033
+ childProps.mode === 'overlay' ||
1034
+ parentProps.mode === 'overlay'
1035
+ ) {
750
1036
  mergedProps.overlayAlignment =
751
- childProps.overlayAlignment ?? parentProps.overlayAlignment ?? 'center';
1037
+ childProps.overlayAlignment ?? parentProps.overlayAlignment ?? 'topStart';
1038
+ }
1039
+ if (childProps.mode !== undefined || parentProps.mode !== undefined) {
1040
+ mergedProps.mode = childProps.mode ?? parentProps.mode;
1041
+ }
1042
+ if (childProps.fit !== undefined || parentProps.fit !== undefined) {
1043
+ mergedProps.fit = childProps.fit ?? parentProps.fit;
1044
+ }
1045
+ if (
1046
+ childProps.clipBehavior !== undefined ||
1047
+ parentProps.clipBehavior !== undefined
1048
+ ) {
1049
+ mergedProps.clipBehavior =
1050
+ childProps.clipBehavior ?? parentProps.clipBehavior;
752
1051
  }
753
1052
 
754
1053
  if (parentProps.fill || childProps.fill) {
@@ -811,12 +1110,34 @@ function normalizeStackLikeNode(
811
1110
  },
812
1111
  };
813
1112
 
814
- if (axis === 'overlay') {
815
- normalizedProps.overlayAlignment = normalizeOverlayAlignment(
816
- props.overlayAlignment ?? props.alignment
817
- );
1113
+ if (props.mode === 'overlay' || props.mode === 'linear') {
1114
+ normalizedProps.mode = props.mode;
1115
+ }
1116
+
1117
+ if (
1118
+ props.fit === 'expand' ||
1119
+ props.fit === 'loose' ||
1120
+ String(props.fit).toLowerCase() === 'expand' ||
1121
+ String(props.fit).toLowerCase() === 'loose'
1122
+ ) {
1123
+ normalizedProps.fit =
1124
+ String(props.fit).toLowerCase() === 'expand' ? 'expand' : 'loose';
1125
+ }
1126
+
1127
+ if (
1128
+ String(props.clipBehavior ?? '').toLowerCase() === 'none' ||
1129
+ String(props.clipBehavior ?? '').toLowerCase() === 'hardedge'
1130
+ ) {
1131
+ normalizedProps.clipBehavior =
1132
+ String(props.clipBehavior).toLowerCase() === 'hardedge'
1133
+ ? 'hardEdge'
1134
+ : 'none';
818
1135
  }
819
1136
 
1137
+ normalizedProps.overlayAlignment = normalizeOverlayAlignment(
1138
+ props.overlayAlignment ?? props.alignment
1139
+ );
1140
+
820
1141
  const fill = normalizeStackFill(props);
821
1142
  if (fill) normalizedProps.fill = fill;
822
1143
 
@@ -862,32 +1183,6 @@ function normalizeStackLikeNode(
862
1183
  };
863
1184
  }
864
1185
 
865
- function parseOverlayGridAlignment(
866
- value?: string
867
- ): Pick<ViewStyle, 'justifyContent' | 'alignItems'> {
868
- switch (normalizeOverlayAlignment(value)) {
869
- case 'topStart':
870
- return { justifyContent: 'flex-start', alignItems: 'flex-start' };
871
- case 'top':
872
- return { justifyContent: 'flex-start', alignItems: 'center' };
873
- case 'topEnd':
874
- return { justifyContent: 'flex-start', alignItems: 'flex-end' };
875
- case 'start':
876
- return { justifyContent: 'center', alignItems: 'flex-start' };
877
- case 'end':
878
- return { justifyContent: 'center', alignItems: 'flex-end' };
879
- case 'bottomStart':
880
- return { justifyContent: 'flex-end', alignItems: 'flex-start' };
881
- case 'bottom':
882
- return { justifyContent: 'flex-end', alignItems: 'center' };
883
- case 'bottomEnd':
884
- return { justifyContent: 'flex-end', alignItems: 'flex-end' };
885
- case 'center':
886
- default:
887
- return { justifyContent: 'center', alignItems: 'center' };
888
- }
889
- }
890
-
891
1186
  function resolveStackDimension(
892
1187
  props: Record<string, any>,
893
1188
  axis: 'vertical' | 'horizontal' | 'overlay',
@@ -910,6 +1205,35 @@ function resolveStackDimension(
910
1205
  return undefined;
911
1206
  }
912
1207
 
1208
+ type TextWidthMode = 'fit' | 'fill' | 'fixed';
1209
+
1210
+ function resolveTextWidthMode(props: Record<string, any>): TextWidthMode {
1211
+ const raw = String(props.size?.width ?? '')
1212
+ .trim()
1213
+ .toLowerCase();
1214
+ if (
1215
+ raw === 'fill' ||
1216
+ raw === 'infinity' ||
1217
+ raw === 'infinite' ||
1218
+ raw === 'double.infinity' ||
1219
+ raw === '+infinity'
1220
+ ) {
1221
+ return 'fill';
1222
+ }
1223
+ if (raw === 'fit') return 'fit';
1224
+ if (raw === 'fixed') return 'fixed';
1225
+ if (isFillDimensionValue(props.width)) return 'fill';
1226
+ return props.width !== undefined ? 'fixed' : 'fit';
1227
+ }
1228
+
1229
+ function resolveTextFixedWidth(
1230
+ props: Record<string, any>
1231
+ ): number | string | undefined {
1232
+ const explicit = normalizeDimension(parseLayoutDimension(props.width, true));
1233
+ if (explicit !== undefined) return explicit;
1234
+ return normalizeDimension(parseLayoutDimension(props.size?.widthPx, true));
1235
+ }
1236
+
913
1237
  function buildWidget(
914
1238
  json: Record<string, any>,
915
1239
  params: {
@@ -1059,6 +1383,13 @@ function buildWidget(
1059
1383
  const gradient = parseGradient(props.foreground);
1060
1384
  const textStyle = getTextStyle(props, 'color');
1061
1385
  const align = parseTextAlign(props.textAlign);
1386
+ const widthMode = resolveTextWidthMode(props);
1387
+ const fixedWidth = resolveTextFixedWidth(props);
1388
+ const widthBounded = params.pageAxisBounds?.widthBounded ?? true;
1389
+ // Shared width-mode contract across React/Flutter:
1390
+ // fit = intrinsic width (no flex, no 100% stretch)
1391
+ // fill = expand within bounded parent constraints
1392
+ // fixed = explicit width value
1062
1393
  if (gradient) {
1063
1394
  node = (
1064
1395
  <GradientText
@@ -1072,6 +1403,31 @@ function buildWidget(
1072
1403
  <Text style={[textStyle, { textAlign: align }]}>{resolvedText}</Text>
1073
1404
  );
1074
1405
  }
1406
+ if (widthMode === 'fill') {
1407
+ if (params.parentFlexAxis === 'horizontal') {
1408
+ if (params.parentMainAxisBounded ?? true) {
1409
+ node = (
1410
+ <View style={{ flex: 1, minWidth: 0, alignSelf: 'stretch' }}>
1411
+ {node}
1412
+ </View>
1413
+ );
1414
+ } else {
1415
+ node = <View style={{ minWidth: 0 }}>{node}</View>;
1416
+ }
1417
+ } else if (widthBounded) {
1418
+ node = (
1419
+ <View style={{ width: '100%', minWidth: 0, alignSelf: 'stretch' }}>
1420
+ {node}
1421
+ </View>
1422
+ );
1423
+ } else {
1424
+ node = <View style={{ minWidth: 0 }}>{node}</View>;
1425
+ }
1426
+ } else if (widthMode === 'fixed') {
1427
+ node = <View style={{ width: fixedWidth, minWidth: 0 }}>{node}</View>;
1428
+ } else {
1429
+ node = <View style={{ minWidth: 0 }}>{node}</View>;
1430
+ }
1075
1431
  break;
1076
1432
  }
1077
1433
  case 'rich_text': {
@@ -1505,14 +1861,14 @@ function buildWidget(
1505
1861
  props.direction === 'vertical' ? 'vertical' : 'horizontal';
1506
1862
  const interaction = props.interaction ?? {};
1507
1863
  const pageControl = props.pageControl ?? {};
1508
- const sliderChildren = (childrenJson ?? []).filter(Boolean);
1864
+ const sliderPages = (childrenJson ?? []).filter(
1865
+ (child) => child && String(child.type ?? '') === 'stack'
1866
+ );
1509
1867
  node = (
1510
1868
  <SliderWidget
1511
- id={json._internalId}
1869
+ id={json._internalId ?? json.id}
1512
1870
  sliderProps={props}
1513
- slidePropsList={sliderChildren.map(
1514
- (child) => child?.properties ?? {}
1515
- )}
1871
+ pagePropsList={sliderPages.map((child) => child?.properties ?? {})}
1516
1872
  direction={direction}
1517
1873
  pageAlignment={
1518
1874
  props.pageAlignment === 'start' || props.pageAlignment === 'end'
@@ -1531,28 +1887,19 @@ function buildWidget(
1531
1887
  pageControl.position === 'top' ? 'top' : 'bottom'
1532
1888
  }
1533
1889
  >
1534
- {sliderChildren.map((child, index) => (
1890
+ {sliderPages.map((child, index) => (
1535
1891
  <View key={`slider-${index}`}>
1536
- {buildWidget(child, { ...params })}
1892
+ {buildWidget(child, {
1893
+ ...params,
1894
+ // Slider pages always render inside a bounded pager viewport.
1895
+ pageAxisBounds: { widthBounded: true, heightBounded: true },
1896
+ })}
1537
1897
  </View>
1538
1898
  ))}
1539
1899
  </SliderWidget>
1540
1900
  );
1541
1901
  break;
1542
1902
  }
1543
- case 'slide': {
1544
- node = buildWidget(
1545
- {
1546
- ...json,
1547
- type: 'stack',
1548
- properties: {
1549
- ...(props || {}),
1550
- },
1551
- },
1552
- { ...params }
1553
- );
1554
- break;
1555
- }
1556
1903
  case 'pageview_indicator': {
1557
1904
  node = (
1558
1905
  <PageViewIndicator
@@ -1597,19 +1944,22 @@ function buildWidget(
1597
1944
  break;
1598
1945
  }
1599
1946
  case 'stack': {
1600
- const axis =
1601
- props.axis === 'horizontal' || props.axis === 'overlay'
1602
- ? props.axis
1603
- : 'vertical';
1604
- const isOverlay = axis === 'overlay';
1947
+ const axis = props.axis === 'horizontal' ? 'horizontal' : 'vertical';
1948
+ const isOverlay = isOverlayStack(childrenJson, props);
1605
1949
  const spacing = Number(props.childSpacing ?? 0);
1606
1950
  const applySpacing = !STACK_SPACE_DISTRIBUTIONS.has(
1607
1951
  String(props.distribution ?? 'start')
1608
1952
  );
1609
- const width = resolveStackDimension(props, axis, 'width', pageAxisBounds);
1953
+ const resolvedAxis = isOverlay ? 'overlay' : axis;
1954
+ const width = resolveStackDimension(
1955
+ props,
1956
+ resolvedAxis,
1957
+ 'width',
1958
+ pageAxisBounds
1959
+ );
1610
1960
  const height = resolveStackDimension(
1611
1961
  props,
1612
- axis,
1962
+ resolvedAxis,
1613
1963
  'height',
1614
1964
  pageAxisBounds
1615
1965
  );
@@ -1661,14 +2011,27 @@ function buildWidget(
1661
2011
  : undefined;
1662
2012
  const stackColor =
1663
2013
  fill?.type === 'solid' || (!fill?.type && fill?.color)
1664
- ? parseColor(fill?.color ?? '#FFFFFFFF')
2014
+ ? parseColor(fill?.color ?? '0x00FFFFFF')
1665
2015
  : parseColor(props.backgroundColor ?? '#00000000');
1666
2016
 
1667
2017
  const baseStackStyle: any = {
1668
2018
  width,
1669
2019
  height,
2020
+ ...resolveFillSizeStyle(props.size, {
2021
+ allowWidthFill: pageAxisBounds.widthBounded,
2022
+ allowHeightFill: pageAxisBounds.heightBounded,
2023
+ }),
2024
+ ...(isOverlay
2025
+ ? resolveOverlayContainerFitStyle(props, pageAxisBounds)
2026
+ : {}),
1670
2027
  borderRadius: borderRadius > 0 ? borderRadius : undefined,
1671
- overflow: borderRadius > 0 ? 'hidden' : undefined,
2028
+ overflow: isOverlay
2029
+ ? resolveClipBehavior(props.clipBehavior) === 'hardEdge'
2030
+ ? 'hidden'
2031
+ : 'visible'
2032
+ : borderRadius > 0
2033
+ ? 'hidden'
2034
+ : undefined,
1672
2035
  borderWidth: borderColor ? borderWidth : undefined,
1673
2036
  borderColor: borderColor ?? undefined,
1674
2037
  ...shadowStyle,
@@ -1688,12 +2051,38 @@ function buildWidget(
1688
2051
  }}
1689
2052
  >
1690
2053
  {(childrenJson ?? []).map((child, index) => {
1691
- const alignment = parseOverlayGridAlignment(
1692
- props.overlayAlignment ?? props.alignment
2054
+ if (isPositionedChild(child)) {
2055
+ if (String(child?.type ?? '') === 'positioned') {
2056
+ return (
2057
+ <React.Fragment key={`stack-overlay-positioned-${index}`}>
2058
+ {buildWidget(child, {
2059
+ ...params,
2060
+ pageAxisBounds,
2061
+ })}
2062
+ </React.Fragment>
2063
+ );
2064
+ }
2065
+ return (
2066
+ <View
2067
+ key={`stack-overlay-positioned-${index}`}
2068
+ style={resolvePositionedStyle(
2069
+ (child?.properties ?? {}) as Record<string, any>
2070
+ )}
2071
+ >
2072
+ {buildWidget(child, {
2073
+ ...params,
2074
+ pageAxisBounds,
2075
+ })}
2076
+ </View>
2077
+ );
2078
+ }
2079
+
2080
+ const alignment = resolveAlignmentToCss(
2081
+ resolveOverlayAlignmentValue(props)
1693
2082
  );
1694
2083
  return (
1695
2084
  <View
1696
- key={`stack-overlay-${index}`}
2085
+ key={`stack-overlay-non-positioned-${index}`}
1697
2086
  style={{
1698
2087
  position: 'absolute',
1699
2088
  top: 0,
@@ -1772,24 +2161,8 @@ function buildWidget(
1772
2161
  break;
1773
2162
  }
1774
2163
  case 'positioned': {
1775
- const left = parseLayoutDimension(props.left);
1776
- const top = parseLayoutDimension(props.top);
1777
- const right = parseLayoutDimension(props.right);
1778
- const bottom = parseLayoutDimension(props.bottom);
1779
- const width = parseLayoutDimension(props.width);
1780
- const height = parseLayoutDimension(props.height);
1781
2164
  node = (
1782
- <View
1783
- style={{
1784
- position: 'absolute',
1785
- left,
1786
- top,
1787
- right,
1788
- bottom,
1789
- width,
1790
- height,
1791
- }}
1792
- >
2165
+ <View style={resolvePositionedStyle(props)}>
1793
2166
  {childJson ? buildWidget(childJson, { ...params }) : null}
1794
2167
  </View>
1795
2168
  );
@@ -1856,6 +2229,10 @@ function buildWidget(
1856
2229
  let shouldExpand = false;
1857
2230
  const parentAxis = params.parentFlexAxis ?? 'vertical';
1858
2231
  const parentMainAxisBounded = params.parentMainAxisBounded ?? true;
2232
+ const sliderWantsMainAxisFill =
2233
+ type === 'slider' &&
2234
+ ((parentAxis === 'vertical' && wantsFillHeight(props)) ||
2235
+ (parentAxis === 'horizontal' && wantsFillWidth(props)));
1859
2236
 
1860
2237
  if (parentMainAxisBounded) {
1861
2238
  if (type === 'stack') {
@@ -1889,9 +2266,25 @@ function buildWidget(
1889
2266
  }
1890
2267
  }
1891
2268
  }
2269
+ if (sliderWantsMainAxisFill) {
2270
+ shouldExpand = true;
2271
+ }
1892
2272
 
1893
2273
  if (shouldExpand) {
1894
- node = <View style={{ flex: 1, minHeight: 0, minWidth: 0 }}>{node}</View>;
2274
+ // Keep cross-axis stretching on the outer expansion wrapper as well.
2275
+ // Without this, a centered parent cross-axis can collapse effective fill width/height.
2276
+ node = (
2277
+ <View
2278
+ style={{
2279
+ flex: 1,
2280
+ minHeight: 0,
2281
+ minWidth: 0,
2282
+ alignSelf: shouldStretchCrossAxis ? 'stretch' : undefined,
2283
+ }}
2284
+ >
2285
+ {node}
2286
+ </View>
2287
+ );
1895
2288
  }
1896
2289
  }
1897
2290
 
@@ -1926,9 +2319,38 @@ function getTextStyle(
1926
2319
  colorKey: string,
1927
2320
  defaultColor?: string
1928
2321
  ): TextStyle {
1929
- const fontFamily = props.fontFamily;
2322
+ const requestedFontFamily = resolveFontFamily(props.fontFamily);
1930
2323
  const fontSize = Number(props.fontSize ?? 14);
1931
- const fontWeight = parseFontWeight(props.fontWeight);
2324
+ const parsedFontWeight = parseFontWeight(props.fontWeight);
2325
+ const requestedWeight = Number(parsedFontWeight);
2326
+ const numericRequestedWeight = Number.isFinite(requestedWeight)
2327
+ ? requestedWeight
2328
+ : undefined;
2329
+ let fontFamily = requestedFontFamily;
2330
+ let fontWeight = parsedFontWeight;
2331
+
2332
+ if (Platform.OS === 'web' && requestedFontFamily) {
2333
+ const resolvedSpec = resolveFontSpec({
2334
+ family: requestedFontFamily,
2335
+ requestedWeight: numericRequestedWeight,
2336
+ });
2337
+ fontFamily = resolvedSpec.resolvedFamily;
2338
+ fontWeight =
2339
+ resolvedSpec.resolvedWeight !== undefined
2340
+ ? String(resolvedSpec.resolvedWeight)
2341
+ : parsedFontWeight;
2342
+ ensureWebGoogleFontLoaded(resolvedSpec.resolvedFamily, [fontWeight]);
2343
+ } else if (requestedFontFamily) {
2344
+ const resolvedSpec = resolveFontSpec({
2345
+ family: requestedFontFamily,
2346
+ requestedWeight: numericRequestedWeight,
2347
+ });
2348
+ void ensureNativeFontLoaded(resolvedSpec).catch(() => undefined);
2349
+ fontWeight = undefined;
2350
+ fontFamily = isFontLoaded(resolvedSpec)
2351
+ ? resolveAppliedFontFamily(resolvedSpec)
2352
+ : resolvedSpec.resolvedFamily;
2353
+ }
1932
2354
  const fontStyle = props.fontStyle === 'italic' ? 'italic' : 'normal';
1933
2355
  const color = parseColor(props[colorKey] ?? defaultColor ?? '0xFF000000');
1934
2356
  const letterSpacing =
@@ -1964,9 +2386,13 @@ function parseGradient(value: any):
1964
2386
  if (colors.length === 0) return undefined;
1965
2387
  const start = alignmentToGradient(value.begin ?? 'topLeft');
1966
2388
  const end = alignmentToGradient(value.end ?? 'bottomRight');
1967
- const stops = Array.isArray(value.stops)
2389
+ const parsedStops = Array.isArray(value.stops)
1968
2390
  ? value.stops.map((s: number) => Number(s))
1969
2391
  : undefined;
2392
+ const stops =
2393
+ parsedStops && parsedStops.length === colors.length
2394
+ ? parsedStops
2395
+ : undefined;
1970
2396
  return { colors, start, end, stops };
1971
2397
  }
1972
2398
 
@@ -2419,10 +2845,26 @@ function GradientText({
2419
2845
  gradient: { colors: string[]; start: any; end: any; stops?: number[] };
2420
2846
  style?: any;
2421
2847
  }) {
2848
+ if (Platform.OS === 'web') {
2849
+ return (
2850
+ <Text style={[style, getWebGradientTextStyle(gradient)]}>{text}</Text>
2851
+ );
2852
+ }
2853
+
2422
2854
  return (
2423
2855
  <MaskedView
2424
2856
  maskElement={
2425
- <Text style={[style, { backgroundColor: 'transparent' }]}>{text}</Text>
2857
+ <Text
2858
+ style={[
2859
+ style,
2860
+ {
2861
+ color: '#FFFFFF',
2862
+ backgroundColor: 'transparent',
2863
+ },
2864
+ ]}
2865
+ >
2866
+ {text}
2867
+ </Text>
2426
2868
  }
2427
2869
  >
2428
2870
  <LinearGradient
@@ -2431,12 +2873,48 @@ function GradientText({
2431
2873
  end={gradient.end}
2432
2874
  locations={gradient.stops}
2433
2875
  >
2434
- <Text style={[style, { opacity: 0 }]}>{text}</Text>
2876
+ <Text style={[style, { color: '#FFFFFF', opacity: 0 }]}>{text}</Text>
2435
2877
  </LinearGradient>
2436
2878
  </MaskedView>
2437
2879
  );
2438
2880
  }
2439
2881
 
2882
+ function getWebGradientTextStyle(gradient: {
2883
+ colors: string[];
2884
+ start: { x: number; y: number };
2885
+ end: { x: number; y: number };
2886
+ stops?: number[];
2887
+ }) {
2888
+ const angle = gradientToCssAngle(gradient.start, gradient.end);
2889
+ const normalizedStops =
2890
+ gradient.stops && gradient.stops.length === gradient.colors.length
2891
+ ? gradient.stops.map((stop) => `${clamp01(stop) * 100}%`)
2892
+ : undefined;
2893
+ const gradientStops = gradient.colors.map((color, index) => {
2894
+ if (!normalizedStops) return color;
2895
+ return `${color} ${normalizedStops[index]}`;
2896
+ });
2897
+ return {
2898
+ backgroundImage: `linear-gradient(${angle}deg, ${gradientStops.join(
2899
+ ', '
2900
+ )})`,
2901
+ backgroundClip: 'text',
2902
+ WebkitBackgroundClip: 'text',
2903
+ color: 'transparent',
2904
+ };
2905
+ }
2906
+
2907
+ function gradientToCssAngle(
2908
+ start: { x: number; y: number },
2909
+ end: { x: number; y: number }
2910
+ ) {
2911
+ const dx = end.x - start.x;
2912
+ const dy = end.y - start.y;
2913
+ const radians = Math.atan2(dy, dx);
2914
+ const degrees = (radians * 180) / Math.PI;
2915
+ return (degrees + 90 + 360) % 360;
2916
+ }
2917
+
2440
2918
  type TextInputStrokeStyle = 'solid' | 'dashed' | 'dotted';
2441
2919
 
2442
2920
  type NormalizedTextInputStroke = {
@@ -3590,11 +4068,20 @@ function FakeProgressBar({
3590
4068
  }
3591
4069
 
3592
4070
  return (
3593
- <View>
4071
+ <View
4072
+ style={{
4073
+ width: '100%',
4074
+ alignSelf: 'stretch',
4075
+ minWidth: 0,
4076
+ flexDirection: 'column',
4077
+ }}
4078
+ >
3594
4079
  <View
3595
4080
  style={{
3596
4081
  height: thickness,
3597
4082
  width: '100%',
4083
+ alignSelf: 'stretch',
4084
+ minWidth: 0,
3598
4085
  borderRadius,
3599
4086
  backgroundColor,
3600
4087
  overflow: 'hidden',
@@ -3613,7 +4100,12 @@ function FakeProgressBar({
3613
4100
  />
3614
4101
  </View>
3615
4102
  {showProgressText ? (
3616
- <Text style={[progressTextStyle, { marginTop: 8 }]}>
4103
+ <Text
4104
+ style={[
4105
+ progressTextStyle,
4106
+ { marginTop: 8, width: '100%', alignSelf: 'stretch' },
4107
+ ]}
4108
+ >
3617
4109
  {progressText}
3618
4110
  </Text>
3619
4111
  ) : null}
@@ -3650,16 +4142,36 @@ function resolveSliderSizeMode(
3650
4142
  fallback: SliderSizeMode
3651
4143
  ): SliderSizeMode {
3652
4144
  const raw = String(sliderProps?.size?.[key] ?? '').toLowerCase();
3653
- if (raw === 'fill' || raw === 'fit' || raw === 'fixed') return raw;
4145
+ if (
4146
+ raw === 'fill' ||
4147
+ raw === 'infinity' ||
4148
+ raw === 'infinite' ||
4149
+ raw === 'double.infinity' ||
4150
+ raw === '+infinity'
4151
+ ) {
4152
+ return 'fill';
4153
+ }
4154
+ if (raw === 'fit' || raw === 'fixed') return raw;
4155
+ if (isFillDimensionValue(sliderProps?.[key])) return 'fill';
3654
4156
  return sliderProps?.[key] !== undefined ? 'fixed' : fallback;
3655
4157
  }
3656
4158
 
3657
- function resolveSlideSizeMode(
3658
- slideProps: Record<string, any>,
4159
+ function resolvePageSizeMode(
4160
+ pageProps: Record<string, any>,
3659
4161
  key: 'width' | 'height'
3660
4162
  ): SliderSizeMode {
3661
- const raw = String(slideProps?.size?.[key] ?? '').toLowerCase();
3662
- if (raw === 'fill' || raw === 'fit' || raw === 'fixed') return raw;
4163
+ const raw = String(pageProps?.size?.[key] ?? '').toLowerCase();
4164
+ if (
4165
+ raw === 'fill' ||
4166
+ raw === 'infinity' ||
4167
+ raw === 'infinite' ||
4168
+ raw === 'double.infinity' ||
4169
+ raw === '+infinity'
4170
+ ) {
4171
+ return 'fill';
4172
+ }
4173
+ if (raw === 'fit' || raw === 'fixed') return raw;
4174
+ if (isFillDimensionValue(pageProps?.[key])) return 'fill';
3663
4175
  return key === 'width' ? 'fill' : 'fit';
3664
4176
  }
3665
4177
 
@@ -3678,24 +4190,31 @@ export function resolveSliderContainerSize(args: {
3678
4190
  heightMode: SliderSizeMode;
3679
4191
  availableWidth: number;
3680
4192
  availableHeight: number;
3681
- activeSlideWidth: number;
3682
- activeSlideHeight: number;
4193
+ activePageWidth: number;
4194
+ activePageHeight: number;
3683
4195
  fixedWidth: number | null;
3684
4196
  fixedHeight: number | null;
3685
4197
  leadingPeek: number;
3686
4198
  trailingPeek: number;
3687
4199
  showDots: boolean;
4200
+ dotsTopInset?: number;
4201
+ dotsBottomInset?: number;
3688
4202
  }): {
3689
4203
  outerWidth: number | string;
3690
4204
  outerHeight: number | string;
3691
4205
  viewportWidth: number;
3692
4206
  viewportHeight: number;
3693
4207
  } {
3694
- const dotsInset = args.showDots ? 8 + 6 + 8 : 0;
4208
+ const dotsTopInset = Math.max(0, args.dotsTopInset ?? 0);
4209
+ const dotsBottomInset = Math.max(
4210
+ 0,
4211
+ args.dotsBottomInset ?? (args.showDots ? 18 : 0)
4212
+ );
4213
+ const dotsInset = dotsTopInset + dotsBottomInset;
3695
4214
  const fitPrimary =
3696
4215
  args.direction === 'horizontal'
3697
- ? args.activeSlideWidth
3698
- : args.activeSlideHeight;
4216
+ ? args.activePageWidth
4217
+ : args.activePageHeight;
3699
4218
  const fitViewportPrimary = Math.max(
3700
4219
  1,
3701
4220
  fitPrimary + args.leadingPeek + args.trailingPeek
@@ -3704,11 +4223,11 @@ export function resolveSliderContainerSize(args: {
3704
4223
  const viewportWidth =
3705
4224
  args.direction === 'horizontal'
3706
4225
  ? fitViewportPrimary
3707
- : Math.max(1, args.activeSlideWidth);
4226
+ : Math.max(1, args.activePageWidth);
3708
4227
  const viewportHeight =
3709
4228
  args.direction === 'vertical'
3710
4229
  ? fitViewportPrimary
3711
- : Math.max(1, args.activeSlideHeight);
4230
+ : Math.max(1, args.activePageHeight);
3712
4231
 
3713
4232
  const fitOuterWidth = Math.min(
3714
4233
  Math.max(1, viewportWidth),
@@ -3743,23 +4262,47 @@ export function resolveSliderContainerSize(args: {
3743
4262
  };
3744
4263
  }
3745
4264
 
3746
- export function resolveSlidePageSize(args: {
3747
- slideProps: Record<string, any>;
4265
+ function resolvePageFixedSizeHint(pageProps: Record<string, any>): {
4266
+ width: number | null;
4267
+ height: number | null;
4268
+ } {
4269
+ const widthMode = resolvePageSizeMode(pageProps, 'width');
4270
+ const heightMode = resolvePageSizeMode(pageProps, 'height');
4271
+ const width =
4272
+ widthMode === 'fixed'
4273
+ ? toFinite(
4274
+ parseLayoutDimension(pageProps.width ?? pageProps.size?.widthPx, true)
4275
+ )
4276
+ : null;
4277
+ const height =
4278
+ heightMode === 'fixed'
4279
+ ? toFinite(
4280
+ parseLayoutDimension(
4281
+ pageProps.height ?? pageProps.size?.heightPx,
4282
+ true
4283
+ )
4284
+ )
4285
+ : null;
4286
+ return { width, height };
4287
+ }
4288
+
4289
+ export function resolvePageSize(args: {
4290
+ pageProps: Record<string, any>;
3748
4291
  direction: 'horizontal' | 'vertical';
3749
4292
  pagePrimary: number;
3750
4293
  crossSize: number | string;
3751
4294
  }): { width: number | string; height: number | string } {
3752
- const widthMode = resolveSlideSizeMode(args.slideProps, 'width');
3753
- const heightMode = resolveSlideSizeMode(args.slideProps, 'height');
4295
+ const widthMode = resolvePageSizeMode(args.pageProps, 'width');
4296
+ const heightMode = resolvePageSizeMode(args.pageProps, 'height');
3754
4297
  const fixedWidth = toFinite(
3755
4298
  parseLayoutDimension(
3756
- args.slideProps.width ?? args.slideProps.size?.widthPx,
4299
+ args.pageProps.width ?? args.pageProps.size?.widthPx,
3757
4300
  true
3758
4301
  )
3759
4302
  );
3760
4303
  const fixedHeight = toFinite(
3761
4304
  parseLayoutDimension(
3762
- args.slideProps.height ?? args.slideProps.size?.heightPx,
4305
+ args.pageProps.height ?? args.pageProps.size?.heightPx,
3763
4306
  true
3764
4307
  )
3765
4308
  );
@@ -3918,7 +4461,7 @@ function resolveSliderBackgroundColor(
3918
4461
  function SliderWidget({
3919
4462
  id,
3920
4463
  sliderProps,
3921
- slidePropsList,
4464
+ pagePropsList,
3922
4465
  direction,
3923
4466
  pageAlignment,
3924
4467
  pageSpacing,
@@ -3932,7 +4475,7 @@ function SliderWidget({
3932
4475
  }: {
3933
4476
  id?: string;
3934
4477
  sliderProps: Record<string, any>;
3935
- slidePropsList: Record<string, any>[];
4478
+ pagePropsList: Record<string, any>[];
3936
4479
  direction: 'horizontal' | 'vertical';
3937
4480
  pageAlignment: 'start' | 'center' | 'end';
3938
4481
  pageSpacing: number;
@@ -3956,7 +4499,10 @@ function SliderWidget({
3956
4499
  width: 1,
3957
4500
  height: 1,
3958
4501
  });
3959
- const [activeSlideSize, setActiveSlideSize] = React.useState({
4502
+ const [measuredPageSizes, setMeasuredPageSizes] = React.useState<
4503
+ Record<number, { width: number; height: number }>
4504
+ >({});
4505
+ const [activePageSize, setActivePageSize] = React.useState({
3960
4506
  width: 1,
3961
4507
  height: 1,
3962
4508
  });
@@ -3973,6 +4519,22 @@ function SliderWidget({
3973
4519
  setCurrentPage((prev) => clampSliderPage(prev, pageCount));
3974
4520
  }, [pageCount]);
3975
4521
 
4522
+ React.useEffect(() => {
4523
+ setMeasuredPageSizes((prev) => {
4524
+ const next: Record<number, { width: number; height: number }> = {};
4525
+ Object.keys(prev).forEach((key) => {
4526
+ const index = Number(key);
4527
+ if (Number.isFinite(index) && index < pageCount) {
4528
+ const cached = prev[index];
4529
+ if (cached) next[index] = cached;
4530
+ }
4531
+ });
4532
+ return Object.keys(next).length === Object.keys(prev).length
4533
+ ? prev
4534
+ : next;
4535
+ });
4536
+ }, [pageCount]);
4537
+
3976
4538
  React.useEffect(() => {
3977
4539
  if (autoAdvance && !isInteracting && pageLength > 1) {
3978
4540
  timerRef.current = setInterval(() => {
@@ -4002,6 +4564,30 @@ function SliderWidget({
4002
4564
  }
4003
4565
  }, [currentPage, pageCount]);
4004
4566
 
4567
+ const syncActivePageSize = React.useCallback(
4568
+ (page: number) => {
4569
+ if (pageCount <= 0) return;
4570
+ const safePage = clampSliderPage(page, pageCount);
4571
+ const measured = measuredPageSizes[safePage];
4572
+ const hint = resolvePageFixedSizeHint(pagePropsList[safePage] ?? {});
4573
+ setActivePageSize((prev) => {
4574
+ const next = {
4575
+ width: Math.max(1, hint.width ?? measured?.width ?? prev.width),
4576
+ height: Math.max(1, hint.height ?? measured?.height ?? prev.height),
4577
+ };
4578
+ return prev.width === next.width && prev.height === next.height
4579
+ ? prev
4580
+ : next;
4581
+ });
4582
+ },
4583
+ [measuredPageSizes, pageCount, pagePropsList]
4584
+ );
4585
+
4586
+ React.useEffect(() => {
4587
+ if (pageCount <= 0) return;
4588
+ syncActivePageSize(currentPage);
4589
+ }, [currentPage, pageCount, syncActivePageSize]);
4590
+
4005
4591
  React.useEffect(() => {
4006
4592
  if (registry && id && children.length > 0) {
4007
4593
  registry.update(id, currentPage % children.length, children.length);
@@ -4015,10 +4601,12 @@ function SliderWidget({
4015
4601
  // 2) trailingPeek = end?0 : start?pagePeek*2 : pagePeek
4016
4602
  // 3) pagePrimary = viewportPrimary - leadingPeek - trailingPeek (clamped >= 1)
4017
4603
  // 4) track offset = activeIndex * (pagePrimary + pageSpacing)
4018
- // 5) fit parent mode follows active slide size but is clamped to container width
4604
+ // 5) fit parent mode follows active page size but is clamped to container width
4019
4605
  // 6) viewport always clips track content (no root horizontal overflow)
4020
4606
  const { leading, trailing } = resolvePeekInsets(pageAlignment, pagePeek);
4021
4607
  const showDots = pageControlEnabled && pageLength > 1;
4608
+ const dotsTopInset = showDots && pageControlPosition === 'top' ? 18 : 0;
4609
+ const dotsBottomInset = showDots && pageControlPosition !== 'top' ? 18 : 0;
4022
4610
  const widthMode = resolveSliderSizeMode(sliderProps, 'width', 'fill');
4023
4611
  const heightMode = resolveSliderSizeMode(sliderProps, 'height', 'fit');
4024
4612
  const fixedWidth = toFinite(parseLayoutDimension(sliderProps.width, true));
@@ -4030,14 +4618,17 @@ function SliderWidget({
4030
4618
  heightMode,
4031
4619
  availableWidth: availableSize.width,
4032
4620
  availableHeight: availableSize.height,
4033
- activeSlideWidth: activeSlideSize.width,
4034
- activeSlideHeight: activeSlideSize.height,
4621
+ activePageWidth: activePageSize.width,
4622
+ activePageHeight: activePageSize.height,
4035
4623
  fixedWidth,
4036
4624
  fixedHeight,
4037
4625
  leadingPeek: leading,
4038
4626
  trailingPeek: trailing,
4039
4627
  showDots,
4628
+ dotsTopInset,
4629
+ dotsBottomInset,
4040
4630
  });
4631
+ const sliderFillSizeStyle = resolveFillSizeStyle(sliderProps.size);
4041
4632
  const viewportPrimary =
4042
4633
  direction === 'horizontal'
4043
4634
  ? resolved.viewportWidth
@@ -4069,63 +4660,50 @@ function SliderWidget({
4069
4660
  style={{
4070
4661
  width: resolved.outerWidth,
4071
4662
  height: resolved.outerHeight,
4072
- minWidth: 1,
4073
- minHeight: 1,
4663
+ minWidth: 0,
4664
+ minHeight: 0,
4665
+ display: 'flex',
4666
+ flexDirection: 'column',
4667
+ ...sliderFillSizeStyle,
4074
4668
  backgroundColor: sliderBackgroundColor,
4075
4669
  borderRadius: 0,
4076
- overflow: 'visible',
4670
+ overflow: 'hidden',
4671
+ position: 'relative',
4077
4672
  }}
4078
4673
  >
4079
- {showDots && pageControlPosition === 'top' ? (
4080
- <View
4081
- style={{
4082
- marginBottom: 8,
4083
- flexDirection: 'row',
4084
- justifyContent: 'center',
4085
- alignItems: 'center',
4086
- }}
4087
- >
4088
- {Array.from({ length: pageLength }).map((_, index) => {
4089
- const isActive = index === currentPage;
4090
- return (
4091
- <View
4092
- key={`slider-dot-top-${index}`}
4093
- style={{
4094
- width: 6,
4095
- height: 6,
4096
- borderRadius: 3,
4097
- marginHorizontal: 3,
4098
- backgroundColor: isActive
4099
- ? parseColor('0xFF111827')
4100
- : parseColor('0xFFD1D5DB'),
4101
- }}
4102
- />
4103
- );
4104
- })}
4105
- </View>
4106
- ) : null}
4107
-
4108
4674
  <View
4109
4675
  style={{
4110
4676
  overflow: 'hidden',
4677
+ width: widthMode === 'fit' ? resolved.viewportWidth : '100%',
4678
+ height: heightMode === 'fit' ? resolved.viewportHeight : '100%',
4679
+ minWidth: 0,
4680
+ minHeight: 0,
4681
+ display: 'flex',
4682
+ flexDirection: 'column',
4683
+ flexGrow: heightMode === 'fit' ? 0 : 1,
4684
+ flexShrink: 1,
4111
4685
  ...(direction === 'horizontal'
4112
4686
  ? {
4113
- width: resolved.viewportWidth,
4114
- height: resolved.viewportHeight,
4115
4687
  paddingLeft: leading,
4116
4688
  paddingRight: trailing,
4689
+ paddingTop: dotsTopInset,
4690
+ paddingBottom: dotsBottomInset,
4117
4691
  }
4118
4692
  : {
4119
- width: resolved.viewportWidth,
4120
- height: resolved.viewportHeight,
4121
- paddingTop: leading,
4122
- paddingBottom: trailing,
4693
+ paddingTop: leading + dotsTopInset,
4694
+ paddingBottom: trailing + dotsBottomInset,
4123
4695
  }),
4124
4696
  }}
4125
4697
  >
4126
4698
  <PagerView
4127
4699
  ref={pagerRef}
4128
- style={{ width: '100%', height: '100%', minHeight: 1 }}
4700
+ style={{
4701
+ width: '100%',
4702
+ height: '100%',
4703
+ minWidth: 0,
4704
+ minHeight: 0,
4705
+ flex: 1,
4706
+ }}
4129
4707
  orientation={direction}
4130
4708
  pageMargin={Math.max(0, pageSpacing)}
4131
4709
  initialPage={clampSliderPage(currentPage, pageCount)}
@@ -4133,6 +4711,7 @@ function SliderWidget({
4133
4711
  const page = event.nativeEvent.position;
4134
4712
  const safePage = clampSliderPage(page, pageCount);
4135
4713
  setCurrentPage(safePage);
4714
+ syncActivePageSize(safePage);
4136
4715
  markInteracting();
4137
4716
  if (registry && id) {
4138
4717
  registry.update(id, safePage % pageLength, pageLength);
@@ -4142,56 +4721,115 @@ function SliderWidget({
4142
4721
  {Array.from({ length: pageCount }).map((_, index) => (
4143
4722
  <View
4144
4723
  key={`slider-page-${index}`}
4145
- onLayout={
4146
- index === currentPage
4147
- ? (event) => {
4148
- const nextWidth = Math.max(
4149
- 1,
4150
- Math.round(event.nativeEvent.layout.width)
4151
- );
4152
- const nextHeight = Math.max(
4153
- 1,
4154
- Math.round(event.nativeEvent.layout.height)
4155
- );
4156
- setActiveSlideSize((prev) =>
4157
- prev.width === nextWidth && prev.height === nextHeight
4158
- ? prev
4159
- : { width: nextWidth, height: nextHeight }
4160
- );
4161
- }
4162
- : undefined
4163
- }
4164
4724
  style={{
4165
- alignSelf: 'flex-start',
4725
+ width: '100%',
4726
+ height: '100%',
4166
4727
  overflow: 'hidden',
4167
- ...(resolveSlidePageSize({
4168
- slideProps: slidePropsList[index] ?? {},
4169
- direction,
4170
- pagePrimary,
4171
- crossSize: '100%',
4172
- }) as ViewStyle),
4728
+ minWidth: 0,
4729
+ minHeight: 0,
4730
+ display: 'flex',
4731
+ flexDirection: 'column',
4173
4732
  }}
4174
4733
  >
4175
- {children[index % pageLength]}
4734
+ <View
4735
+ onLayout={(event) => {
4736
+ const nextWidth = Math.max(
4737
+ 1,
4738
+ Math.round(event.nativeEvent.layout.width)
4739
+ );
4740
+ const nextHeight = Math.max(
4741
+ 1,
4742
+ Math.round(event.nativeEvent.layout.height)
4743
+ );
4744
+
4745
+ setMeasuredPageSizes((prev) => {
4746
+ const current = prev[index];
4747
+ if (
4748
+ current &&
4749
+ current.width === nextWidth &&
4750
+ current.height === nextHeight
4751
+ ) {
4752
+ return prev;
4753
+ }
4754
+ return {
4755
+ ...prev,
4756
+ [index]: { width: nextWidth, height: nextHeight },
4757
+ };
4758
+ });
4759
+
4760
+ if (index === currentPage) {
4761
+ setActivePageSize((prev) =>
4762
+ prev.width === nextWidth && prev.height === nextHeight
4763
+ ? prev
4764
+ : { width: nextWidth, height: nextHeight }
4765
+ );
4766
+ }
4767
+ }}
4768
+ style={{
4769
+ alignSelf: (
4770
+ direction === 'horizontal'
4771
+ ? resolvePageSizeMode(
4772
+ pagePropsList[index] ?? {},
4773
+ 'height'
4774
+ ) === 'fill'
4775
+ : resolvePageSizeMode(
4776
+ pagePropsList[index] ?? {},
4777
+ 'width'
4778
+ ) === 'fill'
4779
+ )
4780
+ ? 'stretch'
4781
+ : 'flex-start',
4782
+ overflow: 'hidden',
4783
+ minWidth: 0,
4784
+ minHeight: 0,
4785
+ display: 'flex',
4786
+ flexDirection: 'column',
4787
+ ...(resolvePageSize({
4788
+ pageProps: pagePropsList[index] ?? {},
4789
+ direction,
4790
+ pagePrimary,
4791
+ crossSize: '100%',
4792
+ }) as ViewStyle),
4793
+ }}
4794
+ >
4795
+ <View
4796
+ style={{
4797
+ width: '100%',
4798
+ height: '100%',
4799
+ minWidth: 0,
4800
+ minHeight: 0,
4801
+ display: 'flex',
4802
+ flexDirection: 'column',
4803
+ flexGrow: 1,
4804
+ }}
4805
+ >
4806
+ {children[index % pageLength]}
4807
+ </View>
4808
+ </View>
4176
4809
  </View>
4177
4810
  ))}
4178
4811
  </PagerView>
4179
4812
  </View>
4180
4813
 
4181
- {showDots && pageControlPosition === 'bottom' ? (
4814
+ {showDots ? (
4182
4815
  <View
4183
4816
  style={{
4184
- marginTop: 8,
4817
+ position: 'absolute',
4818
+ left: 0,
4819
+ right: 0,
4820
+ ...(pageControlPosition === 'top' ? { top: 4 } : { bottom: 4 }),
4821
+ height: 12,
4185
4822
  flexDirection: 'row',
4186
4823
  justifyContent: 'center',
4187
4824
  alignItems: 'center',
4825
+ pointerEvents: 'none',
4188
4826
  }}
4189
4827
  >
4190
4828
  {Array.from({ length: pageLength }).map((_, index) => {
4191
4829
  const isActive = index === currentPage;
4192
4830
  return (
4193
4831
  <View
4194
- key={`slider-dot-bottom-${index}`}
4832
+ key={`slider-dot-${index}`}
4195
4833
  style={{
4196
4834
  width: 6,
4197
4835
  height: 6,
@@ -4235,12 +4873,16 @@ function PageViewIndicator({
4235
4873
  }) {
4236
4874
  const registry = useSliderRegistry();
4237
4875
  const [current, setCurrent] = React.useState(0);
4876
+ const [count, setCount] = React.useState(0);
4238
4877
  const indicatorStyle = style ?? 'dots';
4239
- const count = linkedTo && registry ? registry.getPageCount(linkedTo) : 0;
4240
4878
 
4241
4879
  React.useEffect(() => {
4242
4880
  if (!linkedTo || !registry) return;
4243
- return registry.getNotifier(linkedTo, setCurrent);
4881
+ setCount(registry.getPageCount(linkedTo));
4882
+ return registry.getNotifier(linkedTo, (page) => {
4883
+ setCurrent(page);
4884
+ setCount(registry.getPageCount(linkedTo));
4885
+ });
4244
4886
  }, [linkedTo, registry]);
4245
4887
 
4246
4888
  if (!linkedTo || !registry || count <= 1) return null;