@applicaster/zapp-react-native-ui-components 16.0.0-rc.9 → 16.0.0-rc.91

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 (146) 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/Cell/Cell.tsx +3 -6
  5. package/Components/Cell/TvOSCellComponent.tsx +0 -1
  6. package/Components/CellRendererResolver/index.ts +2 -2
  7. package/Components/ComponentResolver/__tests__/componentResolver.test.js +1 -1
  8. package/Components/FocusableGroup/FocusableTvOS.tsx +11 -7
  9. package/Components/GeneralContentScreen/GeneralContentScreenHookAdapter.tsx +54 -0
  10. package/Components/GeneralContentScreen/__tests__/GeneralContentScreenHookAdapter.test.tsx +64 -0
  11. package/Components/GeneralContentScreen/__tests__/HookContentFocusGroup.web.test.tsx +91 -0
  12. package/Components/GeneralContentScreen/hookAdapter/__tests__/networkService.test.ts +74 -0
  13. package/Components/GeneralContentScreen/hookAdapter/__tests__/runInBackground.test.ts +139 -0
  14. package/Components/GeneralContentScreen/hookAdapter/__tests__/skipExpression.test.ts +294 -0
  15. package/Components/GeneralContentScreen/hookAdapter/__tests__/validationHelper.test.ts +188 -0
  16. package/Components/GeneralContentScreen/hookAdapter/logger.ts +6 -0
  17. package/Components/GeneralContentScreen/hookAdapter/networkService.ts +53 -0
  18. package/Components/GeneralContentScreen/hookAdapter/runInBackground.ts +48 -0
  19. package/Components/GeneralContentScreen/hookAdapter/skipExpression.ts +252 -0
  20. package/Components/GeneralContentScreen/hookAdapter/validationHelper.ts +90 -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/MasterCell/DefaultComponents/ActionButton.tsx +21 -6
  26. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +47 -21
  27. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/ActionButtonController.tsx +19 -4
  28. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/__tests__/ActionButtonController.test.tsx +43 -1
  29. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +34 -6
  30. package/Components/MasterCell/DefaultComponents/Button.tsx +1 -0
  31. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/__tests__/withDimensions.test.ts +420 -125
  32. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/index.ts +114 -19
  33. package/Components/MasterCell/DefaultComponents/Image/hoc/withDimensions.tsx +19 -8
  34. package/Components/MasterCell/DefaultComponents/ImageContainer/index.tsx +5 -3
  35. package/Components/MasterCell/DefaultComponents/PressableView.tsx +29 -9
  36. package/Components/MasterCell/DefaultComponents/Text/hooks/useText.ts +4 -0
  37. package/Components/MasterCell/DefaultComponents/Toggle.tsx +36 -14
  38. package/Components/MasterCell/DefaultComponents/__tests__/Toggle.test.tsx +167 -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/SharedUI/CollapsibleTextContainer/__tests__/factory.test.ts +32 -0
  52. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/index.ts +1 -2
  53. package/Components/MasterCell/contexts/PressedStateContext.ts +3 -0
  54. package/Components/MasterCell/dataAdapter.ts +15 -5
  55. package/Components/MasterCell/hooks/index.ts +2 -0
  56. package/Components/MasterCell/hooks/usePressedState.ts +4 -0
  57. package/Components/MasterCell/index.tsx +30 -5
  58. package/Components/MasterCell/utils/__tests__/resolveColor.test.js +169 -132
  59. package/Components/MasterCell/utils/__tests__/resolveColorForProp.test.js +92 -0
  60. package/Components/MasterCell/utils/index.ts +85 -46
  61. package/Components/ModalComponent/ActionsBottomSheet/ActionButton.tsx +56 -0
  62. package/Components/ModalComponent/ActionsBottomSheet/__tests__/openActionsBottomSheet.test.ts +38 -0
  63. package/Components/ModalComponent/ActionsBottomSheet/boundActionButton.tsx +24 -0
  64. package/Components/ModalComponent/ActionsBottomSheet/index.ts +9 -0
  65. package/Components/ModalComponent/ActionsBottomSheet/openActionsBottomSheet.ts +61 -0
  66. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +392 -0
  67. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +728 -0
  68. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +628 -0
  69. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +982 -0
  70. package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +94 -0
  71. package/Components/ModalComponent/AudioPlayer/Components/__tests__/Item.test.tsx +113 -0
  72. package/Components/ModalComponent/AudioPlayer/Components/index.ts +9 -0
  73. package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +248 -0
  74. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +386 -0
  75. package/Components/ModalComponent/BottomSheetDragArea.tsx +33 -0
  76. package/Components/ModalComponent/BottomSheetModalContent.tsx +419 -50
  77. package/Components/ModalComponent/Button/index.tsx +5 -3
  78. package/Components/ModalComponent/Header/CloseButton.tsx +12 -0
  79. package/Components/ModalComponent/Header/__tests__/CloseButton.test.tsx +72 -0
  80. package/Components/ModalComponent/Header/index.tsx +20 -7
  81. package/Components/ModalComponent/Header/utils.ts +1 -1
  82. package/Components/ModalComponent/ModalBlocksStyleContext.tsx +10 -0
  83. package/Components/ModalComponent/SortableList.tsx +157 -0
  84. package/Components/ModalComponent/SortableList.web.tsx +34 -0
  85. package/Components/ModalComponent/TextInputContent.tsx +112 -0
  86. package/Components/ModalComponent/__tests__/BottomSheetDragArea.test.tsx +74 -0
  87. package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +270 -0
  88. package/Components/ModalComponent/__tests__/SortableList.web.test.tsx +16 -0
  89. package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +156 -0
  90. package/Components/ModalComponent/__tests__/showTextInput.test.ts +95 -0
  91. package/Components/ModalComponent/index.tsx +18 -9
  92. package/Components/ModalComponent/presets/__tests__/getCell.test.ts +90 -0
  93. package/Components/ModalComponent/presets/dynamicCollectionCell.tsx +78 -0
  94. package/Components/ModalComponent/presets/index.ts +27 -0
  95. package/Components/ModalComponent/presets/selectableCell.tsx +28 -0
  96. package/Components/ModalComponent/presets/standardCell.tsx +41 -0
  97. package/Components/ModalComponent/presets/types.ts +35 -0
  98. package/Components/ModalComponent/showTextInput.ts +43 -0
  99. package/Components/ModalComponent/utils.ts +82 -21
  100. package/Components/OfflineHandler/__tests__/__snapshots__/index.test.tsx.snap +4 -85
  101. package/Components/OfflineHandler/__tests__/index.test.tsx +78 -18
  102. package/Components/OfflineHandler/hooks.ts +111 -0
  103. package/Components/OfflineHandler/index.tsx +51 -73
  104. package/Components/OfflineHandler/utils/index.ts +1 -13
  105. package/Components/PlayerContainer/PlayerContainer.tsx +25 -17
  106. package/Components/PlayerContainer/ProgramInfo/index.tsx +3 -4
  107. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +337 -0
  108. package/Components/PreloaderWrapper/index.tsx +1 -1
  109. package/Components/River/ComponentsMap/ComponentsMap.tsx +87 -61
  110. package/Components/River/River.tsx +4 -0
  111. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +140 -89
  112. package/Components/River/__tests__/componentsMap.test.js +8 -3
  113. package/Components/River/__tests__/resultCallback.test.tsx +47 -0
  114. package/Components/Screen/index.tsx +8 -4
  115. package/Components/Screen/navigationHandler.ts +8 -8
  116. package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +5 -3
  117. package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +1 -1
  118. package/Components/ScreenRevealManager/ScreenRevealManager.ts +18 -2
  119. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +36 -0
  120. package/Components/ScreenRevealManager/__tests__/withScreenRevealManager.test.tsx +56 -0
  121. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +21 -4
  122. package/Components/TopCutoffOverlay/index.tsx +2 -2
  123. package/Components/Transitioner/Transitioner.tsx +28 -17
  124. package/Components/VideoLive/LiveImageManager.ts +1 -1
  125. package/Components/VideoModal/utils.ts +6 -1
  126. package/Components/ZappFrameworkComponents/BarView/BarView.tsx +12 -5
  127. package/Components/ZappFrameworkComponents/BarView/__tests__/BarView.test.tsx +2 -2
  128. package/Contexts/CachedDimensionsContext/__tests__/index.test.ts +154 -0
  129. package/Contexts/CachedDimensionsContext/index.ts +17 -2
  130. package/Contexts/ModalNavigationContext/__tests__/index.test.tsx +15 -0
  131. package/Contexts/ModalNavigationContext/index.tsx +2 -2
  132. package/Helpers/ComponentCellSelectionHelper/index.js +0 -6
  133. package/Helpers/index.js +0 -39
  134. package/Hooks/useEntryActionsExpander.ts +63 -0
  135. package/package.json +6 -5
  136. package/Components/OfflineHandler/NotificationView/NotificationView.lg.tsx +0 -112
  137. package/Components/OfflineHandler/NotificationView/NotificationView.samsung.tsx +0 -107
  138. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +0 -153
  139. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +0 -66
  140. package/Components/OfflineHandler/NotificationView/utils.ts +0 -34
  141. package/Components/Screen/utils.ts +0 -16
  142. package/Helpers/Analytics/index.js +0 -95
  143. /package/Components/BackgroundImage/{BackgroundImage.tv.android.tsx → BackgroundImage.android.tv.tsx} +0 -0
  144. /package/Components/BackgroundImage/{BackgroundImage.tv.ios.tsx → BackgroundImage.ios.tv.tsx} +0 -0
  145. /package/Components/MasterCell/DefaultComponents/ButtonContainerView/{index.tv.android.tsx → index.android.tv.tsx} +0 -0
  146. /package/Decorators/ZappPipesDataConnector/__tests__/{Hero.js → Hero.tsx} +0 -0
