@applicaster/zapp-react-native-ui-components 16.0.0-rc.85 → 16.0.0-rc.87

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 (22) hide show
  1. package/Components/GeneralContentScreen/GeneralContentScreenHookAdapter.tsx +15 -0
  2. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +1 -23
  3. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +1 -36
  4. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +1 -26
  5. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +30 -25
  6. package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +7 -2
  7. package/Components/ModalComponent/AudioPlayer/Components/__tests__/Item.test.tsx +48 -1
  8. package/Components/ModalComponent/AudioPlayer/Components/index.ts +0 -2
  9. package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +18 -0
  10. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +54 -0
  11. package/Components/ModalComponent/BottomSheetModalContent.tsx +92 -51
  12. package/Components/ModalComponent/SortableList.tsx +38 -23
  13. package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +17 -0
  14. package/Components/ModalComponent/index.tsx +7 -9
  15. package/Components/River/ComponentsMap/ComponentsMap.tsx +85 -59
  16. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +138 -75
  17. package/Components/River/__tests__/componentsMap.test.js +8 -3
  18. package/Components/Screen/navigationHandler.ts +8 -8
  19. package/Contexts/ModalNavigationContext/__tests__/index.test.tsx +15 -0
  20. package/Contexts/ModalNavigationContext/index.tsx +2 -2
  21. package/package.json +5 -5
  22. package/Components/ModalComponent/AudioPlayer/Components/Action.tsx +0 -314
