@applicaster/zapp-react-native-ui-components 16.0.0-rc.6 → 16.0.0-rc.61

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 (129) 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/BackgroundImage/BackgroundImage.android.tv.tsx +28 -0
  5. package/Components/BackgroundImage/BackgroundImage.ios.tv.tsx +24 -0
  6. package/Components/BackgroundImage/BackgroundImage.tsx +42 -0
  7. package/Components/BackgroundImage/index.ts +1 -0
  8. package/Components/CellRendererResolver/index.ts +2 -2
  9. package/Components/ComponentResolver/__tests__/componentResolver.test.js +1 -1
  10. package/Components/FocusableGroup/FocusableTvOS.tsx +11 -7
  11. package/Components/GeneralContentScreen/GeneralContentScreenHookAdapter.tsx +39 -0
  12. package/Components/GeneralContentScreen/__tests__/GeneralContentScreenHookAdapter.test.tsx +64 -0
  13. package/Components/GeneralContentScreen/__tests__/HookContentFocusGroup.web.test.tsx +91 -0
  14. package/Components/GeneralContentScreen/hookAdapter/__tests__/networkService.test.ts +74 -0
  15. package/Components/GeneralContentScreen/hookAdapter/__tests__/runInBackground.test.ts +139 -0
  16. package/Components/GeneralContentScreen/hookAdapter/__tests__/validationHelper.test.ts +124 -0
  17. package/Components/GeneralContentScreen/hookAdapter/logger.ts +6 -0
  18. package/Components/GeneralContentScreen/hookAdapter/networkService.ts +53 -0
  19. package/Components/GeneralContentScreen/hookAdapter/runInBackground.ts +48 -0
  20. package/Components/GeneralContentScreen/hookAdapter/validationHelper.ts +72 -0
  21. package/Components/GeneralContentScreen/hookFocus/index.tsx +13 -0
  22. package/Components/GeneralContentScreen/hookFocus/index.web.tsx +69 -0
  23. package/Components/GeneralContentScreen/index.ts +2 -0
  24. package/Components/Layout/FullWidthRow.tsx +38 -0
  25. package/Components/Layout/TV/ScreenContainer.tsx +5 -0
  26. package/Components/Layout/TV/__tests__/__snapshots__/index.test.tsx.snap +0 -1
  27. package/Components/Layout/TV/index.tsx +3 -4
  28. package/Components/Layout/TV/index.web.tsx +2 -3
  29. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +16 -5
  30. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +47 -21
  31. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +34 -6
  32. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.tsx +1 -1
  33. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/__tests__/withDimensions.test.ts +420 -125
  34. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/index.ts +114 -19
  35. package/Components/MasterCell/DefaultComponents/Image/hoc/withDimensions.tsx +19 -8
  36. package/Components/MasterCell/DefaultComponents/ImageContainer/index.tsx +5 -3
  37. package/Components/MasterCell/DefaultComponents/PressableView.tsx +29 -9
  38. package/Components/MasterCell/DefaultComponents/Text/hooks/useText.ts +4 -0
  39. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +154 -100
  40. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/AssetComponent.tsx +15 -4
  41. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +56 -22
  42. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +6 -4
  43. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +3 -1
  44. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +32 -21
  45. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +15 -4
  46. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +23 -16
  47. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +8 -3
  48. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +2 -2
  49. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/insertButtonsBetweenLabels.test.ts +45 -13
  50. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +2 -2
  51. package/Components/MasterCell/contexts/PressedStateContext.ts +3 -0
  52. package/Components/MasterCell/dataAdapter.ts +15 -5
  53. package/Components/MasterCell/hooks/index.ts +2 -0
  54. package/Components/MasterCell/hooks/usePressedState.ts +4 -0
  55. package/Components/MasterCell/index.tsx +30 -5
  56. package/Components/MasterCell/utils/__tests__/resolveColor.test.js +169 -132
  57. package/Components/MasterCell/utils/__tests__/resolveColorForProp.test.js +92 -0
  58. package/Components/MasterCell/utils/index.ts +85 -46
  59. package/Components/ModalComponent/AudioPlayer/Components/Action.tsx +314 -0
  60. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +395 -0
  61. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +763 -0
  62. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +653 -0
  63. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +874 -0
  64. package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +89 -0
  65. package/Components/ModalComponent/AudioPlayer/Components/index.ts +11 -0
  66. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +199 -0
  67. package/Components/ModalComponent/BottomSheetDragArea.tsx +33 -0
  68. package/Components/ModalComponent/BottomSheetModalContent.tsx +386 -42
  69. package/Components/ModalComponent/Button/index.tsx +5 -3
  70. package/Components/ModalComponent/Header/index.tsx +20 -7
  71. package/Components/ModalComponent/Header/utils.ts +1 -1
  72. package/Components/ModalComponent/ModalBlocksStyleContext.tsx +10 -0
  73. package/Components/ModalComponent/SortableList.tsx +142 -0
  74. package/Components/ModalComponent/TextInputContent.tsx +100 -0
  75. package/Components/ModalComponent/__tests__/BottomSheetDragArea.test.tsx +74 -0
  76. package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +238 -0
  77. package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +120 -0
  78. package/Components/ModalComponent/__tests__/showTextInput.test.ts +95 -0
  79. package/Components/ModalComponent/index.tsx +11 -0
  80. package/Components/ModalComponent/presets/__tests__/getCell.test.ts +66 -0
  81. package/Components/ModalComponent/presets/dynamicCollectionCell.tsx +78 -0
  82. package/Components/ModalComponent/presets/index.ts +27 -0
  83. package/Components/ModalComponent/presets/selectableCell.tsx +28 -0
  84. package/Components/ModalComponent/presets/standardCell.tsx +41 -0
  85. package/Components/ModalComponent/presets/types.ts +35 -0
  86. package/Components/ModalComponent/showTextInput.ts +43 -0
  87. package/Components/ModalComponent/utils.ts +76 -20
  88. package/Components/OfflineHandler/__tests__/__snapshots__/index.test.tsx.snap +4 -85
  89. package/Components/OfflineHandler/__tests__/index.test.tsx +78 -18
  90. package/Components/OfflineHandler/hooks.ts +123 -0
  91. package/Components/OfflineHandler/index.tsx +51 -73
  92. package/Components/OfflineHandler/utils/index.ts +1 -13
  93. package/Components/PlayerContainer/PlayerContainer.tsx +25 -17
  94. package/Components/PlayerContainer/ProgramInfo/index.tsx +3 -4
  95. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +337 -0
  96. package/Components/PreloaderWrapper/index.tsx +1 -1
  97. package/Components/River/ComponentsMap/ComponentsMap.tsx +2 -2
  98. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +3 -15
  99. package/Components/Screen/TV/hooks/__tests__/useAfterPaint.test.ts +60 -0
  100. package/Components/Screen/TV/hooks/index.ts +2 -0
  101. package/Components/Screen/TV/hooks/useAfterPaint.ts +23 -0
  102. package/Components/Screen/TV/index.web.tsx +16 -7
  103. package/Components/Screen/index.tsx +8 -4
  104. package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +5 -3
  105. package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +1 -1
  106. package/Components/ScreenRevealManager/Overlay.tsx +34 -0
  107. package/Components/ScreenRevealManager/__tests__/Overlay.test.tsx +88 -0
  108. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +8 -19
  109. package/Components/TopCutoffOverlay/index.tsx +2 -2
  110. package/Components/Transitioner/Transitioner.tsx +28 -17
  111. package/Components/VideoLive/LiveImageManager.ts +41 -43
  112. package/Components/VideoModal/utils.ts +6 -1
  113. package/Components/ZappFrameworkComponents/BarView/BarView.tsx +12 -5
  114. package/Components/ZappFrameworkComponents/BarView/__tests__/BarView.test.tsx +2 -2
  115. package/Contexts/CachedDimensionsContext/__tests__/index.test.ts +154 -0
  116. package/Contexts/CachedDimensionsContext/index.ts +17 -2
  117. package/Helpers/ComponentCellSelectionHelper/index.js +0 -6
  118. package/Helpers/index.js +7 -40
  119. package/package.json +6 -5
  120. package/Components/Layout/TV/LayoutBackground.tsx +0 -31
  121. package/Components/OfflineHandler/NotificationView/NotificationView.lg.tsx +0 -112
  122. package/Components/OfflineHandler/NotificationView/NotificationView.samsung.tsx +0 -107
  123. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +0 -153
  124. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +0 -66
  125. package/Components/OfflineHandler/NotificationView/utils.ts +0 -34
  126. package/Components/Screen/utils.ts +0 -16
  127. package/Helpers/Analytics/index.js +0 -95
  128. /package/Components/MasterCell/DefaultComponents/ButtonContainerView/{index.tv.android.tsx → index.android.tv.tsx} +0 -0
  129. /package/Decorators/ZappPipesDataConnector/__tests__/{Hero.js → Hero.tsx} +0 -0
