@applicaster/zapp-react-native-ui-components 16.0.0-alpha.2529997432 → 16.0.0-alpha.2663208063

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 (42) hide show
  1. package/Components/AudioPlayer/tv/__tests__/audioPlayer.test.js +19 -0
  2. package/Components/AudioPlayer/tv/__tests__/index.test.tsx +120 -0
  3. package/Components/AudioPlayer/tv/index.tsx +5 -1
  4. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/__tests__/withDimensions.test.ts +420 -125
  5. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/index.ts +114 -19
  6. package/Components/MasterCell/DefaultComponents/Image/hoc/withDimensions.tsx +19 -8
  7. package/Components/ModalComponent/AudioPlayer/Components/Action.tsx +0 -18
  8. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +17 -24
  9. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +25 -73
  10. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +0 -18
  11. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +75 -79
  12. package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +89 -0
  13. package/Components/ModalComponent/AudioPlayer/Components/index.ts +3 -1
  14. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +199 -0
  15. package/Components/ModalComponent/BottomSheetDragArea.tsx +33 -0
  16. package/Components/ModalComponent/BottomSheetModalContent.tsx +385 -60
  17. package/Components/ModalComponent/Button/index.tsx +5 -3
  18. package/Components/ModalComponent/Header/index.tsx +20 -7
  19. package/Components/ModalComponent/Header/utils.ts +1 -1
  20. package/Components/ModalComponent/ModalBlocksStyleContext.tsx +10 -0
  21. package/Components/ModalComponent/SortableList.tsx +142 -0
  22. package/Components/ModalComponent/SortableList.web.tsx +102 -0
  23. package/Components/ModalComponent/TextInputContent.tsx +100 -0
  24. package/Components/ModalComponent/__tests__/BottomSheetDragArea.test.tsx +74 -0
  25. package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +238 -0
  26. package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +120 -0
  27. package/Components/ModalComponent/__tests__/showTextInput.test.ts +95 -0
  28. package/Components/ModalComponent/index.tsx +11 -0
  29. package/Components/ModalComponent/presets/__tests__/getCell.test.ts +66 -0
  30. package/Components/ModalComponent/presets/dynamicCollectionCell.tsx +78 -0
  31. package/Components/ModalComponent/presets/index.ts +27 -0
  32. package/Components/ModalComponent/presets/selectableCell.tsx +28 -0
  33. package/Components/ModalComponent/presets/standardCell.tsx +41 -0
  34. package/Components/ModalComponent/presets/types.ts +35 -0
  35. package/Components/ModalComponent/showTextInput.ts +43 -0
  36. package/Components/ModalComponent/utils.ts +76 -20
  37. package/Components/PlayerContainer/PlayerContainer.tsx +11 -7
  38. package/Components/PlayerContainer/ProgramInfo/index.tsx +3 -4
  39. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +66 -13
  40. package/Components/VideoLive/LiveImageManager.ts +1 -1
  41. package/package.json +6 -5
  42. /package/Decorators/ZappPipesDataConnector/__tests__/{Hero.js → Hero.tsx} +0 -0
@@ -1,15 +1,85 @@
1
- import {
2
- toFiniteNumberWithDefault,
3
- toNumber,
4
- } from "@applicaster/zapp-react-native-utils/numberUtils";
1
+ import { toFiniteNumberWithDefault } from "@applicaster/zapp-react-native-utils/numberUtils";
5
2
  import { isNil } from "ramda";
3
+ import { coreLogger } from "@applicaster/zapp-react-native-utils/logger";
6
4
 
7
- const dimensionRegex = /\b(width|height)=(\d+)\b/gi;
5
+ const imageResizingLogger = coreLogger
6
+ ?.addSubsystem("components")
7
+ ?.addSubsystem("ImageResizing");
8
8
 