@@ -0,0 +1,56 @@
1
+ import React from "react";
2
+ import { useEntryActionState } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
3
+ import {
4
+ isActionAvailableFor,
5
+ RegisteredAction,
6
+ } from "@applicaster/zapp-react-native-utils/uiActionsRegistrator";
7
+
8
+ import { Button } from "../Button";
9
+
10
+ export type ActionButtonProps = {
11
+ entry: ZappEntry;
12
+ item: RegisteredAction;
13
+ configuration: any;
14
+ width: number;
15
+ component?: ZappUIComponent & { parent?: ZappUIComponent };
16
+ };
17
+
18
+ type Props = ActionButtonProps & {
19
+ onPress?: (item: any) => void;
20
+ };
21
+
22
+ /**
23
+ * Unified action button for the modal bottom sheet.
24
+ *
25
+ * Every item is a `RegisteredAction` resolved from the registry (entry actions,
26
+ * registry actions and legacy context-provider actions alike), so the button
27
+ * simply reads `item.action` and drives it through `useEntryActionState`
28
+ * (backed by the action's observable / `addListener`).
29
+ */
30
+ export function ActionButton(props: Props) {
31
+ const { item, entry, onPress: onItemPress, component } = props;
32
+
33
+ const action = item?.action;
34
+
35
+ const { state, invokeAction } = useEntryActionState(action, entry);
36
+
37
+ const onPress = React.useCallback(
38
+ (_item) => {
39
+ invokeAction({
40
+ context: "toast",
41
+ dismiss: () => onItemPress?.(item),
42
+ ...(component ? { component } : {}),
43
+ });
44
+ },
45
+ [invokeAction, item, onItemPress, component]
46
+ );
47
+
48
+ if (!isActionAvailableFor(action, entry as ZappEntry)) return null;
49
+
50
+ return (
51
+ <Button {...props} item={state} onPress={onPress} label={state?.label} />
52
+ );
53
+ }
54
+
55
+ // Explicitly set display name for the component
56
+ ActionButton.displayName = "ActionButton";
@@ -0,0 +1,38 @@
1
+ import { openBottomSheetModal } from "@applicaster/zapp-react-native-utils/modalState";
2
+ import { boundActionButton } from "../boundActionButton";
3
+ import { openActionsBottomSheet } from "../openActionsBottomSheet";
4
+
5
+ jest.mock("@applicaster/zapp-react-native-utils/modalState", () => ({
6
+ openBottomSheetModal: jest.fn(),
7
+ }));
8
+
9
+ jest.mock("../boundActionButton", () => ({
10
+ boundActionButton: jest.fn(() => jest.fn()),
11
+ }));
12
+
13
+ describe("openActionsBottomSheet", () => {
14
+ beforeEach(() => {
15
+ jest.clearAllMocks();
16
+ });
17
+
18
+ it("binds the List component into the sheet row button", () => {
19
+ const entry = { id: "entry-1" } as ZappEntry;
20
+
21
+ const component = {
22
+ data: { source: "https://example.com/user/collections/abc" },
23
+ };
24
+
25
+ const actions = [
26
+ { identifier: "entry_action_0", action: { invokeAction: jest.fn() } },
27
+ ];
28
+
29
+ openActionsBottomSheet({
30
+ entry,
31
+ actions: actions as any,
32
+ component: component as any,
33
+ });
34
+
35
+ expect(boundActionButton).toHaveBeenCalledWith(entry, component);
36
+ expect(openBottomSheetModal).toHaveBeenCalledTimes(1);
37
+ });
38
+ });
@@ -0,0 +1,24 @@
1
+ import React from "react";
2
+
3
+ import { ActionButton, ActionButtonProps } from "./ActionButton";
4
+
5
+ /**
6
+ * Binds an entry to the sheet's row button.
7
+ *
8
+ * `BottomSheetModalContent` renders `buttonComponent` per item and does not
9
+ * know about entries, so the entry is closed over here instead.
10
+ */
11
+ export const boundActionButton = (
12
+ entry: ZappEntry | ZappFeed,
13
+ component?: ZappUIComponent & { parent?: ZappUIComponent }
14
+ ) => {
15
+ const ActionButtonWithEntry = (props: ActionButtonProps) => (
16
+ <ActionButton {...props} entry={entry as ZappEntry} component={component} />
17
+ );
18
+
19
+ ActionButtonWithEntry.displayName = "ActionButtonWithEntry";
20
+
21
+ return ActionButtonWithEntry;
22
+ };
23
+
24
+ boundActionButton.displayName = "boundActionButton";
@@ -0,0 +1,9 @@
1
+ export { ActionButton } from "./ActionButton";
2
+
3
+ export type { ActionButtonProps } from "./ActionButton";
4
+
5
+ export { boundActionButton } from "./boundActionButton";
6
+
7
+ export { openActionsBottomSheet } from "./openActionsBottomSheet";
8
+
9
+ export type { OpenActionsBottomSheetArgs } from "./openActionsBottomSheet";
@@ -0,0 +1,61 @@
1
+ import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
2
+ import { openBottomSheetModal } from "@applicaster/zapp-react-native-utils/modalState";
3
+ import { noop } from "@applicaster/zapp-react-native-utils/functionUtils";
4
+ import { RegisteredAction } from "@applicaster/zapp-react-native-utils/uiActionsRegistrator";
5
+
6
+ import { boundActionButton } from "./boundActionButton";
7
+
8
+ const { log_warning } = createLogger({
9
+ subsystem: "ActionsBottomSheet",
10
+ category: "Presentation",
11
+ });
12
+
13
+ export type OpenActionsBottomSheetArgs = {
14
+ /** Entry the actions act on; bound into every row button. */
15
+ entry: ZappEntry | ZappFeed;
16
+ /** Already resolved and merged actions, in the order they should appear. */
17
+ actions: RegisteredAction[];
18
+ title?: string;
19
+ summary?: string;
20
+ /** List that opened the sheet; forwarded into row invokeAction context. */
21
+ component?: ZappUIComponent & { parent?: ZappUIComponent };
22
+ };
23
+
24
+ /**
25
+ * Presents a list of resolved actions in the shared bottom sheet.
26
+ *
27
+ * This is the core entry point for "show these actions in a sheet": the
28
+ * open-modal-bottom-sheet plugin uses it for its configured list, and the
29
+ * player overflow uses it for the actions that did not fit on the overlay.
30
+ * Neither has to know about the other, and neither needs the plugin installed.
31
+ */
32
+ export function openActionsBottomSheet({
33
+ entry,
34
+ actions,
35
+ title,
36
+ summary,
37
+ component,
38
+ }: OpenActionsBottomSheetArgs): void {
39
+ if (!actions?.length) {
40
+ // Every action resolved away - unavailable for this entry, or not
41
+ // invokable. An empty sheet looks broken, so say why and stay put.
42
+ log_warning(
43
+ "openActionsBottomSheet: nothing to present - not opening the sheet",
44
+ { entryId: (entry as ZappEntry)?.id }
45
+ );
46
+
47
+ return;
48
+ }
49
+
50
+ openBottomSheetModal({
51
+ modalBottomSheetContentProps: {
52
+ // TODO: should be optional - the button handles its own tap, but
53
+ // BottomSheetModalContent still requires an onPress.
54
+ onPress: noop,
55
+ items: actions,
56
+ buttonComponent: boundActionButton(entry, component),
57
+ title,
58
+ summary,
59
+ },
60
+ });
61
+ }
@@ -0,0 +1,392 @@
1
+ /**
2
+ * Modal Building Block — Button
3
+ *
4
+ * Single-file Expo Snack demo for the Button building block spec.
5
+ * Two variants (with / without leading icon) are driven only by configuration + data.
6
+ *
7
+ * Green (configurable): background, title, icons (size + leading icon enable/action type)
8
+ * Black (hardcoded): action type → icon asset mapping
9
+ */
10
+
11
+ import React from "react";
12
+ import {
13
+ Image,
14
+ Platform,
15
+ Pressable,
16
+ StyleSheet,
17
+ Text,
18
+ View,
19
+ } from "react-native";
20
+ import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
21
+
22
+ const logger = createLogger({ category: "ModalComponent:AudioPlayerButton" });
23
+
24
+ const styles = StyleSheet.create({
25
+ container: {
26
+ alignSelf: "stretch",
27
+ alignItems: "center",
28
+ justifyContent: "center",
29
+ },
30
+ contentRow: {
31
+ flexDirection: "row",
32
+ alignItems: "center",
33
+ justifyContent: "center",
34
+ },
35
+ title: {
36
+ textAlign: "center",
37
+ },
38
+ });
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Configuration (green / editable fields from spec)
42
+ // ---------------------------------------------------------------------------
43
+
44
+ export type ButtonActionType =
45
+ | "addToQueue"
46
+ | "addToPlaylist"
47
+ | "goToPlaylist"
48
+ | "confirm"
49
+ | "cancel";
50
+
51
+ export type TextTransform =
52
+ | "default"
53
+ | "lowercase"
54
+ | "uppercase"
55
+ | "capitalize";
56
+
57
+ export type BackgroundConfiguration = {
58
+ defaultColor: string;
59
+ focusedColor: string;
60
+ inactiveColor: string;
61
+ cornerRadius: number;
62
+ paddingTop: number;
63
+ paddingRight: number;
64
+ paddingBottom: number;
65
+ paddingLeft: number;
66
+ horizontalGutter: number;
67
+ };
68
+
69
+ export type TitleConfiguration = {
70
+ fontColor: string;
71
+ focusedFontColor: string;
72
+ inactiveFontColor: string;
73
+ iosFontFamily: string;
74
+ androidFontFamily: string;
75
+ fontSize: number;
76
+ lineHeight: number;
77
+ iosLetterSpacing: number;
78
+ androidLetterSpacing: number;
79
+ textTransform: TextTransform;
80
+ };
81
+
82
+ export type LeadingIconConfiguration = {
83
+ /** Enable: On / Off */
84
+ enabled: boolean;
85
+ /** Decides which icon will be displayed when enabled */
86
+ actionType?: ButtonActionType;
87
+ /** Optional Studio / modal-blocks asset URI (overrides actionType mapping) */
88
+ asset?: string;
89
+ };
90
+
91
+ export type IconsConfiguration = {
92
+ width: number;
93
+ height: number;
94
+ leadingIcon: LeadingIconConfiguration;
95
+ };
96
+
97
+ export type ButtonConfiguration = {
98
+ background: BackgroundConfiguration;
99
+ title: TitleConfiguration;
100
+ icons: IconsConfiguration;
101
+ };
102
+
103
+ export const DEFAULT_BACKGROUND_CONFIGURATION: BackgroundConfiguration = {
104
+ defaultColor: "#FFFFFF",
105
+ focusedColor: "#E0E0E0",
106
+ inactiveColor: "#999999",
107
+ cornerRadius: 12,
108
+ paddingTop: 12,
109
+ paddingRight: 12,
110
+ paddingBottom: 12,
111
+ paddingLeft: 12,
112
+ horizontalGutter: 6,
113
+ };
114
+
115
+ export const DEFAULT_TITLE_CONFIGURATION: TitleConfiguration = {
116
+ fontColor: "#000000",
117
+ focusedFontColor: "#000000",
118
+ inactiveFontColor: "#000000",
119
+ iosFontFamily: "SFProText-Bold",
120
+ androidFontFamily: "Roboto-Bold",
121
+ fontSize: 15,
122
+ lineHeight: 24,
123
+ iosLetterSpacing: -0.2,
124
+ androidLetterSpacing: 0,
125
+ textTransform: "default",
126
+ };
127
+
128
+ export const DEFAULT_ICONS_CONFIGURATION: IconsConfiguration = {
129
+ width: 24,
130
+ height: 24,
131
+ leadingIcon: {
132
+ enabled: false,
133
+ },
134
+ };
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // Variant configurations
138
+ // ---------------------------------------------------------------------------
139
+
140
+ export const VARIANT_TEXT_ONLY: ButtonConfiguration = {
141
+ background: DEFAULT_BACKGROUND_CONFIGURATION,
142
+ title: DEFAULT_TITLE_CONFIGURATION,
143
+ icons: DEFAULT_ICONS_CONFIGURATION,
144
+ };
145
+
146
+ export const VARIANT_WITH_ICON: ButtonConfiguration = {
147
+ background: DEFAULT_BACKGROUND_CONFIGURATION,
148
+ title: DEFAULT_TITLE_CONFIGURATION,
149
+ icons: {
150
+ ...DEFAULT_ICONS_CONFIGURATION,
151
+ leadingIcon: {
152
+ enabled: true,
153
+ actionType: "addToPlaylist",
154
+ },
155
+ },
156
+ };
157
+
158
+ // ---------------------------------------------------------------------------
159
+ // Data (runtime / localization — not styling config)
160
+ // ---------------------------------------------------------------------------
161
+
162
+ export type ButtonData = {
163
+ title: string;
164
+ disabled?: boolean;
165
+ };
166
+
167
+ export type AudioPlayerButtonProps = {
168
+ configuration?: ButtonConfiguration;
169
+ data?: ButtonData;
170
+ disabled?: boolean;
171
+ focused?: boolean;
172
+ onPress?: () => void;
173
+ };
174
+
175
+ const PLACEHOLDER_RED_BOX =
176
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAJ0lEQVR4nO3NMQEAAAjDsIF/z2ACvlRAU5NMHuvPOQAAAAAAAMBdC/07Ai5sNw+CAAAAAElFTkSuQmCC";
177
+
178
+ function getTextTransformStyle(
179
+ textTransform: TextTransform
180
+ ): "none" | "lowercase" | "uppercase" | "capitalize" {
181
+ return textTransform === "default" ? "none" : textTransform;
182
+ }
183
+
184
+ function shouldShowLeadingIcon(leadingIcon: LeadingIconConfiguration) {
185
+ return (
186
+ leadingIcon.enabled && (!!leadingIcon.actionType || !!leadingIcon.asset)
187
+ );
188
+ }
189
+
190
+ // ---------------------------------------------------------------------------
191
+ // Inner components (hardcoded styling lives here)
192
+ // ---------------------------------------------------------------------------
193
+
194
+ type ButtonContainerProps = {
195
+ focused: boolean;
196
+ disabled: boolean;
197
+ configuration: BackgroundConfiguration;
198
+ children: React.ReactNode;
199
+ onPress?: () => void;
200
+ };
201
+
202
+ function ButtonContainer({
203
+ focused,
204
+ disabled,
205
+ configuration,
206
+ children,
207
+ onPress,
208
+ }: ButtonContainerProps) {
209
+ const defaultBgColor = disabled
210
+ ? configuration.inactiveColor
211
+ : focused
212
+ ? configuration.focusedColor
213
+ : configuration.defaultColor;
214
+
215
+ return (
216
+ <Pressable
217
+ onPress={disabled ? undefined : onPress}
218
+ style={({ pressed }) => [
219
+ styles.container,
220
+ {
221
+ backgroundColor:
222
+ pressed && !disabled ? configuration.focusedColor : defaultBgColor,
223
+ borderRadius: configuration.cornerRadius,
224
+ paddingTop: configuration.paddingTop,
225
+ paddingRight: configuration.paddingRight,
226
+ paddingBottom: configuration.paddingBottom,
227
+ paddingLeft: configuration.paddingLeft,
228
+ },
229
+ ]}
230
+ >
231
+ {children}
232
+ </Pressable>
233
+ );
234
+ }
235
+
236
+ type ButtonTitleProps = {
237
+ title: string;
238
+ focused: boolean;
239
+ disabled: boolean;
240
+ configuration: TitleConfiguration;
241
+ };
242
+
243
+ function ButtonTitle({
244
+ title,
245
+ focused,
246
+ disabled,
247
+ configuration,
248
+ }: ButtonTitleProps) {
249
+ const color = disabled
250
+ ? configuration.inactiveFontColor
251
+ : focused
252
+ ? configuration.focusedFontColor
253
+ : configuration.fontColor;
254
+
255
+ const letterSpacing =
256
+ Platform.OS === "ios"
257
+ ? configuration.iosLetterSpacing
258
+ : configuration.androidLetterSpacing;
259
+
260
+ return (
261
+ <Text
262
+ style={[
263
+ styles.title,
264
+ {
265
+ color,
266
+ fontSize: configuration.fontSize,
267
+ lineHeight: configuration.lineHeight,
268
+ letterSpacing,
269
+ fontFamily: Platform.select({
270
+ ios: configuration.iosFontFamily,
271
+ android: configuration.androidFontFamily,
272
+ default: configuration.androidFontFamily,
273
+ }),
274
+ textTransform: getTextTransformStyle(configuration.textTransform),
275
+ },
276
+ ]}
277
+ numberOfLines={1}
278
+ >
279
+ {title}
280
+ </Text>
281
+ );
282
+ }
283
+
284
+ type ButtonLeadingIconProps = {
285
+ actionType?: ButtonActionType;
286
+ configuration: IconsConfiguration;
287
+ };
288
+
289
+ function ButtonLeadingIcon({ configuration }: ButtonLeadingIconProps) {
290
+ const asset = configuration.leadingIcon.asset || PLACEHOLDER_RED_BOX;
291
+
292
+ React.useEffect(() => {
293
+ if (!configuration.leadingIcon.asset) {
294
+ logger.log_warning(
295
+ "AudioPlayerButton has leadingIcon enabled but no asset was provided. Falling back to placeholder red box. Ensure the relevant plugin (e.g. playlist-experience) provides leadingIcon.asset in styleOverrides.",
296
+ {
297
+ leadingIcon: configuration.leadingIcon,
298
+ }
299
+ );
300
+ }
301
+ }, [configuration.leadingIcon.asset]);
302
+
303
+ if (!asset) {
304
+ return null;
305
+ }
306
+
307
+ return (
308
+ <Image
309
+ source={{ uri: asset }}
310
+ style={{
311
+ width: configuration.width,
312
+ height: configuration.height,
313
+ }}
314
+ resizeMode="contain"
315
+ />
316
+ );
317
+ }
318
+
319
+ // ---------------------------------------------------------------------------
320
+ // AudioPlayerButton — single building block driven by configuration + data
321
+ // ---------------------------------------------------------------------------
322
+
323
+ import { useModalBlocksStyle } from "../../ModalBlocksStyleContext";
324
+ import { mergeButtonConfiguration } from "../utils/mergeStyles";
325
+
326
+ export function AudioPlayerButton({
327
+ configuration = VARIANT_WITH_ICON,
328
+ data = { title: undefined },
329
+ disabled,
330
+ focused = false,
331
+ onPress,
332
+ }: AudioPlayerButtonProps) {
333
+ const modalBlocksStyle = useModalBlocksStyle();
334
+
335
+ const buttonBlock = modalBlocksStyle?.button;
336
+ const actionBlock = modalBlocksStyle?.action;
337
+
338
+ const effectiveConfig = React.useMemo(() => {
339
+ return mergeButtonConfiguration(configuration, buttonBlock, actionBlock);
340
+ }, [configuration, buttonBlock, actionBlock]);
341
+
342
+ const {
343
+ background: effectiveBgConfig,
344
+ title: effectiveTitleConfig,
345
+ icons: iconsConfiguration,
346
+ } = effectiveConfig;
347
+
348
+ const title = data?.title || buttonBlock?.title.default;
349
+ const isDisabled = disabled ?? data.disabled ?? false;
350
+
351
+ React.useEffect(() => {
352
+ if (!title) {
353
+ logger.log_warning(
354
+ "Create New Playlist button rendering with empty title. Ensure data.title is set for AudioPlayerButton or styleOverrides providing button title.",
355
+ {
356
+ leadingIcon: iconsConfiguration.leadingIcon,
357
+ modalBlocksStyle: modalBlocksStyle,
358
+ }
359
+ );
360
+ }
361
+ }, [title]);
362
+
363
+ const { leadingIcon } = iconsConfiguration;
364
+ const showIcon = shouldShowLeadingIcon(leadingIcon);
365
+
366
+ return (
367
+ <ButtonContainer
368
+ focused={focused}
369
+ disabled={isDisabled}
370
+ configuration={effectiveBgConfig}
371
+ onPress={onPress}
372
+ >
373
+ <View style={styles.contentRow}>
374
+ {showIcon ? (
375
+ <>
376
+ <ButtonLeadingIcon
377
+ actionType={leadingIcon.actionType}
378
+ configuration={iconsConfiguration}
379
+ />
380
+ <View style={{ width: effectiveBgConfig.horizontalGutter }} />
381
+ </>
382
+ ) : null}
383
+ <ButtonTitle
384
+ title={title}
385
+ focused={focused}
386
+ disabled={isDisabled}
387
+ configuration={effectiveTitleConfig}
388
+ />
389
+ </View>
390
+ </ButtonContainer>
391
+ );
392
+ }