@@ -1,22 +1,23 @@
1
1
  import React, { useMemo } from "react";
2
2
  import * as R from "ramda";
3
- import validateColor from "validate-color";
3
+ import memoizee from "memoizee";
4
+
4
5
  import { platformSelect } from "@applicaster/zapp-react-native-utils/reactUtils";
5
6
  import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
6
-
7
- import { masterCellLogger } from "../logger";
8
- import { getCellState } from "../../Cell/utils";
7
+ import { isValidColor } from "@applicaster/zapp-react-native-utils/colorUtils";
9
8
  import { getColorFromData } from "@applicaster/zapp-react-native-utils/cellUtils";
10
- import { get } from "@applicaster/zapp-react-native-utils/utils";
11
- import { isCellSelected, useBehaviorUpdate } from "./behaviorProvider";
9
+ import { pathOr, get, isNil } from "@applicaster/zapp-react-native-utils/utils";
10
+ import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
12
11
  import { useRoute } from "@applicaster/zapp-react-native-utils/reactHooks";
13
12
  import { useScreenStateStore } from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useScreenStateStore";
14
- import memoizee from "memoizee";
15
- import stringify from "fast-json-stable-stringify";
13
+ import { isNotEmptyString } from "@applicaster/zapp-react-native-utils/stringUtils";
14
+ import { masterCellLogger } from "../logger";
15
+ import { getCellState } from "../../Cell/utils";
16
+ import { isCellSelected, useBehaviorUpdate } from "./behaviorProvider";
16
17
  import { DataProvider } from "../DefaultComponents/DataProvider";
