@applicaster/zapp-react-native-ui-components 16.0.0-rc.33 → 16.0.0-rc.34

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/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +47 -21
  2. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +34 -6
  3. package/Components/MasterCell/DefaultComponents/ImageContainer/index.tsx +5 -3
  4. package/Components/MasterCell/DefaultComponents/PressableView.tsx +3 -3
  5. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +40 -2
  6. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/AssetComponent.tsx +15 -4
  7. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +56 -22
  8. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +6 -4
  9. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +3 -0
  10. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +1 -1
  11. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +15 -4
  12. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +23 -16
  13. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +8 -3
  14. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +2 -2
  15. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/insertButtonsBetweenLabels.test.ts +45 -13
  16. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +2 -2
  17. package/Components/ModalComponent/AudioPlayer/Components/Action.tsx +368 -0
  18. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +424 -0
  19. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +728 -0
  20. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +709 -0
  21. package/Components/ModalComponent/AudioPlayer/Components/index.ts +7 -0
  22. package/package.json +5 -5
@@ -3,18 +3,20 @@ import { insertButtonsBetweenLabels } from "..";
3
3
  describe("insertButtonsBetweenLabels - depthlevel 1", () => {
4
4
  const buttons = "buttons";
5
5
 
6
- const labels = [
7
- { name: "label_1" },
8
- { name: "label_2" },
9
- { name: "label_3" },
6
+ const slots = [
7
+ { name: "label_1", element: { id: "label_1" } },
8
+ { name: "label_2", element: { id: "label_2" } },
9
+ { name: "label_3", element: { id: "label_3" } },
10
10
  ];
11
11
 
12
+ const labels = slots.map((slot) => slot.element);
13
+
12
14
  it("put buttons on top", () => {
13
15
  const configuration = {
14
16
  tv_buttons_container_position: "on_top",
15
17
  };
16
18
 
17
- const result = insertButtonsBetweenLabels(configuration, buttons, labels);
19
+ const result = insertButtonsBetweenLabels(configuration, buttons, slots);
18
20
 
19
21
  expect(result).toEqual([buttons, ...labels]);
20
22
  });
@@ -24,7 +26,7 @@ describe("insertButtonsBetweenLabels - depthlevel 1", () => {
24
26
  tv_buttons_container_position: "unknown_position",
25
27
  };
26
28
 
27
- const result = insertButtonsBetweenLabels(configuration, buttons, labels);
29
+ const result = insertButtonsBetweenLabels(configuration, buttons, slots);
28
30
 
29
31
  expect(result).toEqual([...labels, buttons]);
30
32
  });
@@ -34,7 +36,7 @@ describe("insertButtonsBetweenLabels - depthlevel 1", () => {
34
36
  tv_buttons_container_position: "label_1",
35
37
  };
36
38
 
37
- const result = insertButtonsBetweenLabels(configuration, buttons, labels);
39
+ const result = insertButtonsBetweenLabels(configuration, buttons, slots);
38
40
 
39
41
  expect(result).toEqual([labels[0], buttons, labels[1], labels[2]]);
40
42
  });
@@ -44,7 +46,7 @@ describe("insertButtonsBetweenLabels - depthlevel 1", () => {
44
46
  tv_buttons_container_position: "label_2",
45
47
  };
46
48
 
47
- const result = insertButtonsBetweenLabels(configuration, buttons, labels);
49
+ const result = insertButtonsBetweenLabels(configuration, buttons, slots);
48
50
 
49
51
  expect(result).toEqual([labels[0], labels[1], buttons, labels[2]]);
50
52
  });
@@ -54,22 +56,52 @@ describe("insertButtonsBetweenLabels - depthlevel 1", () => {
54
56
  tv_buttons_container_position: "label_3",
55
57
  };
56
58
 
57
- const result = insertButtonsBetweenLabels(configuration, buttons, labels);
59
+ const result = insertButtonsBetweenLabels(configuration, buttons, slots);
58
60
 
59
61
  expect(result).toEqual([labels[0], labels[1], labels[2], buttons]);
60
62
  });
61
63
 
