@applicaster/zapp-react-native-ui-components 16.0.0-alpha.3112220329 → 16.0.0-alpha.3202041045

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 (76) hide show
  1. package/Components/LinkHandler/LinkHandler.tsx +2 -7
  2. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +17 -1
  3. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/ActionButtonController.tsx +19 -4
  4. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/__tests__/ActionButtonController.test.tsx +43 -1
  5. package/Components/MasterCell/DefaultComponents/LiveImage/executePreloadHooks.ts +2 -4
  6. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +14 -6
  7. package/Components/MasterCell/DefaultComponents/PressableView.tsx +3 -0
  8. package/Components/MasterCell/DefaultComponents/__tests__/ActionButton.test.tsx +288 -0
  9. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +2 -1
  10. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +4 -2
  11. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/__tests__/factory.test.ts +32 -0
  12. package/Components/MasterCell/__tests__/masterCell.test.js +60 -0
  13. package/Components/MasterCell/index.tsx +28 -4
  14. package/Components/MasterCell/utils/__tests__/withoutFocusedState.test.js +20 -0
  15. package/Components/MasterCell/utils/index.ts +18 -0
  16. package/Components/ModalComponent/ActionsBottomSheet/ActionButton.tsx +4 -2
  17. package/Components/ModalComponent/ActionsBottomSheet/__tests__/openActionsBottomSheet.test.ts +38 -0
  18. package/Components/ModalComponent/ActionsBottomSheet/boundActionButton.tsx +5 -2
  19. package/Components/ModalComponent/ActionsBottomSheet/openActionsBottomSheet.ts +4 -1
  20. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +58 -73
  21. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +65 -56
  22. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +140 -65
  23. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +115 -47
  24. package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +148 -32
  25. package/Components/ModalComponent/AudioPlayer/Components/__tests__/Item.test.tsx +164 -0
  26. package/Components/ModalComponent/AudioPlayer/Components/__tests__/NowPlayingHeaderSection.test.tsx +36 -0
  27. package/Components/ModalComponent/AudioPlayer/Components/index.ts +0 -2
  28. package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +209 -6
  29. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +250 -19
  30. package/Components/ModalComponent/BottomSheetModalContent.tsx +126 -77
  31. package/Components/ModalComponent/SortableList.tsx +36 -22
  32. package/Components/ModalComponent/TextInputContent.tsx +42 -7
  33. package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +111 -6
  34. package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +50 -1
  35. package/Components/ModalComponent/__tests__/showTextInput.test.ts +25 -0
  36. package/Components/ModalComponent/index.tsx +7 -9
  37. package/Components/ModalComponent/presets/__tests__/getCell.test.ts +24 -0
  38. package/Components/ModalComponent/presets/selectableCell.tsx +1 -1
  39. package/Components/ModalComponent/showTextInput.ts +13 -2
  40. package/Components/OfflineHandler/__tests__/index.test.tsx +109 -0
  41. package/Components/OfflineHandler/index.tsx +42 -4
  42. package/Components/PlayerContainer/PlayerContainer.tsx +30 -6
  43. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +51 -0
  44. package/Components/River/ComponentsMap/ComponentsMap.tsx +14 -13
  45. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +140 -75
  46. package/Components/River/__tests__/componentsMap.test.js +8 -3
  47. package/Components/Screen/__tests__/Screen.test.tsx +24 -0
  48. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +2 -0
  49. package/Components/Screen/index.tsx +5 -0
  50. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +1 -1
  51. package/Components/Transitioner/AnimationManager.js +1 -1
  52. package/Components/Transitioner/Scene.tsx +4 -3
  53. package/Components/Transitioner/Transitioner.tsx +71 -33
  54. package/Components/Transitioner/__tests__/transitioner.test.js +372 -3
  55. package/Components/VideoLive/LiveImageManager.ts +134 -14
  56. package/Components/VideoLive/MuteUnmuteController.ts +135 -0
  57. package/Components/VideoLive/PlayerLiveImageComponent.tsx +15 -19
  58. package/Components/VideoLive/__tests__/LiveImageManager.test.ts +59 -0
  59. package/Components/VideoLive/__tests__/LiveImageMuteState.test.ts +311 -0
  60. package/Components/VideoLive/__tests__/MuteUnmuteController.test.ts +135 -0
  61. package/Components/VideoLive/__tests__/PlayerLiveImageComponent.test.tsx +63 -1
  62. package/Components/VideoModal/PlayerWrapper.tsx +4 -1
  63. package/Components/VideoModal/__tests__/PlayerWrapper.test.tsx +1 -0
  64. package/Components/VideoModal/__tests__/playerWrapperStyle.test.ts +58 -0
  65. package/Components/VideoModal/__tests__/playerWrapperUtils.test.ts +16 -0
  66. package/Components/VideoModal/playerWrapperStyle.ts +21 -3
  67. package/Components/VideoModal/playerWrapperUtils.ts +7 -1
  68. package/Contexts/ModalNavigationContext/__tests__/index.test.tsx +15 -0
  69. package/Contexts/ModalNavigationContext/index.tsx +2 -2
  70. package/Decorators/ZappPipesDataConnector/__tests__/screenToolbox.test.ts +347 -0
  71. package/Decorators/ZappPipesDataConnector/__tests__/screenToolboxUnavailable.test.ts +53 -0
  72. package/Decorators/ZappPipesDataConnector/index.tsx +19 -4
  73. package/Decorators/ZappPipesDataConnector/screenToolbox.ts +384 -0
  74. package/assets/empty-Info.png +0 -0
  75. package/package.json +5 -5
  76. package/Components/ModalComponent/AudioPlayer/Components/Action.tsx +0 -314
