@applicaster/zapp-react-native-ui-components 16.0.0-alpha.6593152532 → 16.0.0-alpha.6918106967

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