@applicaster/zapp-react-native-utils 16.0.0-alpha.9183418480 → 16.0.0-alpha.9506982263

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 (126) hide show
  1. package/actionsExecutor/ActionExecutor.ts +8 -10
  2. package/actionsExecutor/ActionExecutorContext.tsx +43 -326
  3. package/actionsExecutor/actions/__tests__/showToast.test.ts +88 -0
  4. package/actionsExecutor/actions/appRestart.ts +24 -0
  5. package/actionsExecutor/actions/confirmDialog.ts +53 -0
  6. package/actionsExecutor/actions/index.ts +30 -0
  7. package/actionsExecutor/actions/localStorageRemove.ts +27 -0
  8. package/actionsExecutor/actions/localStorageSet.ts +28 -0
  9. package/actionsExecutor/actions/localStorageToggleFlag.ts +28 -0
  10. package/actionsExecutor/actions/navigateToScreen.ts +123 -0
  11. package/actionsExecutor/actions/refreshComponent.ts +79 -0
  12. package/actionsExecutor/actions/screenSetVariable.ts +35 -0
  13. package/actionsExecutor/actions/screenToggleFlag.ts +38 -0
  14. package/actionsExecutor/actions/sendCloudEvent.ts +88 -0
  15. package/actionsExecutor/actions/sessionStorageRemove.ts +19 -0
  16. package/actionsExecutor/actions/sessionStorageSet.ts +20 -0
  17. package/actionsExecutor/actions/sessionStorageToggleFlag.ts +15 -0
  18. package/actionsExecutor/actions/showToast.ts +87 -0
  19. package/actionsExecutor/actions/switchLayout.ts +40 -0
  20. package/actionsExecutor/types.ts +59 -0
  21. package/analyticsUtils/AnalyticsEvents/sendMenuClickEvent.ts +2 -2
  22. package/analyticsUtils/PlayerAnalyticsManager.ts +12 -2
  23. package/analyticsUtils/__tests__/analyticsMapper.test.ts +35 -0
  24. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testACP_events.json +1 -1
  25. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testBlockUnlistedParams_rules.json +1 -1
  26. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEmptyRenameKeepsName_events.json +4 -0
  27. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEmptyRenameKeepsName_rules.json +6 -0
  28. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEventRegex_events.json +3 -9
  29. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testIgnoreOrdering_events.json +18 -0
  30. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testIgnoreOrdering_rules.json +16 -0
  31. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexEventNoFallback_events.json +4 -0
  32. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexEventNoFallback_rules.json +6 -0
  33. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexMultiGroupRename_events.json +4 -0
  34. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexMultiGroupRename_rules.json +6 -0
  35. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexNonNamedFullMatch_events.json +4 -0
  36. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexNonNamedFullMatch_rules.json +6 -0
  37. package/analyticsUtils/__tests__/fixtures/index.js +20 -0
  38. package/analyticsUtils/analyticsMapper.ts +4 -1
  39. package/analyticsUtils/playerAnalyticsTracker.ts +26 -3
  40. package/appUtils/HooksManager/index.ts +3 -8
  41. package/appUtils/contextKeysManager/__tests__/getKey/failure.test.ts +1 -1
  42. package/appUtils/contextKeysManager/__tests__/removeKey/failure.test.ts +1 -1
  43. package/appUtils/contextKeysManager/__tests__/setKey/failure/invalidKey.test.ts +4 -4
  44. package/appUtils/contextKeysManager/index.ts +1 -1
  45. package/appUtils/contextKeysManager/utils/index.ts +38 -25
  46. package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +138 -51
  47. package/appUtils/playerManager/OverlayObserver/__tests__/liveContent.test.ts +205 -0
  48. package/appUtils/playerManager/__tests__/playerContent.test.ts +403 -0
  49. package/appUtils/playerManager/__tests__/playerFactory.test.ts +1 -1
  50. package/appUtils/playerManager/__tests__/usePlayerContent.test.tsx +64 -0
  51. package/appUtils/playerManager/{conts.ts → const.ts} +20 -0
  52. package/appUtils/playerManager/index.ts +1 -1
  53. package/appUtils/playerManager/player.ts +115 -3
  54. package/appUtils/playerManager/playerFactory.ts +1 -1
  55. package/appUtils/playerManager/playerNative.ts +1 -1
  56. package/appUtils/playerManager/usePlayerContent.tsx +43 -0
  57. package/appUtils/playerManager/usePlayerControllerSetup.tsx +1 -1
  58. package/appUtils/playerManager/userLanguagePreference.ts +1 -1
  59. package/arrayUtils/__tests__/{anyThruthy.test.ts → anyTruthy.test.ts} +8 -0
  60. package/arrayUtils/__tests__/arrayUtils.test.ts +165 -108
  61. package/arrayUtils/__tests__/isEmptyArray.test.ts +11 -0
  62. package/arrayUtils/__tests__/isFilledArray.test.ts +12 -0
  63. package/arrayUtils/__tests__/isFirst.test.ts +17 -0
  64. package/arrayUtils/__tests__/isIndexInRange.test.ts +14 -0
  65. package/arrayUtils/__tests__/isLast.test.ts +31 -0
  66. package/arrayUtils/__tests__/makeListOf.test.ts +31 -0
  67. package/arrayUtils/__tests__/makeListOfIndexes.test.ts +20 -0
  68. package/arrayUtils/__tests__/sample.test.ts +61 -0
  69. package/arrayUtils/index.ts +57 -20
  70. package/bottomSheet/__tests__/scrollContext.test.tsx +35 -0
  71. package/bottomSheet/index.ts +17 -0
  72. package/cellUtils/index.ts +3 -5
  73. package/colorUtils/__tests__/isTransparentColor.test.ts +76 -0
  74. package/colorUtils/__tests__/isValidColor.test.ts +70 -0
  75. package/colorUtils/index.ts +50 -0
  76. package/manifestUtils/_internals/index.js +6 -0
  77. package/manifestUtils/defaultManifestConfigurations/generalContent.js +35 -0
  78. package/manifestUtils/defaultManifestConfigurations/player.js +185 -19
  79. package/manifestUtils/fieldUtils/__tests__/fieldUtils.test.js +84 -0
  80. package/manifestUtils/fieldUtils/index.js +125 -0
  81. package/manifestUtils/keys.js +9 -0
  82. package/manifestUtils/mobileAction/button/index.js +16 -0
  83. package/manifestUtils/mobileAction/container/index.js +3 -1
  84. package/manifestUtils/mobileAction/groups/defaults.js +3 -1
  85. package/manifestUtils/platformIsTV.js +1 -0
  86. package/numberUtils/__tests__/isMinusZero.test.ts +20 -0
  87. package/numberUtils/__tests__/isZero.test.ts +27 -0
  88. package/numberUtils/__tests__/requireNumber.test.ts +50 -0
  89. package/numberUtils/__tests__/toNumber.test.ts +27 -0
  90. package/numberUtils/__tests__/toNumberWithDefault.test.ts +64 -0
  91. package/numberUtils/__tests__/toNumberWithDefaultZero.test.ts +48 -0
  92. package/numberUtils/index.ts +27 -8
  93. package/package.json +2 -2
  94. package/pipesUtils/__tests__/buildUrlWithQuery.test.ts +33 -0
  95. package/pipesUtils/__tests__/withPipesEndpoint.test.tsx +56 -0
  96. package/pipesUtils/index.ts +1 -0
  97. package/pipesUtils/withPipesEndpoint.tsx +54 -0
  98. package/playerUtils/__tests__/contentDataKeys.test.ts +297 -0
  99. package/playerUtils/__tests__/resolvePlayerTitleSubtitle.test.ts +180 -0
  100. package/playerUtils/contentDataKeys.ts +134 -0
  101. package/playerUtils/index.ts +24 -17
  102. package/playerUtils/isAudioItem.ts +26 -0
  103. package/playerUtils/resolvePlayerTitleSubtitle.ts +76 -0
  104. package/reactHooks/actions/index.ts +51 -1
  105. package/reactHooks/cell-click/index.ts +18 -10
  106. package/reactHooks/feed/__mocks__/useMarkPipesDataStale.ts +4 -0
  107. package/reactHooks/feed/__tests__/useInflatedUrl.test.tsx +25 -0
  108. package/reactHooks/feed/index.ts +5 -1
  109. package/reactHooks/feed/useBatchLoading.ts +9 -1
  110. package/reactHooks/feed/{usePipesCacheReset.ts → useMarkPipesDataStale.ts} +12 -4
  111. package/reactHooks/layout/index.ts +1 -1
  112. package/reactHooks/navigation/__mocks__/index.ts +4 -0
  113. package/reactHooks/navigation/__tests__/useIsScreenActive.test.ts +42 -0
  114. package/reactHooks/navigation/index.ts +1 -1
  115. package/reactHooks/navigation/useIsScreenActive.ts +29 -8
  116. package/reactHooks/state/useComponentScreenState.ts +1 -1
  117. package/reactHooks/utils/index.ts +3 -2
  118. package/reactHooks/videoModal/hooks/useVideoModalScreenData.tsx +22 -4
  119. package/riverComponetsMeasurementProvider/index.tsx +12 -8
  120. package/stringUtils/__tests__/stringUtils.test.js +42 -0
  121. package/stringUtils/index.ts +2 -2
  122. package/uiActionsRegistrator/index.ts +203 -0
  123. package/zappFrameworkUtils/__tests__/localStorageHelper.test.ts +146 -0
  124. package/zappFrameworkUtils/localStorageHelper.ts +19 -15
  125. package/appUtils/playerManager/usePlayerTitleSummaryOverlay.tsx +0 -56
  126. package/reactHooks/feed/__mocks__/usePipesCacheReset.ts +0 -1
