@applicaster/zapp-react-native-ui-components 15.1.13 → 15.1.15-alpha.4236143002

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 (40) hide show
  1. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +4 -1
  2. package/Components/ModalComponent/ActionsBottomSheet/ActionButton.tsx +56 -0
  3. package/Components/ModalComponent/ActionsBottomSheet/__tests__/openActionsBottomSheet.test.ts +38 -0
  4. package/Components/ModalComponent/ActionsBottomSheet/boundActionButton.tsx +24 -0
  5. package/Components/ModalComponent/ActionsBottomSheet/index.ts +9 -0
  6. package/Components/ModalComponent/ActionsBottomSheet/openActionsBottomSheet.ts +61 -0
  7. package/Components/ModalComponent/AudioPlayer/Components/Action.tsx +33 -87
  8. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +57 -74
  9. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +142 -107
  10. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +653 -0
  11. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +427 -159
  12. package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +89 -0
  13. package/Components/ModalComponent/AudioPlayer/Components/__tests__/Item.test.tsx +66 -0
  14. package/Components/ModalComponent/AudioPlayer/Components/index.ts +8 -4
  15. package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +230 -0
  16. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +332 -0
  17. package/Components/ModalComponent/BottomSheetModalContent.tsx +379 -61
  18. package/Components/ModalComponent/Button/index.tsx +5 -3
  19. package/Components/ModalComponent/Header/index.tsx +20 -7
  20. package/Components/ModalComponent/Header/utils.ts +1 -1
  21. package/Components/ModalComponent/ModalBlocksStyleContext.tsx +10 -0
  22. package/Components/ModalComponent/SortableList.tsx +142 -0
  23. package/Components/ModalComponent/SortableList.web.tsx +102 -0
  24. package/Components/ModalComponent/TextInputContent.tsx +112 -0
  25. package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +238 -0
  26. package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +156 -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 +90 -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 +82 -21
  37. package/Components/VideoLive/LiveImageManager.ts +8 -2
  38. package/Hooks/useEntryActionsExpander.ts +63 -0
  39. package/package.json +6 -5
  40. /package/Decorators/ZappPipesDataConnector/__tests__/{Hero.js → Hero.tsx} +0 -0
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect } from "react";
2
2
  import { TouchableOpacity, ViewStyle } from "react-native";
3
3
 
4
4
  import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
5
+ import { useUIComponentContext } from "@applicaster/zapp-react-native-ui-components/Contexts/UIComponentContext";
5
6
 
6
7
  import Image from "./Image";
7
8
  type Props = {
@@ -54,6 +55,7 @@ export const ActionButton = React.memo(function ActionButtonComponent(
54
55
  ) {
55
56
  const { item, action, asset, flavour = "flavour_1", cellUUID } = props;
56
57
  const actionContext = useActions(action?.identifier);
58
+ const component = useUIComponentContext();
57
59
 
58
60
  // TODO: add subscription API for action availability
59
61
  const actionDisabled =
@@ -78,8 +80,9 @@ export const ActionButton = React.memo(function ActionButtonComponent(
78
80
  updateState: (state) => {
79
81
  setActionState(state);
80
82
  },
83
+ ...(component ? { component } : {}),
81
84
  });
82
- }, [actionState, actionContext?.state, item?.id]);
85
+ }, [actionState, actionContext?.state, item, component]);
83
86
 