9
+ const TEMPLATE_REGEX = /{{(width|height)}}/gi;
10
+ const UNRESOLVED_PLACEHOLDER_REGEX = /{{[^}]*}}?/;
11
+ const INVALID_PLACEHOLDER_EXTRACT_REGEX = /{{([^}]*)}}?/g;
12
+ const ALLOWED_PLACEHOLDERS = ["width", "height"];
13
+
14
+ /**
15
+ * Merges query-param string `paramsString` into `baseUrl`.
16
+ * Params from `paramsString` override any existing params with the same key.
17
+ */
18
+ export const mergeQueryParams = (
19
+ baseUrl: string,
20
+ paramsString: string
21
+ ): string => {
22
+ try {
23
+ const urlObj = new URL(baseUrl);
24
+ const newParams = new URLSearchParams(paramsString);
25
+
26
+ newParams.forEach((value, key) => {
27
+ urlObj.searchParams.set(key, value);
28
+ });
29
+
30
+ return urlObj.toString();
31
+ } catch {
32
+ // If URL parsing fails, return original
33
+ return baseUrl;
34
+ }
35
+ };
36
+
37
+ /**
38
+ * Extracts unresolved placeholder names from a param pair and logs a warning.
39
+ */
40
+ const logInvalidPlaceholders = (pair: string): void => {
41
+ const invalidNames: string[] = [];
42
+ let match: RegExpExecArray | null;
43
+
44
+ // Reset lastIndex before using the global regex
45
+ INVALID_PLACEHOLDER_EXTRACT_REGEX.lastIndex = 0;
46
+
47
+ while ((match = INVALID_PLACEHOLDER_EXTRACT_REGEX.exec(pair)) !== null) {
48
+ const name = match[1];
49
+
50
+ if (!ALLOWED_PLACEHOLDERS.includes(name?.toLowerCase())) {
51
+ invalidNames.push(name);
52
+ }
53
+ }
54
+
55
+ if (invalidNames.length > 0) {
56
+ imageResizingLogger?.warning({
57
+ message: `Image resizing: invalid placeholder(s) found: ${invalidNames.map((n) => `{{${n}}}`).join(", ")}. Allowed placeholders are: {{width}}, {{height}}. The param "${pair}" was ignored.`,
58
+ data: { pair, invalidNames, allowedPlaceholders: ALLOWED_PLACEHOLDERS },
59
+ });
60
+ }
61
+ };
62
+
63
+ /**
64
+ * Parses a dimension mapping template string (query param format) and injects
65
+ * the resolved width/height values into the given URL.
66
+ *
67
+ * The template uses `{{width}}` and `{{height}}` placeholders.
68
+ * It is treated as query-param key=value pairs (e.g. "w={{width}}&h={{height}}"
69
+ * or "size={{width}}x{{height}}").
70
+ *
71
+ * Resolved params are **merged** into the original URL – existing params with
72
+ * the same key are replaced; new ones are appended.
73
+ *
74
+ * Invalid mappings (containing unresolved placeholders like `{{heit}}` or
75
+ * malformed `{{heit}`) are silently dropped and logged as warnings.
76
+ * A leading `?` in the template is stripped.
77
+ */
9
78
  export const withDimensions = (
10
79
  url: Option<string>,
11
80
  width: Option<number | string>,
12
- height: Option<number | string>
81
+ height: Option<number | string>,
82
+ dimensionMappingTemplate?: Option<string>
13
83
  ) => {
14
84
  if (isNil(url)) {
15
85
  return url;
@@ -22,23 +92,48 @@ export const withDimensions = (
22
92
  const newWidth = Math.trunc(toFiniteNumberWithDefault(0, width));
23
93
  const newHeight = Math.trunc(toFiniteNumberWithDefault(0, height));
24
94
 
25
- return url.replace(dimensionRegex, (match, name, originalValue) => {
26
- if (name.toLocaleLowerCase() === "width" && newWidth > 0) {
27
- const originalWidth = toNumber(originalValue);
95
+ if (newWidth <= 0 && newHeight <= 0) {
96
+ return url;
97
+ }
98
+
99
+ // Strip leading `?` and whitespace from the template
100
+ const template = dimensionMappingTemplate?.trim()?.replace(/^\?/, "");
28
101
 
29
- if (originalWidth > 0 && originalWidth > newWidth) {
30
- return `${name}=${newWidth}`;
31
- }
32
- }
102
+ if (!template) {
103
+ return url;
104
+ }
33
105
 
34
- if (name.toLocaleLowerCase() === "height" && newHeight > 0) {
35
- const originalHeight = toNumber(originalValue);
106
+ // Replace {{width}} / {{height}} placeholders in the template
107
+ const resolvedTemplate = template.replace(TEMPLATE_REGEX, (_match, name) => {
108
+ if (name.toLowerCase() === "width") {
109
+ return newWidth > 0 ? String(newWidth) : _match;
110
+ }
36
111
 
37
- if (originalHeight > 0 && originalHeight > newHeight) {
38
- return `${name}=${newHeight}`;
39
- }
112
+ if (name.toLowerCase() === "height") {
113
+ return newHeight > 0 ? String(newHeight) : _match;
40
114
  }
41
115
 
42
- return match;
116
+ return _match;
43
117
  });
118
+
119
+ // Filter out param pairs that still contain unresolved / malformed placeholders
120
+ const cleanedParams = resolvedTemplate
121
+ .split("&")
122
+ .filter((pair) => {
123
+ if (UNRESOLVED_PLACEHOLDER_REGEX.test(pair)) {
124
+ logInvalidPlaceholders(pair);
125
+
126
+ return false;
127
+ }
128
+
129
+ return true;
130
+ })
131
+ .join("&");
132
+
133
+ if (!cleanedParams) {
134
+ return url;
135
+ }
136
+
137
+ // Parse the resolved template as query params and merge into URL
138
+ return mergeQueryParams(url, cleanedParams);
44
139
  };
@@ -14,25 +14,36 @@ type Style = {
14
14
  height: Option<number | string>;
15
15
  };
16
16
 
17
- const withAppliedDimensions = (style: Style) => (source: Source) => ({
18
- ...source,
19
- uri: withDimensions(source?.uri, style?.width, style?.height),
20
- });
17
+ const withAppliedDimensions =
18
+ (style: Style, dimensionMappingTemplate: string) => (source: Source) => ({
19
+ ...source,
20
+ uri: withDimensions(
21
+ source?.uri,
22
+ style?.width,
23
+ style?.height,
24
+ dimensionMappingTemplate
25
+ ),
26
+ });
21
27
 
22
28
  export const withDimensionsHOC = (Component) => {
23
29
  return function WithDimensions(props: Record<string, unknown>) {
24
30
  const theme = useTheme<BaseThemePropertiesMobile>();
25
31
 
26
- const useDownScalingImages = toBooleanWithDefaultFalse(
27
- theme.use_downscaling_images
32
+ const useImageResizing = toBooleanWithDefaultFalse(
33
+ theme.use_image_resizing ?? theme.use_downscaling_images
28
34
  );
29
35
 
36
+ const dimensionMappingTemplate = theme.image_resizing_template;
37
+
30
38
  return (
31
39
  <Component
32
40
  {...props}
33
41
  withDimensions={
34
- useDownScalingImages
35
- ? withAppliedDimensions((props?.style || {}) as Style)
42
+ useImageResizing
43
+ ? withAppliedDimensions(
44
+ (props?.style || {}) as Style,
45
+ dimensionMappingTemplate
46
+ )
36
47
  : identity
37
48
  }
38
49
  />
@@ -25,24 +25,6 @@ const styles = StyleSheet.create({
25
25
  flexDirection: "row",
26
26
  alignItems: "center",
27
27
  },
28
- demoScreen: {
29
- flex: 1,
30
- backgroundColor: "#1C1C1E",
31
- paddingTop: 64,
32
- paddingHorizontal: 16,
33
- gap: 12,
34
- },
35
- demoHeading: {
36
- color: "#FFFFFF",
37
- fontSize: 20,
38
- fontWeight: "600",
39
- marginBottom: 8,
40
- },
41
- demoLabel: {
42
- color: "#999999",
43
- fontSize: 13,
44
- marginTop: 8,
45
- },
46
28
  });
47
29
 
48
30
  // ---------------------------------------------------------------------------
@@ -32,24 +32,6 @@ const styles = StyleSheet.create({
32
32
  title: {
33
33
  textAlign: "center",
34
34
  },
35
- demoScreen: {
36
- flex: 1,
37
- backgroundColor: "#1C1C1E",
38
- paddingTop: 64,
39
- paddingHorizontal: 24,
40
- gap: 12,
41
- },
42
- demoHeading: {
43
- color: "#FFFFFF",
44
- fontSize: 20,
45
- fontWeight: "600",
46
- marginBottom: 8,
47
- },
48
- demoLabel: {
49
- color: "#999999",
50
- fontSize: 13,
51
- marginTop: 8,
52
- },
53
35
  });
54
36
 
55
37
  // ---------------------------------------------------------------------------
@@ -310,6 +292,9 @@ function ButtonLeadingIcon({
310
292
  // AudioPlayerButton — single building block driven by configuration + data
311
293
  // ---------------------------------------------------------------------------
312
294
 
295
+ import { useModalBlocksStyle } from "../../ModalBlocksStyleContext";
296
+ import { mergeButtonConfiguration } from "../utils/mergeStyles";
297
+
313
298
  export function AudioPlayerButton({
314
299
  configuration,
315
300
  data,
@@ -317,11 +302,19 @@ export function AudioPlayerButton({
317
302
  focused = false,
318
303
  onPress,
319
304
  }: AudioPlayerButtonProps) {
305
+ const modalBlocksStyle = useModalBlocksStyle();
306
+ const buttonBlock = modalBlocksStyle?.button;
307
+ const actionBlock = modalBlocksStyle?.action;
308
+
309
+ const effectiveConfig = React.useMemo(() => {
310
+ return mergeButtonConfiguration(configuration, buttonBlock, actionBlock);
311
+ }, [configuration, buttonBlock, actionBlock]);
312
+
320
313
  const {
321
- background: backgroundConfiguration,
322
- title: titleConfiguration,
314
+ background: effectiveBgConfig,
315
+ title: effectiveTitleConfig,
323
316
  icons: iconsConfiguration,
324
- } = configuration;
317
+ } = effectiveConfig;
325
318
 
326
319
  const { title } = data;
327
320
  const isDisabled = disabled ?? data.disabled ?? false;
@@ -333,7 +326,7 @@ export function AudioPlayerButton({
333
326
  <ButtonContainer
334
327
  focused={focused}
335
328
  disabled={isDisabled}
336
- configuration={backgroundConfiguration}
329
+ configuration={effectiveBgConfig}
337
330
  onPress={onPress}
338
331
  >
339
332
  <View style={styles.contentRow}>
@@ -343,14 +336,14 @@ export function AudioPlayerButton({
343
336
  actionType={leadingIcon.actionType}
344
337
  configuration={iconsConfiguration}
345
338
  />
346
- <View style={{ width: backgroundConfiguration.horizontalGutter }} />
339
+ <View style={{ width: effectiveBgConfig.horizontalGutter }} />
347
340
  </>
348
341
  ) : null}
349
342
  <ButtonTitle
350
343
  title={title}
351
344
  focused={focused}
352
345
  disabled={isDisabled}
353
- configuration={titleConfiguration}
346
+ configuration={effectiveTitleConfig}
354
347
  />
355
348
  </View>
356
349
  </ButtonContainer>
@@ -10,6 +10,7 @@
10
10
 
11
11
  import React, { useCallback, useState } from "react";
12
12
  import { Platform, Pressable, StyleSheet, Text, View } from "react-native";
13
+ import { isApplePlatform } from "@applicaster/zapp-react-native-utils/reactUtils";
13
14
 
14
15
  const styles = StyleSheet.create({
15
16
  row: {
@@ -45,29 +46,6 @@ const styles = StyleSheet.create({
45
46
  alignItems: "center",
46
47
  justifyContent: "center",
47
48
  },
48
- demoScreen: {
49
- flex: 1,
50
- backgroundColor: "#1C1C1E",
51
- paddingTop: 64,
52
- paddingHorizontal: 0,
53
- gap: 24,
54
- },
55
- demoBlock: {
56
- gap: 8,
57
- paddingHorizontal: 0,
58
- },
59
- demoHeading: {
60
- color: "#FFFFFF",
61
- fontSize: 20,
62
- fontWeight: "600",
63
- marginBottom: 8,
64
- paddingHorizontal: 24,
65
- },
66
- demoLabel: {
67
- color: "#999999",
68
- fontSize: 13,
69
- paddingHorizontal: 24,
70
- },
71
49
  });
72
50
 
73
51
  // ---------------------------------------------------------------------------
@@ -123,7 +101,7 @@ export type ActionButtonsStyleConfiguration = {
123
101
  };
124
102
 
125
103
  export type HeaderConfiguration = {
126
- paddingTop: undefined;
104
+ paddingTop?: number;
127
105
  variant: HeaderVariant;
128
106
  title: TitleConfiguration;
129
107
  closeButton: CloseButtonConfiguration;
@@ -305,6 +283,8 @@ type HeaderContainerProps = {
305
283
  children: React.ReactNode;
306
284
  };
307
285
 
286
+ const TRANSPARENT = "transparent";
287
+
308
288
  function HeaderContainer({
309
289
  variant,
310
290
  configuration,
@@ -317,6 +297,8 @@ function HeaderContainer({
317
297
  ? configuration.paddingTop
318
298
  : container.paddingTop;
319
299
 
300
+ const backgroundColor = (configuration as any).backgroundColor || TRANSPARENT;
301
+
320
302
  return (
321
303
  <View
322
304
  style={{
@@ -324,6 +306,7 @@ function HeaderContainer({
324
306
  paddingRight: container.paddingRight,
325
307
  paddingBottom: container.paddingBottom,
326
308
  paddingLeft: container.paddingLeft,
309
+ backgroundColor: backgroundColor,
327
310
  }}
328
311
  >
329
312
  {children}
@@ -338,17 +321,18 @@ type HeaderTitleProps = {
338
321
  };
339
322
 
340
323
  function HeaderTitle({ title, configuration, centered }: HeaderTitleProps) {
341
- const letterSpacing =
342
- Platform.OS === "ios"
343
- ? configuration.iosLetterSpacing
344
- : configuration.androidLetterSpacing;
324
+ const { fontColor, fontSize } = configuration;
325
+
326
+ const letterSpacing = isApplePlatform()
327
+ ? configuration.iosLetterSpacing
328
+ : configuration.androidLetterSpacing;
345
329
 
346
330
  return (
347
331
  <Text
348
332
  style={{
349
333
  flex: centered ? undefined : 1,
350
- color: configuration.fontColor,
351
- fontSize: configuration.fontSize,
334
+ color: fontColor,
335
+ fontSize: fontSize,
352
336
  lineHeight: configuration.lineHeight,
353
337
  letterSpacing,
354
338
  fontFamily: Platform.select({
@@ -610,6 +594,9 @@ function CenterAlignedHeaderLayout({
610
594
  // AudioPlayerHeader
611
595
  // ---------------------------------------------------------------------------
612
596
 
597
+ import { useModalBlocksStyle } from "../../ModalBlocksStyleContext";
598
+ import { mergeHeaderConfiguration } from "../utils/mergeStyles";
599
+
613
600
  export function AudioPlayerHeader({
614
601
  configuration,
615
602
  data,
@@ -622,7 +609,14 @@ export function AudioPlayerHeader({
622
609
  onDone,
623
610
  onClear,
624
611
  }: AudioPlayerHeaderProps) {
625
- const { variant, title, closeButton, actionButtons } = configuration;
612
+ const modalBlocksStyle = useModalBlocksStyle();
613
+ const headerBlock = modalBlocksStyle?.header;
614
+
615
+ const effectiveConfig = React.useMemo(() => {
616
+ return mergeHeaderConfiguration(configuration, headerBlock);
617
+ }, [configuration, headerBlock]);
618
+
619
+ const { variant, title, closeButton, actionButtons } = effectiveConfig;
626
620
  const { title: titleText } = data;
627
621
 
628
622
  const container = CONTAINER_SPEC[variant];
@@ -767,45 +761,3 @@ export const VARIANT_SECTION_HEADER: HeaderConfiguration = {
767
761
  closeButton: { enabled: false },
768
762
  actionButtons: DEFAULT_ACTION_BUTTONS_STYLE_CONFIGURATION,
769
763
  };
770
-
771
- export default function App() {
772
- return (
773
- <View style={styles.demoScreen}>
774
- <Text style={styles.demoHeading}>Modal Header Building Block</Text>
775
-
776
- <View style={styles.demoBlock}>
777
- <Text style={styles.demoLabel}>Single Line Header</Text>
778
- <AudioPlayerHeader
779
- configuration={VARIANT_SINGLE_LINE_HEADER}
780
- data={{ title: "Playback Speed" }}
781
- />
782
- </View>
783
-
784
- <View style={styles.demoBlock}>
785
- <Text style={styles.demoLabel}>Header With Actions</Text>
786
- <AudioPlayerHeader
787
- configuration={VARIANT_HEADER_WITH_ACTIONS}
788
- data={{ title: "Playback Speed" }}
789
- />
790
- </View>
791
-
792
- <View style={styles.demoBlock}>
793
- <Text style={styles.demoLabel}>
794
- Header With Actions — single side action
795
- </Text>
796
- <AudioPlayerHeader
797
- configuration={VARIANT_HEADER_WITH_CANCEL_ONLY}
798
- data={{ title: "Playback Speed" }}
799
- />
800
- </View>
801
-
802
- <View style={styles.demoBlock}>
803
- <Text style={styles.demoLabel}>Section Header</Text>
804
- <AudioPlayerHeader
805
- configuration={VARIANT_SECTION_HEADER}
806
- data={{ title: "Next in Queue" }}
807
- />
808
- </View>
809
- </View>
810
- );
811
- }
@@ -37,24 +37,6 @@ const styles = StyleSheet.create({
37
37
  underline: {
38
38
  alignSelf: "stretch",
39
39
  },
40
- demoScreen: {
41
- flex: 1,
42
- backgroundColor: "#1C1C1E",
43
- paddingTop: 64,
44
- paddingHorizontal: 16,
45
- gap: 24,
46
- },
47
- demoHeading: {
48
- color: "#FFFFFF",
49
- fontSize: 20,
50
- fontWeight: "600",
51
- marginBottom: 8,
52
- },
53
- demoLabel: {
54
- color: "#999999",
55
- fontSize: 13,
56
- marginTop: 8,
57
- },
58
40
  });
59
41
 
60
42
  // ---------------------------------------------------------------------------