@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,982 @@
1
+ /**
2
+ * Modal Building Block — Item (Queue / Playlist)
3
+ *
4
+ * Single-file Expo Snack demo. Three item forms driven by configuration + data.
5
+ *
6
+ * Green (configurable): background, image, nowPlayingAsset, nowPlayingLabel,
7
+ * textLabel1, textLabel2
8
+ * White (hardcoded): container padding/gutters, nowPlaying container layout,
9
+ * button sizes/padding/assets
10
+ *
11
+ * Variants (VARIANT_*) are preset configuration bundles — not passed as a field.
12
+ */
13
+
14
+ import React from "react";
15
+ import { QBImage as Image } from "@applicaster/zapp-react-native-ui-components/Components/Image";
16
+ import { Platform, Pressable, StyleSheet, Text, View } from "react-native";
17
+ import {
18
+ MenuItem,
19
+ SelectionBehavior,
20
+ } from "@applicaster/zapp-react-native-utils/modalState/types";
21
+ import { useModalBlocksStyle } from "../../ModalBlocksStyleContext";
22
+ import { mergeItemConfiguration } from "../utils/mergeStyles";
23
+
24
+ const styles = StyleSheet.create({
25
+ row: {
26
+ flexDirection: "row",
27
+ alignItems: "center",
28
+ },
29
+ contentColumn: {
30
+ flex: 1,
31
+ justifyContent: "center",
32
+ },
33
+ buttonsRow: {
34
+ flexDirection: "row",
35
+ alignItems: "center",
36
+ },
37
+ });
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Configuration (green fields)
41
+ // ---------------------------------------------------------------------------
42
+
43
+ export type TextTransform =
44
+ | "default"
45
+ | "lowercase"
46
+ | "uppercase"
47
+ | "capitalize";
48
+
49
+ export type BackgroundConfiguration = {
50
+ defaultColor: string;
51
+ focusedColor: string;
52
+ cornerRadius: number;
53
+ paddingTop?: number;
54
+ paddingRight?: number;
55
+ paddingBottom?: number;
56
+ paddingLeft?: number;
57
+ };
58
+
59
+ export type ImageConfiguration = {
60
+ width: number;
61
+ height: number;
62
+ borderWidth: number;
63
+ borderColor: string;
64
+ cornerRadius: number;
65
+ };
66
+
67
+ export type LabelConfiguration = {
68
+ fontColor: string;
69
+ focusedFontColor?: string;
70
+ iosFontFamily: string;
71
+ androidFontFamily: string;
72
+ fontSize: number;
73
+ lineHeight: number;
74
+ iosLetterSpacing: number;
75
+ androidLetterSpacing: number;
76
+ textTransform: TextTransform;
77
+ };
78
+
79
+ export type NowPlayingAssetConfiguration = {
80
+ asset?: string;
81
+ };
82
+
83
+ export type CheckboxAssetsConfiguration = {
84
+ /** Unchecked multiSelect control (item.checkbox_asset). */
85
+ asset?: string;
86
+ /** Checked multiSelect control (item.checked_checkbox_asset). */
87
+ checkedAsset?: string;
88
+ };
89
+
90
+ export type ActionButtonAssetsConfiguration = {
91
+ /** Leading remove control on Edit Order rows. */
92
+ removeAsset?: string;
93
+ /** Trailing drag handle on Edit Order / queue rows. */
94
+ dragAsset?: string;
95
+ };
96
+
97
+ export type ItemConfiguration = {
98
+ background: BackgroundConfiguration;
99
+ image: ImageConfiguration;
100
+ nowPlayingAsset: NowPlayingAssetConfiguration;
101
+ nowPlayingLabel: LabelConfiguration;
102
+ textLabel1: LabelConfiguration;
103
+ textLabel2: LabelConfiguration;
104
+ checkboxAssets?: CheckboxAssetsConfiguration;
105
+ actionButtonAssets?: ActionButtonAssetsConfiguration;
106
+ };
107
+
108
+ export const DEFAULT_TEXT_LABEL_1_CONFIGURATION: LabelConfiguration = {
109
+ fontColor: "#FFFFFF",
110
+ focusedFontColor: "#FFFFFF",
111
+ iosFontFamily: "SFProText-Semibold",
112
+ androidFontFamily: "Roboto-Medium",
113
+ fontSize: 15,
114
+ lineHeight: 18,
115
+ iosLetterSpacing: -0.2,
116
+ androidLetterSpacing: 0,
117
+ textTransform: "default",
118
+ };
119
+
120
+ export const DEFAULT_TEXT_LABEL_2_CONFIGURATION: LabelConfiguration = {
121
+ fontColor: "#999999",
122
+ iosFontFamily: "SFProText-Medium",
123
+ androidFontFamily: "Roboto-Medium",
124
+ fontSize: 13,
125
+ lineHeight: 16,
126
+ iosLetterSpacing: -0.1,
127
+ androidLetterSpacing: 0,
128
+ textTransform: "default",
129
+ };
130
+
131
+ export const DEFAULT_NOW_PLAYING_LABEL_CONFIGURATION: LabelConfiguration = {
132
+ fontColor: "#FFFFFF",
133
+ iosFontFamily: "SFProText-Semibold",
134
+ androidFontFamily: "Roboto-Medium",
135
+ fontSize: 13,
136
+ lineHeight: 16,
137
+ iosLetterSpacing: 0,
138
+ androidLetterSpacing: 0,
139
+ textTransform: "default",
140
+ };
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // Data
144
+ // ---------------------------------------------------------------------------
145
+
146
+ export type ItemButtonAction = "playPause" | "drag" | "multiSelect" | "remove";
147
+
148
+ export type ItemData = {
149
+ imageUri?: string;
150
+ textLabel1: string;
151
+ textLabel2: string;
152
+ /** When set, shows the Now Playing row (equalizer + label). */
153
+ nowPlayingLabel?: string;
154
+ /** Button 2 (left most) — hardcoded action decides icon. */
155
+ leadingButton?: ItemButtonAction;
156
+ /** Button 1 (right most) — hardcoded action decides icon. */
157
+ trailingButton?: ItemButtonAction;
158
+ isSelected?: boolean;
159
+ };
160
+
161
+ export type AudioPlayerTrackItemProps = {
162
+ configuration: ItemConfiguration;
163
+ data: ItemData;
164
+ focused?: boolean;
165
+ focusedLeadingButton?: boolean;
166
+ focusedTrailingButton?: boolean;
167
+ onPress?: () => void;
168
+ onLeadingButtonPress?: () => void;
169
+ onTrailingButtonPress?: () => void;
170
+ renderHandle?: (element: React.ReactElement) => React.ReactElement;
171
+ };
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // Hardcoded (white fields)
175
+ // ---------------------------------------------------------------------------
176
+
177
+ type ContainerSpec = {
178
+ paddingTop: number;
179
+ paddingRight: number;
180
+ paddingBottom: number;
181
+ paddingLeft: number;
182
+ horizontalGutter: number;
183
+ verticalGutter: number;
184
+ };
185
+
186
+ type ButtonLayoutSpec = {
187
+ assetWidth: number;
188
+ assetHeight: number;
189
+ paddingTop: number;
190
+ paddingRight: number;
191
+ paddingBottom: number;
192
+ paddingLeft: number;
193
+ };
194
+
195
+ const CONTAINER_SPEC_LARGE_IMAGE: ContainerSpec = {
196
+ paddingTop: 8,
197
+ paddingRight: 20,
198
+ paddingBottom: 8,
199
+ paddingLeft: 20,
200
+ horizontalGutter: 12,
201
+ verticalGutter: 2,
202
+ };
203
+
204
+ const CONTAINER_SPEC_DEFAULT: ContainerSpec = {
205
+ paddingTop: 8,
206
+ paddingRight: 20,
207
+ paddingBottom: 8,
208
+ paddingLeft: 20,
209
+ horizontalGutter: 12,
210
+ verticalGutter: 2,
211
+ };
212
+
213
+ const BUTTONS_CONTAINER_HORIZONTAL_GUTTER = 4;
214
+
215
+ const NOW_PLAYING_CONTAINER_SPEC = {
216
+ horizontalGutter: 4,
217
+ marginTop: 0,
218
+ marginRight: 0,
219
+ marginBottom: 6,
220
+ marginLeft: 0,
221
+ };
222
+
223
+ const NOW_PLAYING_ASSET_SPEC = {
224
+ width: 16,
225
+ height: 16,
226
+ };
227
+
228
+ const BUTTON_LAYOUT_SPEC: Record<ItemButtonAction, ButtonLayoutSpec> = {
229
+ playPause: {
230
+ assetWidth: 44,
231
+ assetHeight: 44,
232
+ paddingTop: 0,
233
+ paddingRight: 0,
234
+ paddingBottom: 0,
235
+ paddingLeft: 0,
236
+ },
237
+ drag: {
238
+ assetWidth: 24,
239
+ assetHeight: 24,
240
+ paddingTop: 10,
241
+ paddingRight: 10,
242
+ paddingBottom: 10,
243
+ paddingLeft: 10,
244
+ },
245
+ multiSelect: {
246
+ assetWidth: 24,
247
+ assetHeight: 24,
248
+ paddingTop: 10,
249
+ paddingRight: 10,
250
+ paddingBottom: 10,
251
+ paddingLeft: 10,
252
+ },
253
+ remove: {
254
+ assetWidth: 24,
255
+ assetHeight: 24,
256
+ paddingTop: 10,
257
+ paddingRight: 10,
258
+ paddingBottom: 10,
259
+ paddingLeft: 10,
260
+ },
261
+ };
262
+
263
+ const DEFAULT_THUMB_ASSET =
264
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAQklEQVR42u3PMQ0AAAgDsEmcfzNggZekRwU0beezCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAhcLQVXaABdK6k6AAAAAElFTkSuQmCC";
265
+
266
+ const DEFAULT_EQUALIZER_ASSET =
267
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAI0lEQVR42mNgGNTgPxTQzwB0DVQ3gKCBowbQwYAhkBIHBAAA9TvPMeOFaLMAAAAASUVORK5CYII=";
268
+
269
+ function getTextTransformStyle(
270
+ textTransform: TextTransform
271
+ ): "none" | "lowercase" | "uppercase" | "capitalize" {
272
+ return textTransform === "default" ? "none" : textTransform;
273
+ }
274
+
275
+ function getFontFamily(ios: string, android: string) {
276
+ return Platform.select({ ios, android, default: android });
277
+ }
278
+
279
+ /** Hardcoded: large image (72) uses wider horizontal container padding. */
280
+ function resolveContainerSpec(image: ImageConfiguration): ContainerSpec {
281
+ return image.width >= 72
282
+ ? CONTAINER_SPEC_LARGE_IMAGE
283
+ : CONTAINER_SPEC_DEFAULT;
284
+ }
285
+
286
+ export function mapMenuItemToNowPlayingData(
287
+ item: MenuItem,
288
+ isPaused?: boolean,
289
+ nowPlayingLabel = "Now Playing"
290
+ ): ItemData {
291
+ return {
292
+ imageUri: item.icon,
293
+ textLabel1: item.title,
294
+ textLabel2: item.summary ?? "",
295
+ nowPlayingLabel,
296
+ trailingButton: "playPause",
297
+ isSelected: isPaused !== undefined ? !isPaused : item.isSelected,
298
+ };
299
+ }
300
+
301
+ export function mapMenuItemToQueuedData(item: MenuItem): ItemData {
302
+ return {
303
+ imageUri: item.icon,
304
+ textLabel1: item.title,
305
+ textLabel2: item.summary ?? "",
306
+ leadingButton: "remove",
307
+ trailingButton: "drag",
308
+ isSelected: item.isSelected,
309
+ };
310
+ }
311
+
312
+ export function mapMenuItemToPlaylistData(
313
+ item: MenuItem,
314
+ behavior?: SelectionBehavior
315
+ ): ItemData {
316
+ const showSelectionWidget =
317
+ behavior?.selectMode !== "none" && item.trailingButton !== "none";
318
+
319
+ const summary =
320
+ typeof item.summary === "string" ? item.summary.trim() : item.summary;
321
+
322
+ return {
323
+ imageUri: item.icon,
324
+ textLabel1: item.title,
325
+ textLabel2: summary || "",
326
+ trailingButton: showSelectionWidget
327
+ ? item.trailingButton || ("multiSelect" as ItemButtonAction)
328
+ : undefined,
329
+ isSelected: item.isSelected,
330
+ };
331
+ }
332
+
333
+ // ---------------------------------------------------------------------------
334
+ // Inner components
335
+ // ---------------------------------------------------------------------------
336
+
337
+ type ItemContainerProps = {
338
+ image: ImageConfiguration;
339
+ background: BackgroundConfiguration;
340
+ focused: boolean;
341
+ children: React.ReactNode;
342
+ onPress?: () => void;
343
+ };
344
+
345
+ function ItemContainer({
346
+ image,
347
+ background,
348
+ focused,
349
+ children,
350
+ onPress,
351
+ }: ItemContainerProps) {
352
+ const container = resolveContainerSpec(image);
353
+ const bg = focused ? background.focusedColor : background.defaultColor;
354
+
355
+ return (
356
+ <Pressable
357
+ onPress={onPress}
358
+ style={({ pressed }) => ({
359
+ backgroundColor: pressed ? background.focusedColor : bg,
360
+ borderRadius: background.cornerRadius,
361
+ paddingTop: background.paddingTop ?? container.paddingTop,
362
+ paddingRight: background.paddingRight ?? container.paddingRight,
363
+ paddingBottom: background.paddingBottom ?? container.paddingBottom,
364
+ paddingLeft: background.paddingLeft ?? container.paddingLeft,
365
+ })}
366
+ >
367
+ {children}
368
+ </Pressable>
369
+ );
370
+ }
371
+
372
+ type ItemThumbnailProps = {
373
+ configuration: ImageConfiguration;
374
+ imageUri?: string;
375
+ };
376
+
377
+ function ItemThumbnail({ configuration, imageUri }: ItemThumbnailProps) {
378
+ return (
379
+ <Image
380
+ source={{ uri: imageUri ?? DEFAULT_THUMB_ASSET }}
381
+ style={{
382
+ width: configuration.width,
383
+ height: configuration.height,
384
+ borderWidth: configuration.borderWidth,
385
+ borderColor: configuration.borderColor,
386
+ borderRadius: configuration.cornerRadius,
387
+ }}
388
+ resizeMode="cover"
389
+ />
390
+ );
391
+ }
392
+
393
+ type ItemLabelProps = {
394
+ text: string;
395
+ configuration: LabelConfiguration;
396
+ focused?: boolean;
397
+ };
398
+
399
+ function ItemLabel({ text, configuration, focused = false }: ItemLabelProps) {
400
+ if (!text) {
401
+ return null;
402
+ }
403
+
404
+ const color =
405
+ focused && configuration.focusedFontColor
406
+ ? configuration.focusedFontColor
407
+ : configuration.fontColor;
408
+
409
+ return (
410
+ <Text
411
+ style={{
412
+ color,
413
+ fontSize: configuration.fontSize,
414
+ lineHeight: configuration.lineHeight,
415
+ letterSpacing:
416
+ Platform.OS === "ios"
417
+ ? configuration.iosLetterSpacing
418
+ : configuration.androidLetterSpacing,
419
+ fontFamily: getFontFamily(
420
+ configuration.iosFontFamily,
421
+ configuration.androidFontFamily
422
+ ),
423
+ textTransform: getTextTransformStyle(configuration.textTransform),
424
+ }}
425
+ numberOfLines={1}
426
+ >
427
+ {text}
428
+ </Text>
429
+ );
430
+ }
431
+
432
+ type NowPlayingRowProps = {
433
+ assetConfiguration: NowPlayingAssetConfiguration;
434
+ labelConfiguration: LabelConfiguration;
435
+ label: string;
436
+ };
437
+
438
+ function NowPlayingRow({
439
+ assetConfiguration,
440
+ labelConfiguration,
441
+ label,
442
+ }: NowPlayingRowProps) {
443
+ const asset = assetConfiguration.asset ?? DEFAULT_EQUALIZER_ASSET;
444
+
445
+ return (
446
+ <View
447
+ style={{
448
+ flexDirection: "row",
449
+ alignItems: "center",
450
+ marginTop: NOW_PLAYING_CONTAINER_SPEC.marginTop,
451
+ marginRight: NOW_PLAYING_CONTAINER_SPEC.marginRight,
452
+ marginBottom: NOW_PLAYING_CONTAINER_SPEC.marginBottom,
453
+ marginLeft: NOW_PLAYING_CONTAINER_SPEC.marginLeft,
454
+ }}
455
+ >
456
+ <Image
457
+ source={{ uri: asset }}
458
+ style={{
459
+ width: NOW_PLAYING_ASSET_SPEC.width,
460
+ height: NOW_PLAYING_ASSET_SPEC.height,
461
+ }}
462
+ resizeMode="contain"
463
+ />
464
+ <View style={{ width: NOW_PLAYING_CONTAINER_SPEC.horizontalGutter }} />
465
+ <ItemLabel text={label} configuration={labelConfiguration} />
466
+ </View>
467
+ );
468
+ }
469
+
470
+ type ItemButtonProps = {
471
+ action: ItemButtonAction;
472
+ focused?: boolean;
473
+ onPress?: () => void;
474
+ isSelected?: boolean;
475
+ checkboxAssets?: CheckboxAssetsConfiguration;
476
+ actionButtonAssets?: ActionButtonAssetsConfiguration;
477
+ };
478
+
479
+ function ItemButton({
480
+ action,
481
+ focused = false,
482
+ onPress,
483
+ isSelected = false,
484
+ checkboxAssets,
485
+ actionButtonAssets,
486
+ }: ItemButtonProps) {
487
+ const layout = BUTTON_LAYOUT_SPEC[action];
488
+
489
+ if (!layout) {
490
+ return null;
491
+ }
492
+
493
+ const renderIcon = () => {
494
+ switch (action) {
495
+ case "playPause": {
496
+ const iconSize = layout.assetWidth;
497
+ const iconRadius = iconSize / 2;
498
+
499
+ return (
500
+ <View
501
+ style={{
502
+ width: iconSize,
503
+ height: iconSize,
504
+ borderRadius: iconRadius,
505
+ backgroundColor: "#FFFFFF",
506
+ justifyContent: "center",
507
+ alignItems: "center",
508
+ }}
509
+ >
510
+ {isSelected ? (
511
+ <View
512
+ style={{
513
+ flexDirection: "row",
514
+ justifyContent: "space-between",
515
+ width: 10,
516
+ height: 16,
517
+ }}
518
+ >
519
+ <View
520
+ style={{
521
+ width: 3.5,
522
+ height: 16,
523
+ backgroundColor: "#1e1e1e",
524
+ borderRadius: 1,
525
+ }}
526
+ />
527
+ <View
528
+ style={{
529
+ width: 3.5,
530
+ height: 16,
531
+ backgroundColor: "#1e1e1e",
532
+ borderRadius: 1,
533
+ }}
534
+ />
535
+ </View>
536
+ ) : (
537
+ <View
538
+ style={{
539
+ width: 0,
540
+ height: 0,
541
+ backgroundColor: "transparent",
542
+ borderStyle: "solid",
543
+ borderLeftWidth: 14,
544
+ borderRightWidth: 0,
545
+ borderTopWidth: 8,
546
+ borderBottomWidth: 8,
547
+ borderLeftColor: "#1e1e1e",
548
+ borderRightColor: "transparent",
549
+ borderTopColor: "transparent",
550
+ borderBottomColor: "transparent",
551
+ marginLeft: 3,
552
+ }}
553
+ />
554
+ )}
555
+ </View>
556
+ );
557
+ }
558
+
559
+ case "drag": {
560
+ if (actionButtonAssets?.dragAsset) {
561
+ return (
562
+ <Image
563
+ source={{ uri: actionButtonAssets.dragAsset }}
564
+ style={{
565
+ width: layout.assetWidth,
566
+ height: layout.assetHeight,
567
+ }}
568
+ resizeMode="contain"
569
+ />
570
+ );
571
+ }
572
+
573
+ return (
574
+ <View
575
+ style={{
576
+ width: 20,
577
+ height: 12,
578
+ justifyContent: "space-between",
579
+ }}
580
+ >
581
+ <View
582
+ style={{
583
+ height: 2,
584
+ backgroundColor: "rgba(255, 255, 255, 0.7)",
585
+ borderRadius: 1,
586
+ }}
587
+ />
588
+ <View
589
+ style={{
590
+ height: 2,
591
+ backgroundColor: "rgba(255, 255, 255, 0.7)",
592
+ borderRadius: 1,
593
+ }}
594
+ />
595
+ <View
596
+ style={{
597
+ height: 2,
598
+ backgroundColor: "rgba(255, 255, 255, 0.7)",
599
+ borderRadius: 1,
600
+ }}
601
+ />
602
+ </View>
603
+ );
604
+ }
605
+
606
+ case "remove": {
607
+ if (actionButtonAssets?.removeAsset) {
608
+ return (
609
+ <Image
610
+ source={{ uri: actionButtonAssets.removeAsset }}
611
+ style={{
612
+ width: layout.assetWidth,
613
+ height: layout.assetHeight,
614
+ }}
615
+ resizeMode="contain"
616
+ />
617
+ );
618
+ }
619
+
620
+ return (
621
+ <View
622
+ style={{
623
+ width: 24,
624
+ height: 24,
625
+ borderRadius: 12,
626
+ borderWidth: 1.5,
627
+ borderColor: "rgba(255, 255, 255, 0.7)",
628
+ justifyContent: "center",
629
+ alignItems: "center",
630
+ }}
631
+ >
632
+ <View
633
+ style={{
634
+ width: 10,
635
+ height: 1.5,
636
+ backgroundColor: "rgba(255, 255, 255, 0.7)",
637
+ borderRadius: 1,
638
+ }}
639
+ />
640
+ </View>
641
+ );
642
+ }
643
+
644
+ case "multiSelect": {
645
+ const studioAsset = isSelected
646
+ ? checkboxAssets?.checkedAsset
647
+ : checkboxAssets?.asset;
648
+
649
+ if (studioAsset) {
650
+ return (
651
+ <Image
652
+ source={{ uri: studioAsset }}
653
+ style={{
654
+ width: layout.assetWidth,
655
+ height: layout.assetHeight,
656
+ }}
657
+ resizeMode="contain"
658
+ />
659
+ );
660
+ }
661
+
662
+ if (!isSelected) {
663
+ return (
664
+ <View
665
+ style={{
666
+ width: layout.assetWidth,
667
+ height: layout.assetHeight,
668
+ borderRadius: layout.assetWidth / 2,
669
+ borderWidth: 1.5,
670
+ borderColor: "rgba(255, 255, 255, 0.7)",
671
+ backgroundColor: "transparent",
672
+ }}
673
+ />
674
+ );
675
+ }
676
+
677
+ return (
678
+ <View
679
+ style={{
680
+ width: 24,
681
+ height: 24,
682
+ borderRadius: 12,
683
+ backgroundColor: "#FFFFFF",
684
+ justifyContent: "center",
685
+ alignItems: "center",
686
+ }}
687
+ >
688
+ <View
689
+ style={{
690
+ width: 6,
691
+ height: 10,
692
+ borderBottomWidth: 2,
693
+ borderRightWidth: 2,
694
+ borderColor: "#1e1e1e",
695
+ transform: [{ rotate: "45deg" }],
696
+ marginTop: -2,
697
+ }}
698
+ />
699
+ </View>
700
+ );
701
+ }
702
+
703
+ default:
704
+ return null;
705
+ }
706
+ };
707
+
708
+ const buttonPadding = {
709
+ paddingTop: layout.paddingTop,
710
+ paddingRight: layout.paddingRight,
711
+ paddingBottom: layout.paddingBottom,
712
+ paddingLeft: layout.paddingLeft,
713
+ };
714
+
715
+ // A nested Pressable with no handler swallows the parent row press.
716
+ if (!onPress) {
717
+ return (
718
+ <View pointerEvents="none" style={buttonPadding}>
719
+ {renderIcon()}
720
+ </View>
721
+ );
722
+ }
723
+
724
+ return (
725
+ <Pressable
726
+ onPress={(e: any) => {
727
+ e?.stopPropagation?.();
728
+ onPress();
729
+ }}
730
+ style={({ pressed }) => [
731
+ buttonPadding,
732
+ {
733
+ opacity: pressed || focused ? 0.7 : 1,
734
+ },
735
+ ]}
736
+ >
737
+ {renderIcon()}
738
+ </Pressable>
739
+ );
740
+ }
741
+
742
+ type ItemButtonsProps = {
743
+ leadingButton?: ItemButtonAction;
744
+ trailingButton?: ItemButtonAction;
745
+ focusedLeadingButton?: boolean;
746
+ focusedTrailingButton?: boolean;
747
+ onLeadingButtonPress?: () => void;
748
+ onTrailingButtonPress?: () => void;
749
+ isSelected?: boolean;
750
+ renderHandle?: (element: React.ReactElement) => React.ReactElement;
751
+ checkboxAssets?: CheckboxAssetsConfiguration;
752
+ actionButtonAssets?: ActionButtonAssetsConfiguration;
753
+ };
754
+
755
+ function ItemButtons({
756
+ leadingButton,
757
+ trailingButton,
758
+ focusedLeadingButton = false,
759
+ focusedTrailingButton = false,
760
+ onLeadingButtonPress,
761
+ onTrailingButtonPress,
762
+ isSelected = false,
763
+ renderHandle,
764
+ checkboxAssets,
765
+ actionButtonAssets,
766
+ }: ItemButtonsProps) {
767
+ if (!leadingButton && !trailingButton) {
768
+ return null;
769
+ }
770
+
771
+ return (
772
+ <View style={styles.buttonsRow}>
773
+ {leadingButton ? (
774
+ <>
775
+ <ItemButton
776
+ action={leadingButton}
777
+ focused={focusedLeadingButton}
778
+ onPress={onLeadingButtonPress}
779
+ isSelected={isSelected}
780
+ checkboxAssets={checkboxAssets}
781
+ actionButtonAssets={actionButtonAssets}
782
+ />
783
+ <View style={{ width: BUTTONS_CONTAINER_HORIZONTAL_GUTTER }} />
784
+ </>
785
+ ) : null}
786
+ {trailingButton ? (
787
+ trailingButton === "drag" ? (
788
+ renderHandle ? (
789
+ renderHandle(
790
+ <ItemButton
791
+ action={trailingButton}
792
+ focused={focusedTrailingButton}
793
+ onPress={onTrailingButtonPress}
794
+ isSelected={isSelected}
795
+ checkboxAssets={checkboxAssets}
796
+ actionButtonAssets={actionButtonAssets}
797
+ />
798
+ )
799
+ ) : (
800
+ <ItemButton
801
+ action={trailingButton}
802
+ focused={focusedTrailingButton}
803
+ onPress={onTrailingButtonPress}
804
+ isSelected={isSelected}
805
+ checkboxAssets={checkboxAssets}
806
+ actionButtonAssets={actionButtonAssets}
807
+ />
808
+ )
809
+ ) : (
810
+ <ItemButton
811
+ action={trailingButton}
812
+ focused={focusedTrailingButton}
813
+ onPress={onTrailingButtonPress}
814
+ isSelected={isSelected}
815
+ checkboxAssets={checkboxAssets}
816
+ actionButtonAssets={actionButtonAssets}
817
+ />
818
+ )
819
+ ) : null}
820
+ </View>
821
+ );
822
+ }
823
+
824
+ // ---------------------------------------------------------------------------
825
+ // AudioPlayerTrackItem
826
+ // ---------------------------------------------------------------------------
827
+
828
+ export function AudioPlayerTrackItem({
829
+ configuration,
830
+ data,
831
+ focused = false,
832
+ focusedLeadingButton = false,
833
+ focusedTrailingButton = false,
834
+ onPress,
835
+ onLeadingButtonPress,
836
+ onTrailingButtonPress,
837
+ renderHandle,
838
+ }: AudioPlayerTrackItemProps) {
839
+ const modalBlocksStyle = useModalBlocksStyle();
840
+ const showNowPlaying = !!data.nowPlayingLabel;
841
+
842
+ const itemBlock =
843
+ showNowPlaying && modalBlocksStyle?.nowPlayingItem
844
+ ? modalBlocksStyle.nowPlayingItem
845
+ : modalBlocksStyle?.item;
846
+
847
+ const effectiveConfig = React.useMemo(() => {
848
+ return mergeItemConfiguration(configuration, itemBlock);
849
+ }, [configuration, itemBlock]);
850
+
851
+ const {
852
+ background,
853
+ image,
854
+ textLabel1,
855
+ textLabel2,
856
+ nowPlayingLabel,
857
+ nowPlayingAsset,
858
+ checkboxAssets,
859
+ actionButtonAssets,
860
+ } = effectiveConfig;
861
+
862
+ const container = resolveContainerSpec(image);
863
+
864
+ const handleTrailingButtonPress =
865
+ onTrailingButtonPress ??
866
+ (data.trailingButton === "multiSelect" ? onPress : undefined);
867
+
868
+ return (
869
+ <ItemContainer
870
+ image={image}
871
+ background={background}
872
+ focused={focused}
873
+ onPress={onPress}
874
+ >
875
+ <View style={styles.row}>
876
+ <ItemThumbnail configuration={image} imageUri={data.imageUri} />
877
+ <View style={{ width: container.horizontalGutter }} />
878
+ <View style={styles.contentColumn}>
879
+ {showNowPlaying ? (
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} />
901
+ ) : null}
902
+ </View>
903
+ <ItemButtons
904
+ leadingButton={data.leadingButton}
905
+ trailingButton={data.trailingButton}
906
+ focusedLeadingButton={focusedLeadingButton}
907
+ focusedTrailingButton={focusedTrailingButton}
908
+ onLeadingButtonPress={onLeadingButtonPress}
909
+ onTrailingButtonPress={handleTrailingButtonPress}
910
+ isSelected={data.isSelected}
911
+ renderHandle={renderHandle}
912
+ checkboxAssets={checkboxAssets}
913
+ actionButtonAssets={actionButtonAssets}
914
+ />
915
+ </View>
916
+ </ItemContainer>
917
+ );
918
+ }
919
+
920
+ // ---------------------------------------------------------------------------
921
+ // Variant presets — collections of configuration keys (not a variant field)
922
+ // ---------------------------------------------------------------------------
923
+
924
+ /** Variant 1 — Now Playing */
925
+ export const VARIANT_NOW_PLAYING: ItemConfiguration = {
926
+ background: {
927
+ defaultColor: "transparent",
928
+ focusedColor: "transparent",
929
+ cornerRadius: 0,
930
+ },
931
+ image: {
932
+ width: 72,
933
+ height: 72,
934
+ borderWidth: 1,
935
+ borderColor: "rgba(255, 255, 255, 0.1)",
936
+ cornerRadius: 8,
937
+ },
938
+ nowPlayingAsset: {},
939
+ nowPlayingLabel: DEFAULT_NOW_PLAYING_LABEL_CONFIGURATION,
940
+ textLabel1: DEFAULT_TEXT_LABEL_1_CONFIGURATION,
941
+ textLabel2: DEFAULT_TEXT_LABEL_2_CONFIGURATION,
942
+ };
943
+
944
+ /** Default / Variant 2 — Queued Item */
945
+ export const VARIANT_QUEUED_ITEM: ItemConfiguration = {
946
+ background: {
947
+ defaultColor: "transparent",
948
+ focusedColor: "rgba(62, 62, 62, 1)",
949
+ cornerRadius: 12,
950
+ },
951
+ image: {
952
+ width: 48,
953
+ height: 48,
954
+ borderWidth: 1,
955
+ borderColor: "rgba(255, 255, 255, 0.1)",
956
+ cornerRadius: 6,
957
+ },
958
+ nowPlayingAsset: {},
959
+ nowPlayingLabel: DEFAULT_NOW_PLAYING_LABEL_CONFIGURATION,
960
+ textLabel1: DEFAULT_TEXT_LABEL_1_CONFIGURATION,
961
+ textLabel2: DEFAULT_TEXT_LABEL_2_CONFIGURATION,
962
+ };
963
+
964
+ /** Variant 3 — Playlist Item */
965
+ export const VARIANT_PLAYLIST_ITEM: ItemConfiguration = {
966
+ background: {
967
+ defaultColor: "transparent",
968
+ focusedColor: "rgba(62, 62, 62, 1)",
969
+ cornerRadius: 12,
970
+ },
971
+ image: {
972
+ width: 48,
973
+ height: 48,
974
+ borderWidth: 1,
975
+ borderColor: "rgba(255, 255, 255, 0.1)",
976
+ cornerRadius: 6,
977
+ },
978
+ nowPlayingAsset: {},
979
+ nowPlayingLabel: DEFAULT_NOW_PLAYING_LABEL_CONFIGURATION,
980
+ textLabel1: DEFAULT_TEXT_LABEL_1_CONFIGURATION,
981
+ textLabel2: DEFAULT_TEXT_LABEL_2_CONFIGURATION,
982
+ };