@@ -11,6 +11,20 @@ type HookComponentProps = HookPluginProps & {
11
11
  function GeneralContentScreenHookComponent(props: HookComponentProps) {
12
12
  const { hookPlugin, focused, parentFocus } = props;
13
13
 
14
+ const componentsMapExtraPropsMemo = React.useMemo(
15
+ () => ({
16
+ safeArea: {
17
+ edges: {
18
+ top: "additive",
19
+ bottom: "additive",
20
+ left: "off",
21
+ right: "off",
22
+ },
23
+ },
24
+ }),
25
+ []
26
+ );
27
+
14
28
  if (!hookPlugin?.screen_id) {
15
29
  log_error(
16
30
  "GeneralContentScreenHookAdapter: This component should only be used as a hook, but no hookPlugin was provided."
@@ -26,6 +40,7 @@ function GeneralContentScreenHookComponent(props: HookComponentProps) {
26
40
  isScreenWrappedInContainer={false}
27
41
  focused={focused}
28
42
  parentFocus={parentFocus}
43
+ componentsMapExtraProps={componentsMapExtraPropsMemo}
29
44
  />
30
45
  </HookContentFocusGroup>
31
46
  );
@@ -363,7 +363,7 @@ export function AudioPlayerButton({
363
363
  }
364
364
 
365
365
  // ---------------------------------------------------------------------------
366
- // Snack demo — two Button variants from configuration only
366
+ // Variant configurations
367
367
  // ---------------------------------------------------------------------------
368
368
 
369
369
  export const VARIANT_TEXT_ONLY: ButtonConfiguration = {
@@ -383,25 +383,3 @@ export const VARIANT_WITH_ICON: ButtonConfiguration = {
383
383
  },
384
384
  },
385
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
- };
@@ -63,11 +63,7 @@ export type TextTransform =
63
63
  export type TitleAlignment = "left" | "center";
64
64
 
65
65
  /** Selects hardcoded container + action layout (white fields). */
66
- export type HeaderVariant =
67
- | "singleLine"
68
- | "headerWithActions"
69
- | "headerWithActionsCancelOnly"
70
- | "section";
66
+ export type HeaderVariant = "singleLine" | "headerWithActions" | "section";
71
67
 
72
68
  export type TitleConfiguration = {
73
69
  fontColor: string;
@@ -198,13 +194,6 @@ const CONTAINER_SPEC: Record<HeaderVariant, ContainerSpec> = {
198
194
  paddingBottom: 8,
199
195
  paddingLeft: 20,
200
196
  },
201
- headerWithActionsCancelOnly: {
202
- horizontalGutter: 12,
203
- paddingTop: 0,
204
- paddingRight: 20,
205
- paddingBottom: 8,
206
- paddingLeft: 20,
207
- },
208
197
  section: {
209
198
  horizontalGutter: 12,
210
199
  paddingTop: 0,
@@ -228,11 +217,6 @@ const ACTION_BUTTONS_BEHAVIOR: Record<
228
217
  leading: { enabled: true, actionType: "cancel" },
229
218
  trailing: { enabled: true, actionType: "done" },
230
219
  },
231
- headerWithActionsCancelOnly: {
232
- sectionEnabled: true,
233
- leading: { enabled: true, actionType: "cancel" },
234
- trailing: { enabled: false },
235
- },
236
220
  section: {
237
221
  sectionEnabled: true,
238
222
  leading: { enabled: false },
@@ -725,25 +709,6 @@ export const VARIANT_HEADER_WITH_ACTIONS: HeaderConfiguration = {
725
709
  paddingTop: 16,
726
710
  };
727
711
 
728
- /** Variant 2 — single side action (layout rule demo) */
729
- export const VARIANT_HEADER_WITH_CANCEL_ONLY: HeaderConfiguration = {
730
- variant: "headerWithActionsCancelOnly",
731
- title: {
732
- fontColor: "#FFFFFF",
733
- iosFontFamily: "SFProText-Bold",
734
- androidFontFamily: "Roboto-Bold",
735
- fontSize: 22,
736
- lineHeight: 28,
737
- iosLetterSpacing: -0.6,
738
- androidLetterSpacing: 0,
739
- textTransform: "default",
740
- alignment: "center",
741
- },
742
- closeButton: { enabled: false },
743
- actionButtons: DEFAULT_ACTION_BUTTONS_STYLE_CONFIGURATION,
744
- paddingTop: 16,
745
- };
746
-
747
712
  /** Variant 3 — Section Header */
748
713
  export const VARIANT_SECTION_HEADER: HeaderConfiguration = {
749
714
  variant: "section",
@@ -565,7 +565,7 @@ export function AudioPlayerInput({
565
565
  }
566
566
 
567
567
  // ---------------------------------------------------------------------------
568
- // Variant presets — collections of configuration keys (not a variant field)
568
+ // Variant configurations
569
569
  // ---------------------------------------------------------------------------
570
570
 
571
571
  /** Search pill — leading icon + clear button */
@@ -626,28 +626,3 @@ export const VARIANT_NAME_PLAYLIST_INPUT: InputConfiguration = {
626
626
  },
627
627
  },
628
628
  };
629
-
630
- /** Text only search pill — no leading icon */
631
- export const VARIANT_TEXT_ONLY_INPUT: InputConfiguration = {
632
- background: DEFAULT_BACKGROUND_CONFIGURATION,
633
- label: DEFAULT_LABEL_CONFIGURATION,
634
- text: DEFAULT_TEXT_CONFIGURATION,
635
- icons: {
636
- ...DEFAULT_ICONS_CONFIGURATION,
637
- leadingIcon: { enabled: false },
638
- },
639
- };
640
-
641
- /** Search without clear button */
642
- export const VARIANT_SEARCH_WITHOUT_CLEAR: InputConfiguration = {
643
- background: DEFAULT_BACKGROUND_CONFIGURATION,
644
- label: DEFAULT_LABEL_CONFIGURATION,
645
- text: DEFAULT_TEXT_CONFIGURATION,
646
- icons: {
647
- ...DEFAULT_ICONS_CONFIGURATION,
648
- trailingIcon: {
649
- ...DEFAULT_ICONS_CONFIGURATION.trailingIcon,
650
- enabled: false,
651
- },
652
- },
653
- };
@@ -12,14 +12,8 @@
12
12
  */
13
13
 
14
14
  import React from "react";
15
- import {
16
- Image,
17
- Platform,
18
- Pressable,
19
- StyleSheet,
20
- Text,
21
- View,
22
- } from "react-native";
15
+ import { QBImage as Image } from "@applicaster/zapp-react-native-ui-components/Components/Image";
16
+ import { Platform, Pressable, StyleSheet, Text, View } from "react-native";
23
17
  import {
24
18
  MenuItem,
25
19
  SelectionBehavior,
@@ -291,13 +285,14 @@ function resolveContainerSpec(image: ImageConfiguration): ContainerSpec {
291
285
 
292
286
  export function mapMenuItemToNowPlayingData(
293
287
  item: MenuItem,
294
- isPaused?: boolean
288
+ isPaused?: boolean,
289
+ nowPlayingLabel = "Now Playing"
295
290
  ): ItemData {
296
291
  return {
297
292
  imageUri: item.icon,
298
293
  textLabel1: item.title,
299
294
  textLabel2: item.summary ?? "",
300
- nowPlayingLabel: "Now Playing",
295
+ nowPlayingLabel,
301
296
  trailingButton: "playPause",
302
297
  isSelected: isPaused !== undefined ? !isPaused : item.isSelected,
303
298
  };
@@ -402,6 +397,10 @@ type ItemLabelProps = {
402
397
  };
403
398
 
404
399
  function ItemLabel({ text, configuration, focused = false }: ItemLabelProps) {
400
+ if (!text) {
401
+ return null;
402
+ }
403
+
405
404
  const color =
406
405
  focused && configuration.focusedFontColor
407
406
  ? configuration.focusedFontColor
@@ -878,22 +877,28 @@ export function AudioPlayerTrackItem({
878
877
  <View style={{ width: container.horizontalGutter }} />
879
878
  <View style={styles.contentColumn}>
880
879
  {showNowPlaying ? (
881
- <>
882
- <NowPlayingRow
883
- assetConfiguration={nowPlayingAsset}
884
- labelConfiguration={nowPlayingLabel}
885
- label={data.nowPlayingLabel}
886
- />
887
- <View style={{ height: container.verticalGutter }} />
888
- </>
880
+ <NowPlayingRow
881
+ assetConfiguration={nowPlayingAsset}
882
+ labelConfiguration={nowPlayingLabel}
883
+ label={data.nowPlayingLabel}
884
+ />
885
+ ) : null}
886
+ {showNowPlaying && (data.textLabel1 || data.textLabel2) ? (
887
+ <View style={{ height: container.verticalGutter }} />
888
+ ) : null}
889
+ {data.textLabel1 ? (
890
+ <ItemLabel
891
+ text={data.textLabel1}
892
+ configuration={textLabel1}
893
+ focused={focused}
894
+ />
895
+ ) : null}
896
+ {data.textLabel1 && data.textLabel2 ? (
897
+ <View style={{ height: container.verticalGutter }} />
898
+ ) : null}
899
+ {data.textLabel2 ? (
900
+ <ItemLabel text={data.textLabel2} configuration={textLabel2} />
889
901
  ) : null}
890
- <ItemLabel
891
- text={data.textLabel1}
892
- configuration={textLabel1}
893
- focused={focused}
894
- />
895
- <View style={{ height: container.verticalGutter }} />
896
- <ItemLabel text={data.textLabel2} configuration={textLabel2} />
897
902
  </View>
898
903
  <ItemButtons
899
904
  leadingButton={data.leadingButton}
@@ -21,10 +21,14 @@ const styles = StyleSheet.create({
21
21
 
22
22
  type NowPlayingHeaderSectionProps = {
23
23
  onClear?: () => void;
24
+ nextUpTitle?: string;
25
+ nowPlayingTitle?: string;
24
26
  };
25
27
 
26
28
  export function NowPlayingHeaderSection({
27
29
  onClear,
30
+ nextUpTitle = "Next Up",
31
+ nowPlayingTitle = "Now Playing",
28
32
  }: NowPlayingHeaderSectionProps) {
29
33
  const player = usePlayer();
30
34
  const playerState = usePlayerState("queue-now-playing-section");
@@ -53,7 +57,8 @@ export function NowPlayingHeaderSection({
53
57
 
54
58
  const data = mapMenuItemToNowPlayingData(
55
59
  nowPlayingItem,
56
- playerState?.isPaused
60
+ playerState?.isPaused,
61
+ nowPlayingTitle
57
62
  );
58
63
 
59
64
  const handlePlayPause = () => {
@@ -81,7 +86,7 @@ export function NowPlayingHeaderSection({
81
86
  {renderNowPlayingTrack()}
82
87
  <AudioPlayerHeader
83
88
  configuration={VARIANT_SECTION_HEADER}
84
- data={{ title: "Next Up" }}
89
+ data={{ title: nextUpTitle }}
85
90
  onClear={onClear}
86
91
  />
87
92
  </View>
@@ -1,7 +1,11 @@
1
1
  import React from "react";
2
2
  import { Pressable } from "react-native";
3
3
  import { fireEvent, render, screen } from "@testing-library/react-native";
4
- import { AudioPlayerTrackItem, VARIANT_PLAYLIST_ITEM } from "../Item";
4
+ import {
5
+ AudioPlayerTrackItem,
6
+ VARIANT_PLAYLIST_ITEM,
7
+ mapMenuItemToNowPlayingData,
8
+ } from "../Item";
5
9
 
6
10
  const playlistItemData = {
7
11
  textLabel1: "Queue",
@@ -63,4 +67,47 @@ describe("AudioPlayerTrackItem multiSelect checkbox", () => {
63
67
 
64
68
  expect(onPress).toHaveBeenCalledTimes(1);
65
69
  });
70
+
71
+ it("renders only textLabel1 Text element when textLabel2 is missing or empty", () => {
72
+ const { UNSAFE_getAllByType, queryByText } = render(
73
+ <AudioPlayerTrackItem
74
+ configuration={VARIANT_PLAYLIST_ITEM}
75
+ data={{
76
+ textLabel1: "Only Title",
77
+ textLabel2: "",
78
+ }}
79
+ />
80
+ );
81
+
82
+ const { Text } = require("react-native");
83
+ const textNodes = UNSAFE_getAllByType(Text);
84
+ expect(textNodes).toHaveLength(1);
85
+ expect(queryByText("Only Title")).toBeTruthy();
86
+ });
87
+ });
88
+
89
+ describe("mapMenuItemToNowPlayingData", () => {
90
+ const item = {
91
+ id: "item_1",
92
+ title: "Track Title",
93
+ summary: "Artist Name",
94
+ icon: "https://example.com/cover.png",
95
+ };
96
+
97
+ it("uses default 'Now Playing' label when omitted", () => {
98
+ const data = mapMenuItemToNowPlayingData(item as any);
99
+ expect(data.nowPlayingLabel).toBe("Now Playing");
100
+ expect(data.textLabel1).toBe("Track Title");
101
+ expect(data.textLabel2).toBe("Artist Name");
102
+ });
103
+
104
+ it("uses custom nowPlayingLabel when provided", () => {
105
+ const data = mapMenuItemToNowPlayingData(
106
+ item as any,
107
+ false,
108
+ "En cours de lecture"
109
+ );
110
+
111
+ expect(data.nowPlayingLabel).toBe("En cours de lecture");
112
+ });
66
113
  });
@@ -1,5 +1,3 @@
1
- export { AudioPlayerAction } from "./Action";
2
-
3
1
  export { AudioPlayerButton, VARIANT_WITH_ICON } from "./Button";
4
2
 
5
3
  export { AudioPlayerHeader } from "./Header";
@@ -227,4 +227,22 @@ describe("mergeButtonConfiguration background", () => {
227
227
  "https://placehold.co/24x24/FF00FF/FFFFFF/png?text=CN"
228
228
  );
229
229
  });
230
+
231
+ it("applies Studio button title typography (fontFamily, letterSpacing, textTransform)", () => {
232
+ const merged = mergeButtonConfiguration(buttonBase as any, {
233
+ title: {
234
+ default: {
235
+ fontFamily: "CustomFont",
236
+ letterSpacing: -0.5,
237
+ textTransform: "uppercase",
238
+ },
239
+ },
240
+ });
241
+
242
+ expect(merged.title.iosFontFamily).toBe("CustomFont");
243
+ expect(merged.title.androidFontFamily).toBe("CustomFont");
244
+ expect(merged.title.iosLetterSpacing).toBe(-0.5);
245
+ expect(merged.title.androidLetterSpacing).toBe(-0.5);
246
+ expect(merged.title.textTransform).toBe("uppercase");
247
+ });
230
248
  });
@@ -93,6 +93,33 @@ function mergeItemLabel(
93
93
  ...(override.lineHeight !== undefined
94
94
  ? { lineHeight: override.lineHeight }
95
95
  : {}),
96
+ ...(override.fontFamily
97
+ ? {
98
+ iosFontFamily: override.fontFamily,
99
+ androidFontFamily: override.fontFamily,
100
+ }
101
+ : {}),
102
+ ...(override.iosFontFamily
103
+ ? { iosFontFamily: override.iosFontFamily }
104
+ : {}),
105
+ ...(override.androidFontFamily
106
+ ? { androidFontFamily: override.androidFontFamily }
107
+ : {}),
108
+ ...(override.letterSpacing !== undefined
109
+ ? {
110
+ iosLetterSpacing: override.letterSpacing,
111
+ androidLetterSpacing: override.letterSpacing,
112
+ }
113
+ : {}),
114
+ ...(override.iosLetterSpacing !== undefined
115
+ ? { iosLetterSpacing: override.iosLetterSpacing }
116
+ : {}),
117
+ ...(override.androidLetterSpacing !== undefined
118
+ ? { androidLetterSpacing: override.androidLetterSpacing }
119
+ : {}),
120
+ ...(override.textTransform
121
+ ? { textTransform: override.textTransform }
122
+ : {}),
96
123
  };
97
124
  }
98
125
 
@@ -267,6 +294,33 @@ function mergeButtonTitle(
267
294
  ...(override.lineHeight !== undefined
268
295
  ? { lineHeight: override.lineHeight }
269
296
  : {}),
297
+ ...(override.fontFamily
298
+ ? {
299
+ iosFontFamily: override.fontFamily,
300
+ androidFontFamily: override.fontFamily,
301
+ }
302
+ : {}),
303
+ ...(override.iosFontFamily
304
+ ? { iosFontFamily: override.iosFontFamily }
305
+ : {}),
306
+ ...(override.androidFontFamily
307
+ ? { androidFontFamily: override.androidFontFamily }
308
+ : {}),
309
+ ...(override.letterSpacing !== undefined
310
+ ? {
311
+ iosLetterSpacing: override.letterSpacing,
312
+ androidLetterSpacing: override.letterSpacing,
313
+ }
314
+ : {}),
315
+ ...(override.iosLetterSpacing !== undefined
316
+ ? { iosLetterSpacing: override.iosLetterSpacing }
317
+ : {}),
318
+ ...(override.androidLetterSpacing !== undefined
319
+ ? { androidLetterSpacing: override.androidLetterSpacing }
320
+ : {}),
321
+ ...(override.textTransform
322
+ ? { textTransform: override.textTransform }
323
+ : {}),
270
324
  };
271
325
  }
272
326