@@ -781,21 +781,6 @@ function getPlayerConfiguration({ platform, version }) {
781
781
  }
782
782
  );
783
783
 
784
- if (
785
- platform.includes("android") ||
786
- platform.includes("amazon") ||
787
- platform.includes("ios") ||
788
- platform.includes("tvos")
789
- ) {
790
- general.fields.push({
791
- key: "user_agent",
792
- label: "User agent",
793
- initial_value: "",
794
- type: "text_input",
795
- label_tooltip: "Override default user agent string",
796
- });
797
- }
798
-
799
784
  if (platform === "android_for_quickbrick") {
800
785
  general.fields.push({
801
786
  key: "stop_playback_on_task_removal",
@@ -1294,6 +1279,35 @@ function getPlayerConfiguration({ platform, version }) {
1294
1279
  type: "switch",
1295
1280
  label_tooltip: "Select whether you want to display title in player",
1296
1281
  },
1282
+ {
1283
+ section: "title",
1284
+ key: "title_data_key_source",
1285
+ label: "Title data key",
1286
+ initial_value: "title",
1287
+ type: "select",
1288
+ options: [
1289
+ { text: "Title", value: "title" },
1290
+ { text: "Other", value: "other" },
1291
+ ],
1292
+ label_tooltip:
1293
+ "Select which feed data key provides the player content title. Keep 'Title' to use the entry title",
1294
+ },
1295
+ {
1296
+ section: "title",
1297
+ key: "title_custom_data_key",
1298
+ label: "Custom title data key",
1299
+ initial_value: "",
1300
+ type: "text_input",
1301
+ rules: "conditional",
1302
+ conditional_fields: [
1303
+ {
1304
+ key: "styles/title_data_key_source",
1305
+ condition_value: "other",
1306
+ },
1307
+ ],
1308
+ label_tooltip:
1309
+ "Data key on the feed entry, dot notation supported, for example extensions.episode_name. Falls back to the entry title if the key is empty or missing",
1310
+ },
1297
1311
  {
1298
1312
  section: "title",
1299
1313
  key: "title_font_android",
@@ -1701,6 +1715,35 @@ function getPlayerConfiguration({ platform, version }) {
1701
1715
  label_tooltip:
1702
1716
  "Select whether you want to display subtitle in player",
1703
1717
  },
1718
+ {
1719
+ section: "subtitle",
1720
+ key: "subtitle_data_key_source",
1721
+ label: "Subtitle data key",
1722
+ initial_value: "summary",
1723
+ type: "select",
1724
+ options: [
1725
+ { text: "Subtitle", value: "summary" },
1726
+ { text: "Other", value: "other" },
1727
+ ],
1728
+ label_tooltip:
1729
+ "Select which feed data key provides the player content subtitle. Keep 'Subtitle' to use the entry summary",
1730
+ },
1731
+ {
1732
+ section: "subtitle",
1733
+ key: "subtitle_custom_data_key",
1734
+ label: "Custom subtitle data key",
1735
+ initial_value: "",
1736
+ type: "text_input",
1737
+ rules: "conditional",
1738
+ conditional_fields: [
1739
+ {
1740
+ key: "styles/subtitle_data_key_source",
1741
+ condition_value: "other",
1742
+ },
1743
+ ],
1744
+ label_tooltip:
1745
+ "Data key on the feed entry, dot notation supported, for example extensions.episode_subtitle. Falls back to the entry summary if the key is empty or missing",
1746
+ },
1704
1747
  {
1705
1748
  section: "subtitle",
1706
1749
  key: "subtitle_font_android",
@@ -2415,6 +2458,21 @@ function getPlayerConfiguration({ platform, version }) {
2415
2458
  );
2416
2459
  }
2417
2460
 
2461
+ if (
2462
+ platform.includes("android") ||
2463
+ platform.includes("amazon") ||
2464
+ platform.includes("ios") ||
2465
+ platform.includes("tvos")
2466
+ ) {
2467
+ general.fields.push({
2468
+ key: "user_agent",
2469
+ label: "User agent",
2470
+ initial_value: "",
2471
+ type: "text_input",
2472
+ label_tooltip: "Override default user agent string",
2473
+ });
2474
+ }
2475
+
2418
2476
  compact([
2419
2477
  {
2420
2478
  key: "liveCatchUpEnabled",
@@ -2850,6 +2908,33 @@ function getPlayerConfiguration({ platform, version }) {
2850
2908
  "Video Player - Title Text",
2851
2909
  "Video Player Title Text Label Styles",
2852
2910
  [
2911
+ {
2912
+ key: "video_player_title_data_key_source",
2913
+ label: "Video title data key",
2914
+ initial_value: "title",
2915
+ type: "select",
2916
+ options: [
2917
+ { text: "Title", value: "title" },
2918
+ { text: "Other", value: "other" },
2919
+ ],
2920
+ label_tooltip:
2921
+ "Select which feed data key provides the video player title. Keep 'Title' to use the entry title",
2922
+ },
2923
+ {
2924
+ key: "video_player_title_custom_data_key",
2925
+ label: "Custom video title data key",
2926
+ initial_value: "",
2927
+ type: "text_input",
2928
+ rules: "conditional",
2929
+ conditional_fields: [
2930
+ {
2931
+ key: "styles/video_player_title_data_key_source",
2932
+ condition_value: "other",
2933
+ },
2934
+ ],
2935
+ label_tooltip:
2936
+ "Data key on the feed entry, dot notation supported, for example extensions.episode_name. Falls back to the entry title if the key is empty or missing",
2937
+ },
2853
2938
  {
2854
2939
  key: "player_title_color",
2855
2940
  label: "Font color",
@@ -2935,6 +3020,33 @@ function getPlayerConfiguration({ platform, version }) {
2935
3020
  "Video Player - Summary Text",
2936
3021
  "Video Player Summary Text Label Styles",
2937
3022
  [
3023
+ {
3024
+ key: "video_player_summary_data_key_source",
3025
+ label: "Video summary data key",
3026
+ initial_value: "summary",
3027
+ type: "select",
3028
+ options: [
3029
+ { text: "Summary", value: "summary" },
3030
+ { text: "Other", value: "other" },
3031
+ ],
3032
+ label_tooltip:
3033
+ "Select which feed data key provides the video player summary. Keep 'Summary' to use the entry summary",
3034
+ },
3035
+ {
3036
+ key: "video_player_summary_custom_data_key",
3037
+ label: "Custom video summary data key",
3038
+ initial_value: "",
3039
+ type: "text_input",
3040
+ rules: "conditional",
3041
+ conditional_fields: [
3042
+ {
3043
+ key: "styles/video_player_summary_data_key_source",
3044
+ condition_value: "other",
3045
+ },
3046
+ ],
3047
+ label_tooltip:
3048
+ "Data key on the feed entry, dot notation supported, for example extensions.episode_subtitle. Falls back to the entry summary if the key is empty or missing",
3049
+ },
2938
3050
  {
2939
3051
  key: "player_summary_color",
2940
3052
  label: "Font color",
@@ -3020,6 +3132,33 @@ function getPlayerConfiguration({ platform, version }) {
3020
3132
  "Audio Player - Title Text",
3021
3133
  "Audio Player Title Text Label Styles",
3022
3134
  [
3135
+ {
3136
+ key: "audio_player_title_data_key_source",
3137
+ label: "Audio title data key",
3138
+ initial_value: "title",
3139
+ type: "select",
3140
+ options: [
3141
+ { text: "Title", value: "title" },
3142
+ { text: "Other", value: "other" },
3143
+ ],
3144
+ label_tooltip:
3145
+ "Select which feed data key provides the audio player title. Keep 'Title' to use the entry title",
3146
+ },
3147
+ {
3148
+ key: "audio_player_title_custom_data_key",
3149
+ label: "Custom audio title data key",
3150
+ initial_value: "",
3151
+ type: "text_input",
3152
+ rules: "conditional",
3153
+ conditional_fields: [
3154
+ {
3155
+ key: "styles/audio_player_title_data_key_source",
3156
+ condition_value: "other",
3157
+ },
3158
+ ],
3159
+ label_tooltip:
3160
+ "Data key on the feed entry, dot notation supported, for example extensions.episode_name. Falls back to the entry title if the key is empty or missing",
3161
+ },
3023
3162
  {
3024
3163
  key: "audio_player_title_color",
3025
3164
  label: "Audio player title color",
@@ -3105,6 +3244,33 @@ function getPlayerConfiguration({ platform, version }) {
3105
3244
  "Audio Player - Summary Text",
3106
3245
  "Audio Player Summary Text Label Styles",
3107
3246
  [
3247
+ {
3248
+ key: "audio_player_summary_data_key_source",
3249
+ label: "Audio summary data key",
3250
+ initial_value: "summary",
3251
+ type: "select",
3252
+ options: [
3253
+ { text: "Summary", value: "summary" },
3254
+ { text: "Other", value: "other" },
3255
+ ],
3256
+ label_tooltip:
3257
+ "Select which feed data key provides the audio player summary. Keep 'Summary' to use the entry summary",
3258
+ },
3259
+ {
3260
+ key: "audio_player_summary_custom_data_key",
3261
+ label: "Custom audio summary data key",
3262
+ initial_value: "",
3263
+ type: "text_input",
3264
+ rules: "conditional",
3265
+ conditional_fields: [
3266
+ {
3267
+ key: "styles/audio_player_summary_data_key_source",
3268
+ condition_value: "other",
3269
+ },
3270
+ ],
3271
+ label_tooltip:
3272
+ "Data key on the feed entry, dot notation supported, for example extensions.episode_subtitle. Falls back to the entry summary if the key is empty or missing",
3273
+ },
3108
3274
  {
3109
3275
  key: "audio_player_summary_color",
3110
3276
  label: "Audio player summary color",
@@ -3522,13 +3688,13 @@ function getPlayerConfiguration({ platform, version }) {
3522
3688
  label: "Advanced Player Styles",
3523
3689
  type: "text_input",
3524
3690
  multiline: true,
3525
- initial_value: `.vjs-text-track-cue {
3526
- inset: unset !important;
3527
- top: 94% !important;
3691
+ initial_value: `.vjs-text-track-cue {
3692
+ top: auto !important;
3528
3693
  bottom: 0 !important;
3529
3694
  left: 0 !important;
3530
- right: 0 !important;
3695
+ right: auto !important;
3531
3696
  width: 100% !important;
3697
+ height: auto !important;
3532
3698
  }`,
3533
3699
  label_tooltip:
3534
3700
  "This field allows you to inject custom CSS styles to override default player styles. The styles will be injected into the document head and can be used to customize the appearance of the player. Leave empty to use only default styles.",
@@ -1,9 +1,93 @@
1
1
  const {
2
+ parseShorthand,
3
+ margin,
4
+ padding,
2
5
  withConditional,
3
6
  getConditionalKey,
4
7
  createConditionalField,
5
8
  } = require("..");
6
9
 
10
+ describe("parseShorthand", () => {
11
+ it("1 value → all four sides equal", () => {
12
+ expect(parseShorthand("8")).toEqual([8, 8, 8, 8]);
13
+ });
14
+
15
+ it("2 values → top/bottom, right/left", () => {
16
+ expect(parseShorthand("0 20")).toEqual([0, 20, 0, 20]);
17
+ });
18
+
19
+ it("3 values → top, right/left, bottom", () => {
20
+ expect(parseShorthand("0 0 20")).toEqual([0, 0, 20, 0]);
21
+ });
22
+
23
+ it("4 values → top right bottom left", () => {
24
+ expect(parseShorthand("1 2 3 4")).toEqual([1, 2, 3, 4]);
25
+ });
26
+
27
+ it("throws on invalid count", () => {
28
+ expect(() => parseShorthand("1 2 3 4 5")).toThrow();
29
+ });
30
+ });
31
+
32
+ describe("margin", () => {
33
+ it("returns 4 field descriptors with correct suffixes", () => {
34
+ const fields = margin("margin", { mobile: "0" });
35
+
36
+ expect(fields.map((f) => f.suffix)).toEqual([
37
+ "margin top",
38
+ "margin right",
39
+ "margin bottom",
40
+ "margin left",
41
+ ]);
42
+ });
43
+
44
+ it("sets type to number_input", () => {
45
+ margin("margin", { mobile: "0" }).forEach((f) => {
46
+ expect(f.type).toBe("number_input");
47
+ });
48
+ });
49
+
50
+ it("expands shorthand per platform", () => {
51
+ const fields = margin("margin", { mobile: "0 0 20", tv: "0 0 90" });
52
+ const byKey = Object.fromEntries(fields.map((f) => [f.suffix, f]));
53
+ expect(byKey["margin top"].initialValue).toEqual({ mobile: 0, tv: 0 });
54
+ expect(byKey["margin bottom"].initialValue).toEqual({ mobile: 20, tv: 90 });
55
+ expect(byKey["margin left"].initialValue).toEqual({ mobile: 0, tv: 0 });
56
+ });
57
+
58
+ it("supports a custom suffix prefix", () => {
59
+ const fields = margin("input margin", { mobile: "4" });
60
+ expect(fields[0].suffix).toBe("input margin top");
61
+ });
62
+ });
63
+
64
+ describe("padding", () => {
65
+ it("returns 4 field descriptors with correct suffixes", () => {
66
+ const fields = padding("padding", { mobile: "0" });
67
+
68
+ expect(fields.map((f) => f.suffix)).toEqual([
69
+ "padding top",
70
+ "padding right",
71
+ "padding bottom",
72
+ "padding left",
73
+ ]);
74
+ });
75
+
76
+ it("expands 2-value shorthand symmetrically", () => {
77
+ const fields = padding("padding", { mobile: "0 20", tv: "0 124" });
78
+ const byKey = Object.fromEntries(fields.map((f) => [f.suffix, f]));
79
+
80
+ expect(byKey["padding right"].initialValue).toEqual({
81
+ mobile: 20,
82
+ tv: 124,
83
+ });
84
+
85
+ expect(byKey["padding left"].initialValue).toEqual({ mobile: 20, tv: 124 });
86
+ expect(byKey["padding top"].initialValue).toEqual({ mobile: 0, tv: 0 });
87
+ expect(byKey["padding bottom"].initialValue).toEqual({ mobile: 0, tv: 0 });
88
+ });
89
+ });
90
+
7
91
  describe("manifestUtils/fieldUtils", () => {
8
92
  it("appends conditions and adds all_conditions when there is more than one condition", () => {
9
93
  const config = {
@@ -1,3 +1,121 @@
1
+ const { ZAPPIFEST_FIELDS } = require("../keys");
2
+ const { toCamelCase } = require("../_internals");
3
+
4
+ const SIDES = ["top", "right", "bottom", "left"];
5
+
6
+ /**
7
+ * Parses a CSS margin/padding shorthand string into [top, right, bottom, left].
8
+ *
9
+ * 1 value → all four sides
10
+ * 2 values → top/bottom, right/left
11
+ * 3 values → top, right/left, bottom
12
+ * 4 values → top, right, bottom, left
13
+ *
14
+ * @param {string} str
15
+ * @returns {[number, number, number, number]}
16
+ */
17
+ function parseShorthand(str) {
18
+ const parts = String(str).trim().split(/\s+/).map(Number);
19
+
20
+ switch (parts.length) {
21
+ case 1:
22
+ return [parts[0], parts[0], parts[0], parts[0]];
23
+ case 2:
24
+ return [parts[0], parts[1], parts[0], parts[1]];
25
+ case 3:
26
+ return [parts[0], parts[1], parts[2], parts[1]];
27
+ case 4:
28
+ return parts;
29
+ default:
30
+ throw new Error(
31
+ `Shorthand expects 1–4 space-separated values, got ${parts.length}: "${str}"`
32
+ );
33
+ }
34
+ }
35
+
36
+ function spacingFields(prefix, initialValues) {
37
+ const platforms = Object.keys(initialValues);
38
+
39
+ const parsed = Object.fromEntries(
40
+ platforms.map((p) => [p, parseShorthand(initialValues[p])])
41
+ );
42
+
43
+ return SIDES.map((side, i) => ({
44
+ suffix: `${prefix} ${side}`,
45
+ type: ZAPPIFEST_FIELDS.number_input,
46
+ initialValue: Object.fromEntries(platforms.map((p) => [p, parsed[p][i]])),
47
+ }));
48
+ }
49
+
50
+ /**
51
+ * Generates four margin field descriptors (top/right/bottom/left) from a
52
+ * per-platform CSS shorthand string.
53
+ *
54
+ * @param {string} suffix - prefix word for generated field names, e.g. "margin"
55
+ * @param {Record<string, string>} initialValues - e.g. { mobile: "0 0 20", tv: "0 0 90" }
56
+ * @returns {object[]}
57
+ */
58
+ function margin(suffix, initialValues) {
59
+ return spacingFields(suffix, initialValues);
60
+ }
61
+
62
+ /**
63
+ * Generates four padding field descriptors (top/right/bottom/left) from a
64
+ * per-platform CSS shorthand string.
65
+ *
66
+ * @param {string} suffix - prefix word for generated field names, e.g. "padding"
67
+ * @param {Record<string, string>} initialValues - e.g. { mobile: "0 20", tv: "0 124" }
68
+ * @returns {object[]}
69
+ */
70
+ function padding(suffix, initialValues) {
71
+ return spacingFields(suffix, initialValues);
72
+ }
73
+
74
+ /**
75
+ * Returns a flat camelCase defaults object for a spacing shorthand.
76
+ * e.g. marginDefaults("margin", "0 0 20") → { marginTop: 0, marginRight: 0, marginBottom: 20, marginLeft: 0 }
77
+ *
78
+ * @param {string} suffix - e.g. "margin" or "padding"
79
+ * @param {string} shorthand - CSS-like shorthand string
80
+ * @returns {Record<string, number>}
81
+ */
82
+ function spacingDefaults(suffix, shorthand) {
83
+ const values = parseShorthand(shorthand);
84
+
85
+ return Object.fromEntries(
86
+ SIDES.map((side, i) => [toCamelCase(`${suffix} ${side}`), values[i]])
87
+ );
88
+ }
89
+
90
+ function marginDefaults(suffix, shorthand) {
91
+ return spacingDefaults(suffix, shorthand);
92
+ }
93
+
94
+ function paddingDefaults(suffix, shorthand) {
95
+ return spacingDefaults(suffix, shorthand);
96
+ }
97
+
98
+ /**
99
+ * Returns four field descriptors (top/right/bottom/left) with only suffix and type — no initialValue.
100
+ * Used when defaults are stored separately in the subgroup spec.
101
+ *
102
+ * @param {string} prefix - e.g. "margin" or "padding"
103
+ * @returns {object[]}
104
+ */
105
+ function marginFields(prefix) {
106
+ return SIDES.map((side) => ({
107
+ suffix: `${prefix} ${side}`,
108
+ type: ZAPPIFEST_FIELDS.number_input,
109
+ }));
110
+ }
111
+
112
+ function paddingFields(prefix) {
113
+ return SIDES.map((side) => ({
114
+ suffix: `${prefix} ${side}`,
115
+ type: ZAPPIFEST_FIELDS.number_input,
116
+ }));
117
+ }
118
+
1
119
  /**
2
120
  * Appends new conditional_fields from conditions array
3
121
  * to config.conditional_fields
@@ -48,6 +166,13 @@ function createConditionalField(key, condition_value, category = "styles") {
48
166
  }
49
167
 
50
168
  module.exports = {
169
+ parseShorthand,
170
+ margin,
171
+ padding,
172
+ marginDefaults,
173
+ paddingDefaults,
174
+ marginFields,
175
+ paddingFields,
51
176
  withConditional,
52
177
  getConditionalKey,
53
178
  createConditionalField,
@@ -43,6 +43,7 @@ const TV_PLATFORMS = [
43
43
  "tvos_for_quickbrick",
44
44
  "samsung_tv",
45
45
  "lg_tv",
46
+ "vidaa",
46
47
  "vizio",
47
48
  ];
48
49
 
@@ -630,6 +631,14 @@ const MOBILE_ACTION_BUTTON_FIELDS = [
630
631
  type: ZAPPIFEST_FIELDS.number_input,
631
632
  suffix: "margin left",
632
633
  },
634
+ {
635
+ type: ZAPPIFEST_FIELDS.number_input,
636
+ suffix: "horizontal gutter",
637
+ },
638
+ {
639
+ type: ZAPPIFEST_FIELDS.number_input,
640
+ suffix: "vertical gutter",
641
+ },
633
642
  ];
634
643
 
635
644
  const TV_MENU_LABEL_FIELDS = [
@@ -129,6 +129,22 @@ function mobileActionButton({ label, description, defaults, isFirstButton }) {
129
129
  conditions.push(createConditionalField(labelEnabledKey, true));
130
130
  }
131
131
 
132
+ const assetAlignmentKey = keyPrefixGenerator("asset_alignment");
133
+
134
+ // horizontal_gutter fields depends on [asset_alignment: left or asset_alignment: right]
135
+ if (isKeyHasSuffix("horizontal_gutter", key)) {
136
+ conditions.push(
137
+ createConditionalField(assetAlignmentKey, ["left", "right"])
138
+ );
139
+ }
140
+
141
+ // vertical_gutter fields depends on [asset_alignment: above or asset_alignment: below]
142
+ if (isKeyHasSuffix("vertical_gutter", key)) {
143
+ conditions.push(
144
+ createConditionalField(assetAlignmentKey, ["above", "below"])
145
+ );
146
+ }
147
+
132
148
  return withConditional(conditions)(field);
133
149
  });
134
150
 
@@ -49,7 +49,9 @@ function mobileActionButtonsContainer({ label, description, defaults }) {
49
49
 
50
50
  // over_image_position depends on [positionKey: over_image]
51
51
  if (isKeyHasSuffix("over_image_position", key)) {
52
- conditions.push(createConditionalField(positionKey, "over_image"));
52
+ conditions.push(
53
+ createConditionalField(positionKey, defaults.position[0])
54
+ );
53
55
  }
54
56
 
55
57
  // horizontal_gutter depends on [stackingKey: horizontal]
@@ -33,7 +33,7 @@ const DEFAULT_MOBILE_ACTION_BUTTON_SHARED_DEFAULTS = {
33
33
  assetHeight: 24,
34
34
  assetWidth: 24,
35
35
  assetMarginTop: 0,
36
- assetMarginRight: 6,
36
+ assetMarginRight: 0,
37
37
  assetMarginBottom: 0,
38
38
  assetMarginLeft: 0,
39
39
  labelEnabled: true,
@@ -50,6 +50,8 @@ const DEFAULT_MOBILE_ACTION_BUTTON_SHARED_DEFAULTS = {
50
50
  marginRight: 0,
51
51
  marginBottom: 0,
52
52
  marginLeft: 0,
53
+ horizontalGutter: 8,
54
+ verticalGutter: 8,
53
55
  };
54
56
 
55
57
  const DEFAULT_MOBILE_ACTION_BUTTON_PRESETS = {
@@ -4,6 +4,7 @@ function platformIsTV(platform) {
4
4
  "android_tv_for_quickbrick",
5
5
  "amazon_fire_tv_for_quickbrick",
6
6
  "lg_tv",
7
+ "vidaa",
7
8
  "samsung_tv",
8
9
  "web",
9
10
  "vizio",
@@ -0,0 +1,20 @@
1
+ import { isMinusZero } from "..";
2
+
3
+ describe("isMinusZero", () => {
4
+ it("return true for minus zero", () => {
5
+ expect(isMinusZero(-0)).toBe(true);
6
+ });
7
+
8
+ it("return false for positive zero", () => {
9
+ expect(isMinusZero(0)).toBe(false);
10
+ });
11
+
12
+ it("return false for non-zero numbers", () => {
13
+ const inputs = [-1, 1, 100, 0.1, -0.1, Infinity, -Infinity];
14
+ expect.assertions(inputs.length);
15
+
16
+ inputs.forEach((input) => {
17
+ expect(isMinusZero(input)).toBe(false);
18
+ });
19
+ });
20
+ });
@@ -0,0 +1,27 @@
1
+ import { isZero } from "..";
2
+
3
+ describe("isZero", () => {
4
+ it("return true for zero and minus zero", () => {
5
+ expect(isZero(0)).toBe(true);
6
+ expect(isZero(-0)).toBe(true);
7
+ });
8
+
9
+ it("return false for non-zero numbers", () => {
10
+ const inputs = [-1, 1, 100, 0.1, -0.1, Infinity, -Infinity, NaN];
11
+ expect.assertions(inputs.length);
12
+
13
+ inputs.forEach((input) => {
14
+ expect(isZero(input)).toBe(false);
15
+ });
16
+ });
17
+
18
+ it("return false for non-number values", () => {
19
+ const inputs = ["0", "-0", null, undefined, "", {}, [], false, true];
20
+
21
+ expect.assertions(inputs.length);
22
+
23
+ inputs.forEach((input) => {
24
+ expect(isZero(input)).toBe(false);
25
+ });
26
+ });
27
+ });
@@ -0,0 +1,50 @@
1
+ import { requireNumber } from "..";
2
+
3
+ describe("requireNumber", () => {
4
+ it("return number if object key holds a number", () => {
5
+ const inputs = [-1, 1, 100, 0.2];
6
+ expect.assertions(inputs.length);
7
+
8
+ inputs.forEach((input) => {
9
+ const output = requireNumber({ value: input }, "value");
10
+ expect(output).toBe(input);
11
+ });
12
+ });
13
+
14
+ it("return number if object key holds a string as number", () => {
15
+ const inputs = ["-1", "1", "100", "0.2"];
16
+ expect.assertions(inputs.length);
17
+
18
+ inputs.forEach((input) => {
19
+ const output = requireNumber({ value: input }, "value");
20
+ expect(output).toBe(Number(input));
21
+ });
22
+ });
23
+
24
+ it("return null if object key holds zero", () => {
25
+ expect(requireNumber({ value: 0 }, "value")).toBeNull();
26
+ expect(requireNumber({ value: "0" }, "value")).toBeNull();
27
+ });
28
+
29
+ it("return null if object key is missing or not a number", () => {
30
+ const cases = [
31
+ [{}, "value"],
32
+ [{ value: null }, "value"],
33
+ [{ value: undefined }, "value"],
34
+ [{ value: NaN }, "value"],
35
+ [{ value: "" }, "value"],
36
+ [{ value: "vfdvf" }, "value"],
37
+ [{ value: {} }, "value"],
38
+ [{ value: [] }, "value"],
39
+ [null, "value"],
40
+ [undefined, "value"],
41
+ ] as const;
42
+
43
+ expect.assertions(cases.length);
44
+
45
+ cases.forEach(([obj, key]) => {
46
+ const output = requireNumber(obj, key);
47
+ expect(output).toBeNull();
48
+ });
49
+ });
50
+ });