84
87
  useEffect(() => {
85
88
  if (typeof actionContext?.addListener === "function") {
@@ -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
+ }
@@ -11,31 +11,20 @@
11
11
  */
12
12
 
13
13
  import React from "react";
14
- import { Image, Platform, StyleSheet, Text, View } from "react-native";
14
+ import {
15
+ Image,
16
+ Platform,
17
+ Pressable,
18
+ StyleSheet,
19
+ Text,
20
+ View,
21
+ } from "react-native";
15
22
 
16
23
  const styles = StyleSheet.create({
17
24
  row: {
18
25
  flexDirection: "row",
19
26
  alignItems: "center",
20
27
  },
21
- demoScreen: {
22
- flex: 1,
23
- backgroundColor: "#1C1C1E",
24
- paddingTop: 64,
25
- paddingHorizontal: 16,
26
- gap: 12,
27
- },
28
- demoHeading: {
29
- color: "#FFFFFF",
30
- fontSize: 20,
31
- fontWeight: "600",
32
- marginBottom: 8,
33
- },
34
- demoLabel: {
35
- color: "#999999",
36
- fontSize: 13,
37
- marginTop: 8,
38
- },
39
28
  });
40
29
 
41
30
  // ---------------------------------------------------------------------------
@@ -123,10 +112,11 @@ export type ActionData = {
123
112
  showTrailingChevron?: boolean;
124
113
  };
125
114
 
126
- export type ItemProps = {
115
+ export type AudioPlayerActionProps = {
127
116
  configuration: ActionConfiguration;
128
117
  data: ActionData;
129
118
  focused?: boolean;
119
+ onPress?: () => void;
130
120
  };
131
121
 
132
122
  // ---------------------------------------------------------------------------
@@ -187,28 +177,32 @@ type ActionContainerProps = {
187
177
  configuration: BackgroundConfiguration;
188
178
  focused: boolean;
189
179
  children: React.ReactNode;
180
+ onPress?: () => void;
190
181
  };
191
182
 
192
183
  function ActionContainer({
193
184
  configuration,
194
185
  focused,
195
186
  children,
187
+ onPress,
196
188
  }: ActionContainerProps) {
197
189
  return (
198
- <View
199
- style={{
200
- backgroundColor: focused
201
- ? configuration.focusedColor
202
- : configuration.defaultColor,
190
+ <Pressable
191
+ onPress={onPress}
192
+ style={({ pressed }) => ({
193
+ backgroundColor:
194
+ pressed || focused
195
+ ? configuration.focusedColor
196
+ : configuration.defaultColor,
203
197
  borderRadius: configuration.cornerRadius,
204
198
  paddingTop: CONTAINER_SPEC.paddingTop,
205
199
  paddingRight: CONTAINER_SPEC.paddingRight,
206
200
  paddingBottom: CONTAINER_SPEC.paddingBottom,
207
201
  paddingLeft: CONTAINER_SPEC.paddingLeft,
208
- }}
202
+ })}
209
203
  >
210
204
  {children}
211
- </View>
205
+ </Pressable>
212
206
  );
213
207
  }
214
208
 
@@ -279,10 +273,15 @@ function ActionTrailingIcon({ configuration }: ActionTrailingIconProps) {
279
273
  }
280
274
 
281
275
  // ---------------------------------------------------------------------------
282
- // Item
276
+ // AudioPlayerAction
283
277
  // ---------------------------------------------------------------------------
284
278
 
285
- export function Item({ configuration, data, focused = false }: ItemProps) {
279
+ export function AudioPlayerAction({
280
+ configuration,
281
+ data,
282
+ focused = false,
283
+ onPress,
284
+ }: AudioPlayerActionProps) {
286
285
  const { background, title, trailingIcon } = configuration;
287
286
  const { title: label, leadingActionType, showTrailingChevron } = data;
288
287
 
@@ -290,7 +289,11 @@ export function Item({ configuration, data, focused = false }: ItemProps) {
290
289
  const showTrailing = shouldShowTrailingIcon(showTrailingChevron);
291
290
 
292
291
  return (
293
- <ActionContainer configuration={background} focused={focused}>
292
+ <ActionContainer
293
+ configuration={background}
294
+ focused={focused}
295
+ onPress={onPress}
296
+ >
294
297
  <View style={styles.row}>
295
298
  {showLeading ? (
296
299
  <>
@@ -309,60 +312,3 @@ export function Item({ configuration, data, focused = false }: ItemProps) {
309
312
  </ActionContainer>
310
313
  );
311
314
  }
312
-
313
- // ---------------------------------------------------------------------------
314
- // Demo
315
- // ---------------------------------------------------------------------------
316
-
317
- export default function App() {
318
- return (
319
- <View style={styles.demoScreen}>
320
- <Text style={styles.demoHeading}>Modal Action Building Block</Text>
321
-
322
- <Text style={styles.demoLabel}>
323
- Leading icon + text — Add To Queue (focused)
324
- </Text>
325
- <Item
326
- configuration={DEFAULT_ACTION_CONFIGURATION}
327
- data={{ title: "Add To Queue", leadingActionType: "addToQueue" }}
328
- focused
329
- />
330
-
331
- <Text style={styles.demoLabel}>
332
- Leading icon + text + trailing chevron — Add To Playlist
333
- </Text>
334
- <Item
335
- configuration={DEFAULT_ACTION_CONFIGURATION}
336
- data={{
337
- title: "Add To Playlist",
338
- leadingActionType: "addToPlaylist",
339
- showTrailingChevron: true,
340
- }}
341
- />
342
-
343
- <Text style={styles.demoLabel}>Text only — Go To Playlist</Text>
344
- <Item
345
- configuration={DEFAULT_ACTION_CONFIGURATION}
346
- data={{ title: "Go To Playlist" }}
347
- />
348
-
349
- <Text style={styles.demoLabel}>Leading icon + text — Favorite</Text>
350
- <Item
351
- configuration={DEFAULT_ACTION_CONFIGURATION}
352
- data={{ title: "Favorite", leadingActionType: "favorite" }}
353
- />
354
-
355
- <Text style={styles.demoLabel}>Leading icon + text — Share</Text>
356
- <Item
357
- configuration={DEFAULT_ACTION_CONFIGURATION}
358
- data={{ title: "Share", leadingActionType: "share" }}
359
- />
360
-
361
- <Text style={styles.demoLabel}>Text only — Go To Artist</Text>
362
- <Item
363
- configuration={DEFAULT_ACTION_CONFIGURATION}
364
- data={{ title: "Go To Artist" }}
365
- />
366
- </View>
367
- );
368
- }
@@ -9,7 +9,14 @@
9
9
  */
10
10
 
11
11
  import React from "react";
12
- import { Image, Platform, StyleSheet, Text, View } from "react-native";
12
+ import {
13
+ Image,
14
+ Platform,
15
+ Pressable,
16
+ StyleSheet,
17
+ Text,
18
+ View,
19
+ } from "react-native";
13
20
 
14
21
  const styles = StyleSheet.create({
15
22
  container: {
@@ -25,24 +32,6 @@ const styles = StyleSheet.create({
25
32
  title: {
26
33
  textAlign: "center",
27
34
  },
28
- demoScreen: {
29
- flex: 1,
30
- backgroundColor: "#1C1C1E",
31
- paddingTop: 64,
32
- paddingHorizontal: 24,
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
35
  });
47
36
 
48
37
  // ---------------------------------------------------------------------------
@@ -92,6 +81,8 @@ export type LeadingIconConfiguration = {
92
81
  enabled: boolean;
93
82
  /** Decides which icon will be displayed when enabled */
94
83
  actionType?: ButtonActionType;
84
+ /** Optional Studio / modal-blocks asset URI (overrides actionType mapping) */
85
+ asset?: string;
95
86
  };
96
87
 
97
88
  export type IconsConfiguration = {
@@ -148,11 +139,12 @@ export type ButtonData = {
148
139
  disabled?: boolean;
149
140
  };
150
141
 
151
- export type ItemProps = {
142
+ export type AudioPlayerButtonProps = {
152
143
  configuration: ButtonConfiguration;
153
144
  data: ButtonData;
154
145
  disabled?: boolean;
155
146
  focused?: boolean;
147
+ onPress?: () => void;
156
148
  };
157
149
 
158
150
  // ---------------------------------------------------------------------------
@@ -170,7 +162,7 @@ const ACTION_ICON_ASSETS: Record<ButtonActionType, string> = {
170
162
  confirm:
171
163
  "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAJElEQVR42mNgGAWjYBSQCv4TwKMWDHELho/hNE/3o2AUDDcAAPv0QMAyy1CtAAAAAElFTkSuQmCC",
172
164
  cancel:
173
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAASElEQVR42mNgGAUjGvynkVoUTf+pqA6nxv8UqiHbEooNx2cQ1QzHZiDVDcdlCcOQsoCmQUTTSKZpMqV5RqNLUUHTwm4UDBMAALBLQ73LTvoyAAAAAElFTkSuQmCC",
165
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAASElEQVR42mNgGAWjGvynkVoUTf+pqA6nxv8UqiHbEooNx2cQ1QzHZiDVDcdlCcOQsoCmQUTTSKZpMqV5RqNLUUHTwm4UDBMAALBLQ73LTvoyAAAAAElFTkSuQmCC",
174
166
  };
175
167
 
176
168
  function getTextTransformStyle(
@@ -180,7 +172,9 @@ function getTextTransformStyle(
180
172
  }
181
173
 
182
174
  function shouldShowLeadingIcon(leadingIcon: LeadingIconConfiguration) {
183
- return leadingIcon.enabled && !!leadingIcon.actionType;
175
+ return (
176
+ leadingIcon.enabled && (!!leadingIcon.actionType || !!leadingIcon.asset)
177
+ );
184
178
  }
185
179
 
186
180
  // ---------------------------------------------------------------------------
@@ -192,6 +186,7 @@ type ButtonContainerProps = {
192
186
  disabled: boolean;
193
187
  configuration: BackgroundConfiguration;
194
188
  children: React.ReactNode;
189
+ onPress?: () => void;
195
190
  };
196
191
 
197
192
  function ButtonContainer({
@@ -199,19 +194,22 @@ function ButtonContainer({
199
194
  disabled,
200
195
  configuration,
201
196
  children,
197
+ onPress,
202
198
  }: ButtonContainerProps) {
203
- const backgroundColor = disabled
199
+ const defaultBgColor = disabled
204
200
  ? configuration.inactiveColor
205
201
  : focused
206
202
  ? configuration.focusedColor
207
203
  : configuration.defaultColor;
208
204
 
209
205
  return (
210
- <View
211
- style={[
206
+ <Pressable
207
+ onPress={disabled ? undefined : onPress}
208
+ style={({ pressed }) => [
212
209
  styles.container,
213
210
  {
214
- backgroundColor,
211
+ backgroundColor:
212
+ pressed && !disabled ? configuration.focusedColor : defaultBgColor,
215
213
  borderRadius: configuration.cornerRadius,
216
214
  paddingTop: configuration.paddingTop,
217
215
  paddingRight: configuration.paddingRight,
@@ -221,7 +219,7 @@ function ButtonContainer({
221
219
  ]}
222
220
  >
223
221
  {children}
224
- </View>
222
+ </Pressable>
225
223
  );
226
224
  }
227
225
 
@@ -274,7 +272,7 @@ function ButtonTitle({
274
272
  }
275
273
 
276
274
  type ButtonLeadingIconProps = {
277
- actionType: ButtonActionType;
275
+ actionType?: ButtonActionType;
278
276
  configuration: IconsConfiguration;
279
277
  };
280
278
 
@@ -282,9 +280,17 @@ function ButtonLeadingIcon({
282
280
  actionType,
283
281
  configuration,
284
282
  }: ButtonLeadingIconProps) {
283
+ const asset =
284
+ configuration.leadingIcon.asset ||
285
+ (actionType ? ACTION_ICON_ASSETS[actionType] : undefined);
286
+
287
+ if (!asset) {
288
+ return null;
289
+ }
290
+
285
291
  return (
286
292
  <Image
287
- source={{ uri: ACTION_ICON_ASSETS[actionType] }}
293
+ source={{ uri: asset }}
288
294
  style={{
289
295
  width: configuration.width,
290
296
  height: configuration.height,
@@ -295,20 +301,32 @@ function ButtonLeadingIcon({
295
301
  }
296
302
 
297
303
  // ---------------------------------------------------------------------------
298
- // Item — single building block driven by configuration + data
304
+ // AudioPlayerButton — single building block driven by configuration + data
299
305
  // ---------------------------------------------------------------------------
300
306
 
301
- export function Item({
307
+ import { useModalBlocksStyle } from "../../ModalBlocksStyleContext";
308
+ import { mergeButtonConfiguration } from "../utils/mergeStyles";
309
+
310
+ export function AudioPlayerButton({
302
311
  configuration,
303
312
  data,
304
313
  disabled,
305
314
  focused = false,
306
- }: ItemProps) {
315
+ onPress,
316
+ }: AudioPlayerButtonProps) {
317
+ const modalBlocksStyle = useModalBlocksStyle();
318
+ const buttonBlock = modalBlocksStyle?.button;
319
+ const actionBlock = modalBlocksStyle?.action;
320
+
321
+ const effectiveConfig = React.useMemo(() => {
322
+ return mergeButtonConfiguration(configuration, buttonBlock, actionBlock);
323
+ }, [configuration, buttonBlock, actionBlock]);
324
+
307
325
  const {
308
- background: backgroundConfiguration,
309
- title: titleConfiguration,
326
+ background: effectiveBgConfig,
327
+ title: effectiveTitleConfig,
310
328
  icons: iconsConfiguration,
311
- } = configuration;
329
+ } = effectiveConfig;
312
330
 
313
331
  const { title } = data;
314
332
  const isDisabled = disabled ?? data.disabled ?? false;
@@ -320,7 +338,8 @@ export function Item({
320
338
  <ButtonContainer
321
339
  focused={focused}
322
340
  disabled={isDisabled}
323
- configuration={backgroundConfiguration}
341
+ configuration={effectiveBgConfig}
342
+ onPress={onPress}
324
343
  >
325
344
  <View style={styles.contentRow}>
326
345
  {showIcon ? (
@@ -329,14 +348,14 @@ export function Item({
329
348
  actionType={leadingIcon.actionType}
330
349
  configuration={iconsConfiguration}
331
350
  />
332
- <View style={{ width: backgroundConfiguration.horizontalGutter }} />
351
+ <View style={{ width: effectiveBgConfig.horizontalGutter }} />
333
352
  </>
334
353
  ) : null}
335
354
  <ButtonTitle
336
355
  title={title}
337
356
  focused={focused}
338
357
  disabled={isDisabled}
339
- configuration={titleConfiguration}
358
+ configuration={effectiveTitleConfig}
340
359
  />
341
360
  </View>
342
361
  </ButtonContainer>
@@ -386,39 +405,3 @@ export const VARIANT_LARGE_ICON: ButtonConfiguration = {
386
405
  },
387
406
  },
388
407
  };
389
-
390
- export default function App() {
391
- return (
392
- <View style={styles.demoScreen}>
393
- <Text style={styles.demoHeading}>Modal Button Building Block</Text>
394
-
395
- <Text style={styles.demoLabel}>Variant 1 — Text only (confirmation)</Text>
396
- <Item configuration={VARIANT_TEXT_ONLY} data={{ title: "Confirm" }} />
397
-
398
- <Text style={styles.demoLabel}>Variant 2 — Leading icon + text</Text>
399
- <Item
400
- configuration={VARIANT_WITH_ICON}
401
- data={{ title: "Add To Playlist" }}
402
- />
403
-
404
- <Text style={styles.demoLabel}>Inactive state</Text>
405
- <Item
406
- configuration={VARIANT_TEXT_ONLY}
407
- data={{ title: "Unavailable" }}
408
- disabled
409
- />
410
-
411
- <Text style={styles.demoLabel}>Custom title config — uppercase</Text>
412
- <Item
413
- configuration={VARIANT_UPPERCASE_TITLE}
414
- data={{ title: "Save Changes" }}
415
- />
416
-
417
- <Text style={styles.demoLabel}>Custom icons config — 32×32</Text>
418
- <Item
419
- configuration={VARIANT_LARGE_ICON}
420
- data={{ title: "Add To Queue" }}
421
- />
422
- </View>
423
- );
424
- }