@@ -76,6 +76,66 @@ describe("masterCellBuilder", () => {
76
76
  });
77
77
  });
78
78
 
79
+ describe("cellOptions.interactive", () => {
80
+ // renders a single View tagged with the state the cell was built with
81
+ const dataAdapter =
82
+ () =>
83
+ ({ state }) => [
84
+ { type: "View", style: {}, props: { testID: `state-${state}` } },
85
+ ];
86
+
87
+ // Only the root View gets this testID: cell elements pass their own testID, which overrides it
88
+ const ROOT_TEST_ID = "master-cell-root";
89
+
90
+ // forwardRef: MasterCell attaches a ref to its root View
91
+ const TestView = React.forwardRef((props, ref) => (
92
+ <View testID={ROOT_TEST_ID} {...props} ref={ref} />
93
+ ));
94
+
95
+ const renderCell = (cellOptions, state) => {
96
+ const MasterCell = masterCellBuilder({
97
+ cellConfiguration: singleView,
98
+ dataAdapter,
99
+ components: { View: TestView },
100
+ cellOptions,
101
+ });
102
+
103
+ return renderWithProviders(<MasterCell item={entry} state={state} />);
104
+ };
105
+
106
+ const getRoot = (wrapper) => wrapper.getByTestId(ROOT_TEST_ID);
107
+
108
+ it("keeps focused state and default root props when omitted", () => {
109
+ const wrapper = renderCell(undefined, "focused");
110
+ const root = getRoot(wrapper);
111
+
112
+ expect(wrapper.getByTestId("state-focused")).toBeDefined();
113
+ expect(root.props.onStartShouldSetResponder).toBeUndefined();
114
+ expect(root.props.collapsable).toBeUndefined();
115
+ });
116
+
117
+ it("claims body touches on a non-collapsable root when false", () => {
118
+ const wrapper = renderCell({ interactive: false }, "default");
119
+ const root = getRoot(wrapper);
120
+
121
+ expect(root.props.onStartShouldSetResponder()).toBe(true);
122
+ expect(root.props.collapsable).toBe(false);
123
+ });
124
+
125
+ it("renders focused as default when false", () => {
126
+ const wrapper = renderCell({ interactive: false }, "focused");
127
+
128
+ expect(wrapper.getByTestId("state-default")).toBeDefined();
129
+ expect(wrapper.queryByTestId("state-focused")).toBeNull();
130
+ });
131
+
132
+ it("renders focused_selected as selected when false", () => {
133
+ const wrapper = renderCell({ interactive: false }, "focused_selected");
134
+
135
+ expect(wrapper.getByTestId("state-selected")).toBeDefined();
136
+ });
137
+ });
138
+
79
139
  describe.skip("when using custom default components", () => {
80
140
  // the collapsable prop is added here to easily check from the snapshot that the mock
81
141
  // component is being used instead of the default one
@@ -10,7 +10,11 @@ import { useScreenState } from "@applicaster/zapp-react-native-utils/screenState
10
10
  import { defaultComponents } from "./DefaultComponents";
11
11
  import { defaultDataAdapter } from "./dataAdapter";
12
12
  import { elementMapper, mapElementWithKey } from "./elementMapper";
13
- import { getEntryState, hasFocusableInsideBuilder } from "./utils";
13
+ import {
14
+ getEntryState,
15
+ hasFocusableInsideBuilder,
16
+ withoutFocusedState,
17
+ } from "./utils";
14
18
 
15
19
  type Props = {
16
20
  item: ZappEntry;
@@ -19,6 +23,16 @@ type Props = {
19
23
 
20
24
  const defaultContainerStyle = { flex: 1 };
21
25
 
26
+ /**
27
+ * Root props of a non-interactive cell: the root view claims touches on the cell body,
28
+ * so the host list's touchable never fires (buttons, being deeper, still receive theirs).
29
+ * `collapsable: false` keeps Android / Fabric from flattening the view away.
30
+ */
31
+ const nonInteractiveRootProps = {
32
+ onStartShouldSetResponder: () => true,
33
+ collapsable: false,
34
+ };
35
+
22
36
  /**
23
37
  * Constructs a MasterCell with the provided options
24
38
  * @param {Object} options
@@ -27,6 +41,8 @@ const defaultContainerStyle = { flex: 1 };
27
41
  * @param {Function} options.dataAdapter optional dataAdapter to use instead of the default one.
28
42
  * See dataAdapter.js for more info
29
43
  * @param {containerStyles} options.containerStyle style to apply at the top level of the master cell
44
+ * @param {Object} options.cellOptions optional cell options. `interactive: false` makes the cell body
45
+ * ignore taps and render without focused / pressed states (inner buttons keep working). Defaults to `true`.
30
46
  * @returns {Function} Functional React Component to render the masterCell with the provided options
31
47
  */
32
48
 
@@ -39,6 +55,7 @@ type MasterCellBuilderOptions = {
39
55
  isRTL?: boolean;
40
56
  skipButtons?: boolean;
41
57
  component?: ZappComponent;
58
+ interactive?: boolean;
42
59
  };
43
60
  };
44
61
 
@@ -49,7 +66,7 @@ export function masterCellBuilder({
49
66
  containerStyle = defaultContainerStyle,
50
67
  cellOptions = {},
51
68
  }: MasterCellBuilderOptions) {
52
- const { skipButtons = false, component } = cellOptions;
69
+ const { skipButtons = false, component, interactive = true } = cellOptions;
53
70
 
54
71
  const componentsMap = R.merge(defaultComponents, components);
55
72
  const { View } = componentsMap;
@@ -88,7 +105,10 @@ export function masterCellBuilder({
88
105
  () =>
89
106
  elementsBuilder({
90
107
  entry: item,
91
- state: getEntryState(state, entryIsSelected),
108
+ state: getEntryState(
109
+ interactive ? state : withoutFocusedState(state),
110
+ entryIsSelected
111
+ ),
92
112
  allowDynamicColorsOutsideExtensions:
93
113
  theme?.allow_dynamic_colors_outside_extensions,
94
114
  }),
@@ -134,7 +154,11 @@ export function masterCellBuilder({
134
154
  );
135
155
 
136
156
  return (
137
- <View style={containerStyle} ref={wrapperRef}>
157
+ <View
158
+ style={containerStyle}
159
+ ref={wrapperRef}
160
+ {...(interactive ? undefined : nonInteractiveRootProps)}
161
+ >
138
162
  {elements.map(remapElements)}
139
163
  </View>
140
164
  );
@@ -0,0 +1,20 @@
1
+ import { withoutFocusedState } from "..";
2
+
3
+ describe("withoutFocusedState", () => {
4
+ it("maps focused to default", () => {
5
+ expect(withoutFocusedState("focused")).toBe("default");
6
+ });
7
+
8
+ it("maps focused_selected to selected", () => {
9
+ expect(withoutFocusedState("focused_selected")).toBe("selected");
10
+ });
11
+
12
+ it("keeps default and selected states", () => {
13
+ expect(withoutFocusedState("default")).toBe("default");
14
+ expect(withoutFocusedState("selected")).toBe("selected");
15
+ });
16
+
17
+ it("keeps a missing state", () => {
18
+ expect(withoutFocusedState(undefined)).toBeUndefined();
19
+ });
20
+ });
@@ -386,3 +386,21 @@ export function getEntryState(state, selected) {
386
386
 
387
387
  return selected ? "selected" : "default";
388
388
  }
389
+
390
+ /**
391
+ * Drops focused (pressed) variants of a cell state, so a non-interactive cell
392
+ * always renders its idle look. `selected` is kept – it is not touch feedback.
393
+ */
394
+ export function withoutFocusedState(
395
+ state?: CellState | null
396
+ ): CellState | null | undefined {
397
+ if (state === "focused_selected") {
398
+ return "selected";
399
+ }
400
+
401
+ if (state === "focused") {
402
+ return "default";
403
+ }
404
+
405
+ return state;
406
+ }
@@ -12,6 +12,7 @@ export type ActionButtonProps = {
12
12
  item: RegisteredAction;
13
13
  configuration: any;
14
14
  width: number;
15
+ component?: ZappUIComponent & { parent?: ZappUIComponent };
15
16
  };
16
17
 
17
18
  type Props = ActionButtonProps & {
@@ -27,7 +28,7 @@ type Props = ActionButtonProps & {
27
28
  * (backed by the action's observable / `addListener`).
28
29
  */
29
30
  export function ActionButton(props: Props) {
30
- const { item, entry, onPress: onItemPress } = props;
31
+ const { item, entry, onPress: onItemPress, component } = props;
31
32
 
32
33
  const action = item?.action;
33
34
 
@@ -38,9 +39,10 @@ export function ActionButton(props: Props) {
38
39
  invokeAction({
39
40
  context: "toast",
40
41
  dismiss: () => onItemPress?.(item),
42
+ ...(component ? { component } : {}),
41
43
  });
42
44
  },
43
- [invokeAction, item, onItemPress]
45
+ [invokeAction, item, onItemPress, component]
44
46
  );
45
47
 
46
48
  if (!isActionAvailableFor(action, entry as ZappEntry)) return null;
@@ -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
+ });
@@ -8,9 +8,12 @@ import { ActionButton, ActionButtonProps } from "./ActionButton";
8
8
  * `BottomSheetModalContent` renders `buttonComponent` per item and does not
9
9
  * know about entries, so the entry is closed over here instead.
10
10
  */
11
- export const boundActionButton = (entry: ZappEntry | ZappFeed) => {
11
+ export const boundActionButton = (
12
+ entry: ZappEntry | ZappFeed,
13
+ component?: ZappUIComponent & { parent?: ZappUIComponent }
14
+ ) => {
12
15
  const ActionButtonWithEntry = (props: ActionButtonProps) => (
13
- <ActionButton {...props} entry={entry as ZappEntry} />
16
+ <ActionButton {...props} entry={entry as ZappEntry} component={component} />
14
17
  );
15
18
 
16
19
  ActionButtonWithEntry.displayName = "ActionButtonWithEntry";
@@ -17,6 +17,8 @@ export type OpenActionsBottomSheetArgs = {
17
17
  actions: RegisteredAction[];
18
18
  title?: string;
19
19
  summary?: string;
20
+ /** List that opened the sheet; forwarded into row invokeAction context. */
21
+ component?: ZappUIComponent & { parent?: ZappUIComponent };
20
22
  };
21
23
 
22
24
  /**
@@ -32,6 +34,7 @@ export function openActionsBottomSheet({
32
34
  actions,
33
35
  title,
34
36
  summary,
37
+ component,
35
38
  }: OpenActionsBottomSheetArgs): void {
36
39
  if (!actions?.length) {
37
40
  // Every action resolved away - unavailable for this entry, or not
@@ -50,7 +53,7 @@ export function openActionsBottomSheet({
50
53
  // BottomSheetModalContent still requires an onPress.
51
54
  onPress: noop,
52
55
  items: actions,
53
- buttonComponent: boundActionButton(entry),
56
+ buttonComponent: boundActionButton(entry, component),
54
57
  title,
55
58
  summary,
56
59
  },
@@ -17,6 +17,9 @@ import {
17
17
  Text,
18
18
  View,
19
19
  } from "react-native";
20
+ import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
21
+
22
+ const logger = createLogger({ category: "ModalComponent:AudioPlayerButton" });
20
23
 
21
24
  const styles = StyleSheet.create({
22
25
  container: {
@@ -130,6 +133,28 @@ export const DEFAULT_ICONS_CONFIGURATION: IconsConfiguration = {
130
133
  },
131
134
  };
132
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
+
133
158
  // ---------------------------------------------------------------------------
134
159
  // Data (runtime / localization — not styling config)
135
160
  // ---------------------------------------------------------------------------
@@ -140,30 +165,15 @@ export type ButtonData = {
140
165
  };
141
166
 
142
167
  export type AudioPlayerButtonProps = {
143
- configuration: ButtonConfiguration;
144
- data: ButtonData;
168
+ configuration?: ButtonConfiguration;
169
+ data?: ButtonData;
145
170
  disabled?: boolean;
146
171
  focused?: boolean;
147
172
  onPress?: () => void;
148
173
  };
149
174
 
150
- // ---------------------------------------------------------------------------
151
- // Hardcoded spec values (black fields)
152
- // ---------------------------------------------------------------------------
153
-
154
- // Hardcoded: action type → icon asset mapping (base64 PNG)
155
- const ACTION_ICON_ASSETS: Record<ButtonActionType, string> = {
156
- addToQueue:
157
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAKUlEQVR42mNgGAVDCfxHw6MWjFpAvEGk4lELKLdgNJmOWkA7C0YBbQAAwiZvkR9eFgQAAAAASUVORK5CYII=",
158
- addToPlaylist:
159
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAJklEQVR42mNgGAUjFvzHgmluATF4cFswGsmjkTwayaORPAqGLwAA6Jpxj9Ms9QMAAAAASUVORK5CYII=",
160
- goToPlaylist:
161
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAALklEQVR42mNgGAWjYNiD/7Q2/D8tLflPiSX/ycQ0t+D/yLGAEkeMWjAKRgG9AQB4zlSshwSzWwAAAABJRU5ErkJggg==",
162
- confirm:
163
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAJElEQVR42mNgGAWjYBSQCv4TwKMWDHELho/hNE/3o2AUDDcAAPv0QMAyy1CtAAAAAElFTkSuQmCC",
164
- cancel:
165
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAASElEQVR42mNgGAWjGvynkVoUTf+pqA6nxv8UqiHbEooNx2cQ1QzHZiDVDcdlCcOQsoCmQUTTSKZpMqV5RqNLUUHTwm4UDBMAALBLQ73LTvoyAAAAAElFTkSuQmCC",
166
- };
175
+ const PLACEHOLDER_RED_BOX =
176
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAJ0lEQVR4nO3NMQEAAAjDsIF/z2ACvlRAU5NMHuvPOQAAAAAAAMBdC/07Ai5sNw+CAAAAAElFTkSuQmCC";
167
177
 
168
178
  function getTextTransformStyle(
169
179
  textTransform: TextTransform
@@ -276,13 +286,19 @@ type ButtonLeadingIconProps = {
276
286
  configuration: IconsConfiguration;
277
287
  };
278
288
 
279
- function ButtonLeadingIcon({
280
- actionType,
281
- configuration,
282
- }: ButtonLeadingIconProps) {
283
- const asset =
284
- configuration.leadingIcon.asset ||
285
- (actionType ? ACTION_ICON_ASSETS[actionType] : undefined);
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]);
286
302
 
287
303
  if (!asset) {
288
304
  return null;
@@ -308,13 +324,14 @@ import { useModalBlocksStyle } from "../../ModalBlocksStyleContext";
308
324
  import { mergeButtonConfiguration } from "../utils/mergeStyles";
309
325
 
310
326
  export function AudioPlayerButton({
311
- configuration,
312
- data,
327
+ configuration = VARIANT_WITH_ICON,
328
+ data = { title: undefined },
313
329
  disabled,
314
330
  focused = false,
315
331
  onPress,
316
332
  }: AudioPlayerButtonProps) {
317
333
  const modalBlocksStyle = useModalBlocksStyle();
334
+
318
335
  const buttonBlock = modalBlocksStyle?.button;
319
336
  const actionBlock = modalBlocksStyle?.action;
320
337
 
@@ -328,9 +345,21 @@ export function AudioPlayerButton({
328
345
  icons: iconsConfiguration,
329
346
  } = effectiveConfig;
330
347
 
331
- const { title } = data;
348
+ const title = data?.title || buttonBlock?.title.default;
332
349
  const isDisabled = disabled ?? data.disabled ?? false;
333
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
+
334
363
  const { leadingIcon } = iconsConfiguration;
335
364
  const showIcon = shouldShowLeadingIcon(leadingIcon);
336
365
 
@@ -361,47 +390,3 @@ export function AudioPlayerButton({
361
390
  </ButtonContainer>
362
391
  );
363
392
  }
364
-
365
- // ---------------------------------------------------------------------------
366
- // Snack demo — two Button variants from configuration only
367
- // ---------------------------------------------------------------------------
368
-
369
- export const VARIANT_TEXT_ONLY: ButtonConfiguration = {
370
- background: DEFAULT_BACKGROUND_CONFIGURATION,
371
- title: DEFAULT_TITLE_CONFIGURATION,
372
- icons: DEFAULT_ICONS_CONFIGURATION,
373
- };
374
-
375
- export const VARIANT_WITH_ICON: ButtonConfiguration = {
376
- background: DEFAULT_BACKGROUND_CONFIGURATION,
377
- title: DEFAULT_TITLE_CONFIGURATION,
378
- icons: {
379
- ...DEFAULT_ICONS_CONFIGURATION,
380
- leadingIcon: {
381
- enabled: true,
382
- actionType: "addToPlaylist",
383
- },
384
- },
385
- };
386
-
387
- export const VARIANT_UPPERCASE_TITLE: ButtonConfiguration = {
388
- background: DEFAULT_BACKGROUND_CONFIGURATION,
389
- title: {
390
- ...DEFAULT_TITLE_CONFIGURATION,
391
- textTransform: "uppercase",
392
- },
393
- icons: DEFAULT_ICONS_CONFIGURATION,
394
- };
395
-
396
- export const VARIANT_LARGE_ICON: ButtonConfiguration = {
397
- background: DEFAULT_BACKGROUND_CONFIGURATION,
398
- title: DEFAULT_TITLE_CONFIGURATION,
399
- icons: {
400
- width: 32,
401
- height: 32,
402
- leadingIcon: {
403
- enabled: true,
404
- actionType: "addToQueue",
405
- },
406
- },
407
- };