17
18
 
18
19
  const hasElementSpecificViewType = (viewType) => (element) => {
19
- if (R.isNil(element)) {
20
+ if (isNil(element)) {
20
21
  return false;
21
22
  }
22
23
 
@@ -28,12 +29,20 @@ const hasElementSpecificViewType = (viewType) => (element) => {
28
29
  return hasElementsSpecificViewType(viewType)(element.elements);
29
30
  };
30
31
 
32
+ export const hasElementsSpecificViewType = (viewType) => (elements) => {
33
+ if (isNilOrEmpty(elements)) {
34
+ return false;
35
+ }
36
+
37
+ return R.any(hasElementSpecificViewType(viewType))(elements);
38
+ };
39
+
31
40
  const logWarning = (
32
41
  colorValueFromCellStyle,
33
42
  colorFromProp,
34
43
  colorValueFromEntry
35
44
  ) => {
36
- if (R.isNil(colorValueFromEntry)) {
45
+ if (isNil(colorValueFromEntry)) {
37
46
  masterCellLogger.warn({
38
47
  message: `Cannot resolve property ${colorValueFromCellStyle} from the entry.`,
39
48
  data: {
@@ -44,15 +53,10 @@ const logWarning = (
44
53
  }
45
54
  };
46
55
 
47
- export const hasElementsSpecificViewType = (viewType) => (elements) => {
48
- if (R.isNil(elements) || R.isEmpty(elements)) {
49
- return false;
50
- }
51
-
52
- return R.any(hasElementSpecificViewType(viewType))(elements);
53
- };
54
-
55
- function resolveColorForProp(entry: any, colorFromProp: string | undefined) {
56
+ export function resolveColorForProp(
57
+ entry: any,
58
+ colorFromProp: string | undefined
59
+ ) {
56
60
  if (!colorFromProp) {
57
61
  return undefined;
58
62
  }
@@ -62,9 +66,7 @@ function resolveColorForProp(entry: any, colorFromProp: string | undefined) {
62
66
  colorFromProp.split(".")
63
67
  );
64
68
 
65
- const color = colorFromProp.replace(".00", "").replace(".0", ""); // https://github.com/dreamyguy/validate-color/issues/44
66
-
67
- if (nestedEntryValue === undefined && !validateColor(color)) {
69
+ if (nestedEntryValue === undefined && !isValidColor(colorFromProp)) {
68
70
  logWarning(colorFromProp, colorFromProp, nestedEntryValue);
69
71
 
70
72
  return undefined;
@@ -86,36 +88,73 @@ function resolveColorForProp(entry: any, colorFromProp: string | undefined) {
86
88
 
87
89
  const getColorKeys = memoizee((style) => {
88
90
  const styleKeys = Object.keys(style);
89
- const colorKeys = styleKeys.filter((key) => /color/i.test(key));
90
91
 
91
- return colorKeys;
92
+ return styleKeys.filter((key) => /color/i.test(key));
92
93
  });
93
94
 
94
- export const resolveColor = memoizee(
95
- (entry, style) => {
96
- if (style === null || style === undefined) {
97
- return style;
98
- }
95
+ /**
96
+ * A color style value is either a literal color (e.g. "#FFFFFF") or a
97
+ * data-mapping path into the entry. Data mappings always point at the entry
98
+ * `extensions` object (e.g. "extensions.brandColor"), so we only need to do a
99
+ * path lookup when the value starts with "extensions" — everything else is a
100
+ * literal color and can be returned as-is.
101
+ *
102
+ * This avoids the previous, very expensive approach that ran `validateColor`
103
+ * on every value and memoized on a full `fast-json-stable-stringify` of the
104
+ * entire entry.
105
+ */
106
+ const EXTENSIONS_PREFIX = "extensions";
107
+
108
+ const isDataMappingPath = (value: string): boolean =>
109
+ typeof value === "string" && value.startsWith(EXTENSIONS_PREFIX);
110
+
111
+ export const resolveColor = (
112
+ entry,
113
+ style,
114
+ allowDynamicColorsOutsideExtensions
115
+ ) => {
116
+ if (style === null || style === undefined) {
117
+ return style;
118
+ }
119
+
120
+ const colorKeys = getColorKeys(style);
121
+
122
+ if (colorKeys.length === 0) {
123
+ return style;
124
+ }
125
+
126
+ return colorKeys.reduce(
127
+ (acc, key) => {
128
+ const value = acc[key];
99
129
 
100
- return getColorKeys(style).reduce(
101
- (acc, value) => {
102
- if (acc[value] && typeof acc[value] === "string") {
103
- const colorStyle = resolveColorForProp(entry, acc[value]);
130
+ // 1. The Expensive Edge-Case (Only for the app with root mappings)
131
+ if (allowDynamicColorsOutsideExtensions && isNotEmptyString(value)) {
132
+ const possibleColor = resolveColorForProp(entry, value);
104
133
 
105
- acc[value] = colorStyle;
106
- }
134
+ acc[key] = isValidColor(possibleColor) ? possibleColor : null;
107
135
 
108
136
  return acc;
109
- },
110
- { ...style }
111
- );
112
- },
113
- {
114
- normalizer: (args) => {
115
- return [stringify(args[0]), stringify(args[1])].join("|");
137
+ }
138
+
139
+ // 2. The Fast Path (For 99% of apps)
140
+ if (isDataMappingPath(value)) {
141
+ // resolve the mapped color from the entry; fall back to null
142
+ // (RN ignores null style values) when the path is missing
143
+ const possibleColor = pathOr(null, value.split("."), entry);
144
+
145
+ acc[key] = possibleColor;
146
+
147
+ return acc;
148
+ }
149
+
150
+ // 3. Default Case: Treat as a raw color string
151
+ acc[key] = value;
152
+
153
+ return acc;
116
154
  },
117
- }
118
- );
155
+ { ...style }
156
+ );
157
+ };
119
158
 
120
159
  export function isVideoPreviewEnabled({
121
160
  enable_video_preview = false,
@@ -238,7 +277,7 @@ const recursiveCloneElement = (focused: boolean) => (element) => {
238
277
  };
239
278
 
240
279
  export const recursiveCloneElementsWithState = (focused: boolean, children) => {
241
- if (R.isNil(children) || R.isEmpty(children)) {
280
+ if (isNilOrEmpty(children)) {
242
281
  return undefined;
243
282
  }
244
283
 
@@ -252,7 +291,7 @@ export const cloneChildrenWithIds = (
252
291
  ids: string[],
253
292
  children: React.ReactElement[]
254
293
  ) => {
255
- if (R.isNil(children) || R.isEmpty(children)) {
294
+ if (isNilOrEmpty(children)) {
256
295
  return undefined;
257
296
  }
258
297
 
@@ -0,0 +1,314 @@
1
+ /**
2
+ * Modal Building Block — Action
3
+ *
4
+ * Single-file Expo Snack demo for the Action list item building block spec.
5
+ * Three forms (text only / leading icon + text / leading + text + trailing)
6
+ * are driven by data — no separate variants.
7
+ *
8
+ * Green (configurable): background, title, trailingIcon.chevronAsset
9
+ * White (hardcoded): container padding/gutter, icon size, leading icon assets,
10
+ * leading/trailing visibility logic from data
11
+ */
12
+
13
+ import React from "react";
14
+ import {
15
+ Image,
16
+ Platform,
17
+ Pressable,
18
+ StyleSheet,
19
+ Text,
20
+ View,
21
+ } from "react-native";
22
+
23
+ const styles = StyleSheet.create({
24
+ row: {
25
+ flexDirection: "row",
26
+ alignItems: "center",
27
+ },
28
+ });
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Configuration (green / editable fields from spec)
32
+ // ---------------------------------------------------------------------------
33
+
34
+ export type TextTransform =
35
+ | "default"
36
+ | "lowercase"
37
+ | "uppercase"
38
+ | "capitalize";
39
+
40
+ export type BackgroundConfiguration = {
41
+ defaultColor: string;
42
+ focusedColor: string;
43
+ cornerRadius: number;
44
+ };
45
+
46
+ export type TitleConfiguration = {
47
+ fontColor: string;
48
+ focusedFontColor: string;
49
+ iosFontFamily: string;
50
+ androidFontFamily: string;
51
+ fontSize: number;
52
+ lineHeight: number;
53
+ iosLetterSpacing: number;
54
+ androidLetterSpacing: number;
55
+ textTransform: TextTransform;
56
+ };
57
+
58
+ export type TrailingIconConfiguration = {
59
+ chevronAsset?: string;
60
+ };
61
+
62
+ export type ActionConfiguration = {
63
+ background: BackgroundConfiguration;
64
+ title: TitleConfiguration;
65
+ trailingIcon: TrailingIconConfiguration;
66
+ };
67
+
68
+ export const DEFAULT_BACKGROUND_CONFIGURATION: BackgroundConfiguration = {
69
+ defaultColor: "transparent",
70
+ focusedColor: "rgba(62, 62, 62, 1)",
71
+ cornerRadius: 12,
72
+ };
73
+
74
+ export const DEFAULT_TITLE_CONFIGURATION: TitleConfiguration = {
75
+ fontColor: "#FFFFFF",
76
+ focusedFontColor: "#FFFFFF",
77
+ iosFontFamily: "SFProText-Semibold",
78
+ androidFontFamily: "Roboto-Medium",
79
+ fontSize: 15,
80
+ lineHeight: 24,
81
+ iosLetterSpacing: -0.2,
82
+ androidLetterSpacing: 0,
83
+ textTransform: "default",
84
+ };
85
+
86
+ export const DEFAULT_TRAILING_ICON_CONFIGURATION: TrailingIconConfiguration =
87
+ {};
88
+
89
+ export const DEFAULT_ACTION_CONFIGURATION: ActionConfiguration = {
90
+ background: DEFAULT_BACKGROUND_CONFIGURATION,
91
+ title: DEFAULT_TITLE_CONFIGURATION,
92
+ trailingIcon: DEFAULT_TRAILING_ICON_CONFIGURATION,
93
+ };
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Data (runtime — controls which elements are shown)
97
+ // ---------------------------------------------------------------------------
98
+
99
+ export type LeadingActionType =
100
+ | "addToQueue"
101
+ | "addToPlaylist"
102
+ | "favorite"
103
+ | "share"
104
+ | "sleepTimer"
105
+ | "playbackSpeed";
106
+
107
+ export type ActionData = {
108
+ title: string;
109
+ /** When set, leading icon is shown (Enable: On). */
110
+ leadingActionType?: LeadingActionType;
111
+ /** When true, trailing chevron is shown (Enable: On). */
112
+ showTrailingChevron?: boolean;
113
+ };
114
+
115
+ export type AudioPlayerActionProps = {
116
+ configuration: ActionConfiguration;
117
+ data: ActionData;
118
+ focused?: boolean;
119
+ onPress?: () => void;
120
+ };
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // Hardcoded spec values (white fields)
124
+ // ---------------------------------------------------------------------------
125
+
126
+ const CONTAINER_SPEC = {
127
+ paddingTop: 13,
128
+ paddingRight: 10,
129
+ paddingBottom: 13,
130
+ paddingLeft: 10,
131
+ horizontalGutter: 12,
132
+ };
133
+
134
+ const ICON_SPEC = {
135
+ width: 24,
136
+ height: 24,
137
+ };
138
+
139
+ const DEFAULT_CHEVRON_ASSET =
140
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAALElEQVR42mNgGAXUBlu2bPmPjIeWBeiGDy0Lhrbrh1/qGXqZaxSMglEw0gEA4c+IpUrpYX4AAAAASUVORK5CYII=";
141
+
142
+ /** Hardcoded: action type → leading icon asset (base64 PNG). */
143
+ const LEADING_ICON_ASSETS: Record<LeadingActionType, string> = {
144
+ addToQueue:
145
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAALElEQVR42mNgGAUjE/xHAzS3gFgweC0Y+nEwGslkBRFFwTj8LBjNyaNgmAMAJTf+EEEZcS0AAAAASUVORK5CYII=",
146
+ addToPlaylist:
147
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAKUlEQVR42mNgGAVDBvxHA6MWjFpAvEGkglELKLdgNJmOWkA7C0YBzQAAivu+UKRR1ZIAAAAASUVORK5CYII=",
148
+ favorite:
149
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAKklEQVR42mNgGAWjYBTgBP+xAGLkRi0YQhbgM4wqBo9aMHgsGAWjgCAAALyAZqh4yyZZAAAAAElFTkSuQmCC",
150
+ share:
151
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAK0lEQVR42mNgGAVDDvxHAqMWjFowUiz4TwQYgRYQYxBFlo1aMPAWjAK6AgD2g7pUUT7VeAAAAABJRU5ErkJggg==",
152
+ sleepTimer:
153
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAYUlEQVR42tWUSQ4AIAgD+f+n8eJRliIQ6MmEZTSxJZokvipdXgZpAbzOO17AglIXI7XULxmCoEMwRGuWam6AdRsN4IJYTRH4PIDkhxTA92zUPNBcFDDHaOVR0RJ2LXG9QgemFVK8tpo4PwAAAABJRU5ErkJggg==",
154
+ playbackSpeed:
155
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAK0lEQVR42mNgGAVDDvxHAqMWjFowUiz4TwQYgRYQYxBFlo1aMPAWjAK6AgD2g7pUUT7VeAAAAABJRU5ErkJggg==",
156
+ };
157
+
158
+ function getTextTransformStyle(
159
+ textTransform: TextTransform
160
+ ): "none" | "lowercase" | "uppercase" | "capitalize" {
161
+ return textTransform === "default" ? "none" : textTransform;
162
+ }
163
+
164
+ function shouldShowLeadingIcon(leadingActionType?: LeadingActionType) {
165
+ return !!leadingActionType;
166
+ }
167
+
168
+ function shouldShowTrailingIcon(showTrailingChevron?: boolean) {
169
+ return !!showTrailingChevron;
170
+ }
171
+
172
+ // ---------------------------------------------------------------------------
173
+ // Inner components
174
+ // ---------------------------------------------------------------------------
175
+
176
+ type ActionContainerProps = {
177
+ configuration: BackgroundConfiguration;
178
+ focused: boolean;
179
+ children: React.ReactNode;
180
+ onPress?: () => void;
181
+ };
182
+
183
+ function ActionContainer({
184
+ configuration,
185
+ focused,
186
+ children,
187
+ onPress,
188
+ }: ActionContainerProps) {
189
+ return (
190
+ <Pressable
191
+ onPress={onPress}
192
+ style={({ pressed }) => ({
193
+ backgroundColor:
194
+ pressed || focused
195
+ ? configuration.focusedColor
196
+ : configuration.defaultColor,
197
+ borderRadius: configuration.cornerRadius,
198
+ paddingTop: CONTAINER_SPEC.paddingTop,
199
+ paddingRight: CONTAINER_SPEC.paddingRight,
200
+ paddingBottom: CONTAINER_SPEC.paddingBottom,
201
+ paddingLeft: CONTAINER_SPEC.paddingLeft,
202
+ })}
203
+ >
204
+ {children}
205
+ </Pressable>
206
+ );
207
+ }
208
+
209
+ type ActionTitleProps = {
210
+ title: string;
211
+ configuration: TitleConfiguration;
212
+ focused: boolean;
213
+ };
214
+
215
+ function ActionTitle({ title, configuration, focused }: ActionTitleProps) {
216
+ const letterSpacing =
217
+ Platform.OS === "ios"
218
+ ? configuration.iosLetterSpacing
219
+ : configuration.androidLetterSpacing;
220
+
221
+ return (
222
+ <Text
223
+ style={{
224
+ flex: 1,
225
+ color: focused
226
+ ? configuration.focusedFontColor
227
+ : configuration.fontColor,
228
+ fontSize: configuration.fontSize,
229
+ lineHeight: configuration.lineHeight,
230
+ letterSpacing,
231
+ fontFamily: Platform.select({
232
+ ios: configuration.iosFontFamily,
233
+ android: configuration.androidFontFamily,
234
+ default: configuration.androidFontFamily,
235
+ }),
236
+ textTransform: getTextTransformStyle(configuration.textTransform),
237
+ }}
238
+ numberOfLines={1}
239
+ >
240
+ {title}
241
+ </Text>
242
+ );
243
+ }
244
+
245
+ type ActionLeadingIconProps = {
246
+ actionType: LeadingActionType;
247
+ };
248
+
249
+ function ActionLeadingIcon({ actionType }: ActionLeadingIconProps) {
250
+ return (
251
+ <Image
252
+ source={{ uri: LEADING_ICON_ASSETS[actionType] }}
253
+ style={{ width: ICON_SPEC.width, height: ICON_SPEC.height }}
254
+ resizeMode="contain"
255
+ />
256
+ );
257
+ }
258
+
259
+ type ActionTrailingIconProps = {
260
+ configuration: TrailingIconConfiguration;
261
+ };
262
+
263
+ function ActionTrailingIcon({ configuration }: ActionTrailingIconProps) {
264
+ const asset = configuration.chevronAsset ?? DEFAULT_CHEVRON_ASSET;
265
+
266
+ return (
267
+ <Image
268
+ source={{ uri: asset }}
269
+ style={{ width: ICON_SPEC.width, height: ICON_SPEC.height }}
270
+ resizeMode="contain"
271
+ />
272
+ );
273
+ }
274
+
275
+ // ---------------------------------------------------------------------------
276
+ // AudioPlayerAction
277
+ // ---------------------------------------------------------------------------
278
+
279
+ export function AudioPlayerAction({
280
+ configuration,
281
+ data,
282
+ focused = false,
283
+ onPress,
284
+ }: AudioPlayerActionProps) {
285
+ const { background, title, trailingIcon } = configuration;
286
+ const { title: label, leadingActionType, showTrailingChevron } = data;
287
+
288
+ const showLeading = shouldShowLeadingIcon(leadingActionType);
289
+ const showTrailing = shouldShowTrailingIcon(showTrailingChevron);
290
+
291
+ return (
292
+ <ActionContainer
293
+ configuration={background}
294
+ focused={focused}
295
+ onPress={onPress}
296
+ >
297
+ <View style={styles.row}>
298
+ {showLeading ? (
299
+ <>
300
+ <ActionLeadingIcon actionType={leadingActionType} />
301
+ <View style={{ width: CONTAINER_SPEC.horizontalGutter }} />
302
+ </>
303
+ ) : null}
304
+ <ActionTitle title={label} configuration={title} focused={focused} />
305
+ {showTrailing ? (
306
+ <>
307
+ <View style={{ width: CONTAINER_SPEC.horizontalGutter }} />
308
+ <ActionTrailingIcon configuration={trailingIcon} />
309
+ </>
310
+ ) : null}
311
+ </View>
312
+ </ActionContainer>
313
+ );
314
+ }