62
- it("put underneath all", () => {
64
+ it("put above label_2", () => {
65
+ const configuration = {
66
+ tv_buttons_container_position: "above_label_2",
67
+ };
68
+
69
+ const result = insertButtonsBetweenLabels(configuration, buttons, slots);
70
+
71
+ expect(result).toEqual([labels[0], buttons, labels[1], labels[2]]);
72
+ });
73
+
74
+ it("keeps the slot when its label is empty (null element)", () => {
75
+ const slotsWithEmpty = [
76
+ { name: "label_1", element: { id: "label_1" } },
77
+ { name: "label_2", element: null },
78
+ { name: "label_3", element: { id: "label_3" } },
79
+ ];
80
+
81
+ const configuration = {
82
+ tv_buttons_container_position: "above_label_2",
83
+ };
84
+
85
+ const result = insertButtonsBetweenLabels(
86
+ configuration,
87
+ buttons,
88
+ slotsWithEmpty
89
+ );
90
+
91
+ expect(result).toEqual([{ id: "label_1" }, buttons, { id: "label_3" }]);
92
+ });
93
+
94
+ it("appends buttons when the target is unknown", () => {
63
95
  const configuration = {
64
96
  tv_buttons_container_position: "label_X",
65
97
  };
66
98
 
67
- const result = insertButtonsBetweenLabels(configuration, buttons, labels);
99
+ const result = insertButtonsBetweenLabels(configuration, buttons, slots);
68
100
 
69
101
  expect(result).toEqual([labels[0], labels[1], labels[2], buttons]);
70
102
  });
71
103
 
72
- it("put underneath all", () => {
104
+ it("appends buttons when there are no slots", () => {
73
105
  const configuration = {
74
106
  tv_buttons_container_position: "label_X",
75
107
  };
@@ -88,7 +120,7 @@ describe("insertButtonsBetweenLabels - depthlevel 1", () => {
88
120
  tv_buttons_container_position: "label_X",
89
121
  };
90
122
 
91
- const result = insertButtonsBetweenLabels(configuration, null, labels);
123
+ const result = insertButtonsBetweenLabels(configuration, null, slots);
92
124
 
93
125
  expect(result).toEqual(labels);
94
126
  });
@@ -34,7 +34,7 @@ export const memoizedGetPluginIdentifier = memoizee(getPluginIdentifier);
34
34
  export const insertButtonsBetweenLabels = (
35
35
  configuration: Record<string, unknown>,
36
36
  buttons,
37
- labels = []
37
+ slots = [] // { name, element } per canonical text label slot
38
38
  ) =>
39
39
  insertBetweenLabels(
40
40
  {
@@ -45,7 +45,7 @@ export const insertButtonsBetweenLabels = (
45
45
  appendWhenMissing: true,
46
46
  },
47
47
  buttons,
48
- labels
48
+ slots
49
49
  );
50
50
 
51
51
  export const insertButtonsBetweenLabelContainers = (
@@ -0,0 +1,368 @@
1
+ /**
2
+ * Modal Building Block — Action
3
+ *
4
+ * Single-file Expo Snack demo for the Action list item building block spec.
5
+ * Three forms (text only / leading icon + text / leading + text + trailing)
6
+ * are driven by data — no separate variants.
7
+ *
8
+ * Green (configurable): background, title, trailingIcon.chevronAsset
9
+ * White (hardcoded): container padding/gutter, icon size, leading icon assets,
10
+ * leading/trailing visibility logic from data
11
+ */
12
+
13
+ import React from "react";
14
+ import { Image, Platform, StyleSheet, Text, View } from "react-native";
15
+
16
+ const styles = StyleSheet.create({
17
+ row: {
18
+ flexDirection: "row",
19
+ alignItems: "center",
20
+ },
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
+ });
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Configuration (green / editable fields from spec)
43
+ // ---------------------------------------------------------------------------
44
+
45
+ export type TextTransform =
46
+ | "default"
47
+ | "lowercase"
48
+ | "uppercase"
49
+ | "capitalize";
50
+
51
+ export type BackgroundConfiguration = {
52
+ defaultColor: string;
53
+ focusedColor: string;
54
+ cornerRadius: number;
55
+ };
56
+
57
+ export type TitleConfiguration = {
58
+ fontColor: string;
59
+ focusedFontColor: string;
60
+ iosFontFamily: string;
61
+ androidFontFamily: string;
62
+ fontSize: number;
63
+ lineHeight: number;
64
+ iosLetterSpacing: number;
65
+ androidLetterSpacing: number;
66
+ textTransform: TextTransform;
67
+ };
68
+
69
+ export type TrailingIconConfiguration = {
70
+ chevronAsset?: string;
71
+ };
72
+
73
+ export type ActionConfiguration = {
74
+ background: BackgroundConfiguration;
75
+ title: TitleConfiguration;
76
+ trailingIcon: TrailingIconConfiguration;
77
+ };
78
+
79
+ export const DEFAULT_BACKGROUND_CONFIGURATION: BackgroundConfiguration = {
80
+ defaultColor: "transparent",
81
+ focusedColor: "rgba(62, 62, 62, 1)",
82
+ cornerRadius: 12,
83
+ };
84
+
85
+ export const DEFAULT_TITLE_CONFIGURATION: TitleConfiguration = {
86
+ fontColor: "#FFFFFF",
87
+ focusedFontColor: "#FFFFFF",
88
+ iosFontFamily: "SFProText-Semibold",
89
+ androidFontFamily: "Roboto-Medium",
90
+ fontSize: 15,
91
+ lineHeight: 24,
92
+ iosLetterSpacing: -0.2,
93
+ androidLetterSpacing: 0,
94
+ textTransform: "default",
95
+ };
96
+
97
+ export const DEFAULT_TRAILING_ICON_CONFIGURATION: TrailingIconConfiguration =
98
+ {};
99
+
100
+ export const DEFAULT_ACTION_CONFIGURATION: ActionConfiguration = {
101
+ background: DEFAULT_BACKGROUND_CONFIGURATION,
102
+ title: DEFAULT_TITLE_CONFIGURATION,
103
+ trailingIcon: DEFAULT_TRAILING_ICON_CONFIGURATION,
104
+ };
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Data (runtime — controls which elements are shown)
108
+ // ---------------------------------------------------------------------------
109
+
110
+ export type LeadingActionType =
111
+ | "addToQueue"
112
+ | "addToPlaylist"
113
+ | "favorite"
114
+ | "share"
115
+ | "sleepTimer"
116
+ | "playbackSpeed";
117
+
118
+ export type ActionData = {
119
+ title: string;
120
+ /** When set, leading icon is shown (Enable: On). */
121
+ leadingActionType?: LeadingActionType;
122
+ /** When true, trailing chevron is shown (Enable: On). */
123
+ showTrailingChevron?: boolean;
124
+ };
125
+
126
+ export type ItemProps = {
127
+ configuration: ActionConfiguration;
128
+ data: ActionData;
129
+ focused?: boolean;
130
+ };
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // Hardcoded spec values (white fields)
134
+ // ---------------------------------------------------------------------------
135
+
136
+ const CONTAINER_SPEC = {
137
+ paddingTop: 13,
138
+ paddingRight: 10,
139
+ paddingBottom: 13,
140
+ paddingLeft: 10,
141
+ horizontalGutter: 12,
142
+ };
143
+
144
+ const ICON_SPEC = {
145
+ width: 24,
146
+ height: 24,
147
+ };
148
+
149
+ const DEFAULT_CHEVRON_ASSET =
150
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAALElEQVR42mNgGAXUBlu2bPmPjIeWBeiGDy0Lhrbrh1/qGXqZaxSMglEw0gEA4c+IpUrpYX4AAAAASUVORK5CYII=";
151
+
152
+ /** Hardcoded: action type → leading icon asset (base64 PNG). */
153
+ const LEADING_ICON_ASSETS: Record<LeadingActionType, string> = {
154
+ addToQueue:
155
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAALElEQVR42mNgGAUjE/xHAzS3gFgweC0Y+nEwGslkBRFFwTj8LBjNyaNgmAMAJTf+EEEZcS0AAAAASUVORK5CYII=",
156
+ addToPlaylist:
157
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAKUlEQVR42mNgGAVDBvxHA6MWjFpAvEGkglELKLdgNJmOWkA7C0YBzQAAivu+UKRR1ZIAAAAASUVORK5CYII=",
158
+ favorite:
159
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAKklEQVR42mNgGAWjYBTgBP+xAGLkRi0YQhbgM4wqBo9aMHgsGAWjgCAAALyAZqh4yyZZAAAAAElFTkSuQmCC",
160
+ share:
161
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAK0lEQVR42mNgGAVDDvxHAqMWjFowUiz4TwQYgRYQYxBFlo1aMPAWjAK6AgD2g7pUUT7VeAAAAABJRU5ErkJggg==",
162
+ sleepTimer:
163
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAYUlEQVR42tWUSQ4AIAgD+f+n8eJRliIQ6MmEZTSxJZokvipdXgZpAbzOO17AglIXI7XULxmCoEMwRGuWam6AdRsN4IJYTRH4PIDkhxTA92zUPNBcFDDHaOVR0RJ2LXG9QgemFVK8tpo4PwAAAABJRU5ErkJggg==",
164
+ playbackSpeed:
165
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAK0lEQVR42mNgGAVDDvxHAqMWjFowUiz4TwQYgRYQYxBFlo1aMPAWjAK6AgD2g7pUUT7VeAAAAABJRU5ErkJggg==",
166
+ };
167
+
168
+ function getTextTransformStyle(
169
+ textTransform: TextTransform
170
+ ): "none" | "lowercase" | "uppercase" | "capitalize" {
171
+ return textTransform === "default" ? "none" : textTransform;
172
+ }
173
+
174
+ function shouldShowLeadingIcon(leadingActionType?: LeadingActionType) {
175
+ return !!leadingActionType;
176
+ }
177
+
178
+ function shouldShowTrailingIcon(showTrailingChevron?: boolean) {
179
+ return !!showTrailingChevron;
180
+ }
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // Inner components
184
+ // ---------------------------------------------------------------------------
185
+
186
+ type ActionContainerProps = {
187
+ configuration: BackgroundConfiguration;
188
+ focused: boolean;
189
+ children: React.ReactNode;
190
+ };
191
+
192
+ function ActionContainer({
193
+ configuration,
194
+ focused,
195
+ children,
196
+ }: ActionContainerProps) {
197
+ return (
198
+ <View
199
+ style={{
200
+ backgroundColor: focused
201
+ ? configuration.focusedColor
202
+ : configuration.defaultColor,
203
+ borderRadius: configuration.cornerRadius,
204
+ paddingTop: CONTAINER_SPEC.paddingTop,
205
+ paddingRight: CONTAINER_SPEC.paddingRight,
206
+ paddingBottom: CONTAINER_SPEC.paddingBottom,
207
+ paddingLeft: CONTAINER_SPEC.paddingLeft,
208
+ }}
209
+ >
210
+ {children}
211
+ </View>
212
+ );
213
+ }
214
+
215
+ type ActionTitleProps = {
216
+ title: string;
217
+ configuration: TitleConfiguration;
218
+ focused: boolean;
219
+ };
220
+
221
+ function ActionTitle({ title, configuration, focused }: ActionTitleProps) {
222
+ const letterSpacing =
223
+ Platform.OS === "ios"
224
+ ? configuration.iosLetterSpacing
225
+ : configuration.androidLetterSpacing;
226
+
227
+ return (
228
+ <Text
229
+ style={{
230
+ flex: 1,
231
+ color: focused
232
+ ? configuration.focusedFontColor
233
+ : configuration.fontColor,
234
+ fontSize: configuration.fontSize,
235
+ lineHeight: configuration.lineHeight,
236
+ letterSpacing,
237
+ fontFamily: Platform.select({
238
+ ios: configuration.iosFontFamily,
239
+ android: configuration.androidFontFamily,
240
+ default: configuration.androidFontFamily,
241
+ }),
242
+ textTransform: getTextTransformStyle(configuration.textTransform),
243
+ }}
244
+ numberOfLines={1}
245
+ >
246
+ {title}
247
+ </Text>
248
+ );
249
+ }
250
+
251
+ type ActionLeadingIconProps = {
252
+ actionType: LeadingActionType;
253
+ };
254
+
255
+ function ActionLeadingIcon({ actionType }: ActionLeadingIconProps) {
256
+ return (
257
+ <Image
258
+ source={{ uri: LEADING_ICON_ASSETS[actionType] }}
259
+ style={{ width: ICON_SPEC.width, height: ICON_SPEC.height }}
260
+ resizeMode="contain"
261
+ />
262
+ );
263
+ }
264
+
265
+ type ActionTrailingIconProps = {
266
+ configuration: TrailingIconConfiguration;
267
+ };
268
+
269
+ function ActionTrailingIcon({ configuration }: ActionTrailingIconProps) {
270
+ const asset = configuration.chevronAsset ?? DEFAULT_CHEVRON_ASSET;
271
+
272
+ return (
273
+ <Image
274
+ source={{ uri: asset }}
275
+ style={{ width: ICON_SPEC.width, height: ICON_SPEC.height }}
276
+ resizeMode="contain"
277
+ />
278
+ );
279
+ }
280
+
281
+ // ---------------------------------------------------------------------------
282
+ // Item
283
+ // ---------------------------------------------------------------------------
284
+
285
+ export function Item({ configuration, data, focused = false }: ItemProps) {
286
+ const { background, title, trailingIcon } = configuration;
287
+ const { title: label, leadingActionType, showTrailingChevron } = data;
288
+
289
+ const showLeading = shouldShowLeadingIcon(leadingActionType);
290
+ const showTrailing = shouldShowTrailingIcon(showTrailingChevron);
291
+
292
+ return (
293
+ <ActionContainer configuration={background} focused={focused}>
294
+ <View style={styles.row}>
295
+ {showLeading ? (
296
+ <>
297
+ <ActionLeadingIcon actionType={leadingActionType} />
298
+ <View style={{ width: CONTAINER_SPEC.horizontalGutter }} />
299
+ </>
300
+ ) : null}
301
+ <ActionTitle title={label} configuration={title} focused={focused} />
302
+ {showTrailing ? (
303
+ <>
304
+ <View style={{ width: CONTAINER_SPEC.horizontalGutter }} />
305
+ <ActionTrailingIcon configuration={trailingIcon} />
306
+ </>
307
+ ) : null}
308
+ </View>
309
+ </ActionContainer>
310
+ );
311
+ }
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
+ }