@applicaster/zapp-react-native-ui-components 16.0.0-rc.8 → 16.0.0-rc.81

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 (132) 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 +39 -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 +16 -5
  26. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +47 -21
  27. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +34 -6
  28. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/__tests__/withDimensions.test.ts +420 -125
  29. package/Components/MasterCell/DefaultComponents/Image/hoc/utils/index.ts +114 -19
  30. package/Components/MasterCell/DefaultComponents/Image/hoc/withDimensions.tsx +19 -8
  31. package/Components/MasterCell/DefaultComponents/ImageContainer/index.tsx +5 -3
  32. package/Components/MasterCell/DefaultComponents/PressableView.tsx +29 -9
  33. package/Components/MasterCell/DefaultComponents/Text/hooks/useText.ts +4 -0
  34. package/Components/MasterCell/DefaultComponents/Toggle.tsx +36 -14
  35. package/Components/MasterCell/DefaultComponents/__tests__/Toggle.test.tsx +167 -0
  36. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +154 -100
  37. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/AssetComponent.tsx +15 -4
  38. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +56 -22
  39. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +6 -4
  40. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +3 -1
  41. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +32 -21
  42. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +15 -4
  43. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +23 -16
  44. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +8 -3
  45. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +2 -2
  46. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/insertButtonsBetweenLabels.test.ts +45 -13
  47. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +2 -2
  48. package/Components/MasterCell/contexts/PressedStateContext.ts +3 -0
  49. package/Components/MasterCell/dataAdapter.ts +15 -5
  50. package/Components/MasterCell/hooks/index.ts +2 -0
  51. package/Components/MasterCell/hooks/usePressedState.ts +4 -0
  52. package/Components/MasterCell/index.tsx +30 -5
  53. package/Components/MasterCell/utils/__tests__/resolveColor.test.js +169 -132
  54. package/Components/MasterCell/utils/__tests__/resolveColorForProp.test.js +92 -0
  55. package/Components/MasterCell/utils/index.ts +85 -46
  56. package/Components/ModalComponent/ActionsBottomSheet/ActionButton.tsx +54 -0
  57. package/Components/ModalComponent/ActionsBottomSheet/boundActionButton.tsx +21 -0
  58. package/Components/ModalComponent/ActionsBottomSheet/index.ts +9 -0
  59. package/Components/ModalComponent/ActionsBottomSheet/openActionsBottomSheet.ts +58 -0
  60. package/Components/ModalComponent/AudioPlayer/Components/Action.tsx +314 -0
  61. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +407 -0
  62. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +763 -0
  63. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +653 -0
  64. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +960 -0
  65. package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +89 -0
  66. package/Components/ModalComponent/AudioPlayer/Components/index.ts +11 -0
  67. package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +230 -0
  68. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +332 -0
  69. package/Components/ModalComponent/BottomSheetDragArea.tsx +33 -0
  70. package/Components/ModalComponent/BottomSheetModalContent.tsx +383 -42
  71. package/Components/ModalComponent/Button/index.tsx +5 -3
  72. package/Components/ModalComponent/Header/index.tsx +20 -7
  73. package/Components/ModalComponent/Header/utils.ts +1 -1
  74. package/Components/ModalComponent/ModalBlocksStyleContext.tsx +10 -0
  75. package/Components/ModalComponent/SortableList.tsx +142 -0
  76. package/Components/ModalComponent/SortableList.web.tsx +34 -0
  77. package/Components/ModalComponent/TextInputContent.tsx +100 -0
  78. package/Components/ModalComponent/__tests__/BottomSheetDragArea.test.tsx +74 -0
  79. package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +238 -0
  80. package/Components/ModalComponent/__tests__/SortableList.web.test.tsx +16 -0
  81. package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +120 -0
  82. package/Components/ModalComponent/__tests__/showTextInput.test.ts +95 -0
  83. package/Components/ModalComponent/index.tsx +11 -0
  84. package/Components/ModalComponent/presets/__tests__/getCell.test.ts +66 -0
  85. package/Components/ModalComponent/presets/dynamicCollectionCell.tsx +78 -0
  86. package/Components/ModalComponent/presets/index.ts +27 -0
  87. package/Components/ModalComponent/presets/selectableCell.tsx +28 -0
  88. package/Components/ModalComponent/presets/standardCell.tsx +41 -0
  89. package/Components/ModalComponent/presets/types.ts +35 -0
  90. package/Components/ModalComponent/showTextInput.ts +43 -0
  91. package/Components/ModalComponent/utils.ts +82 -21
  92. package/Components/OfflineHandler/__tests__/__snapshots__/index.test.tsx.snap +4 -85
  93. package/Components/OfflineHandler/__tests__/index.test.tsx +78 -18
  94. package/Components/OfflineHandler/hooks.ts +111 -0
  95. package/Components/OfflineHandler/index.tsx +51 -73
  96. package/Components/OfflineHandler/utils/index.ts +1 -13
  97. package/Components/PlayerContainer/PlayerContainer.tsx +25 -17
  98. package/Components/PlayerContainer/ProgramInfo/index.tsx +3 -4
  99. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +337 -0
  100. package/Components/PreloaderWrapper/index.tsx +1 -1
  101. package/Components/River/ComponentsMap/ComponentsMap.tsx +2 -2
  102. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +3 -15
  103. package/Components/Screen/index.tsx +8 -4
  104. package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +5 -3
  105. package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +1 -1
  106. package/Components/ScreenRevealManager/ScreenRevealManager.ts +18 -2
  107. package/Components/ScreenRevealManager/__tests__/ScreenRevealManager.test.ts +36 -0
  108. package/Components/ScreenRevealManager/__tests__/withScreenRevealManager.test.tsx +56 -0
  109. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +20 -3
  110. package/Components/TopCutoffOverlay/index.tsx +2 -2
  111. package/Components/Transitioner/Transitioner.tsx +28 -17
  112. package/Components/VideoLive/LiveImageManager.ts +1 -1
  113. package/Components/VideoModal/utils.ts +6 -1
  114. package/Components/ZappFrameworkComponents/BarView/BarView.tsx +12 -5
  115. package/Components/ZappFrameworkComponents/BarView/__tests__/BarView.test.tsx +2 -2
  116. package/Contexts/CachedDimensionsContext/__tests__/index.test.ts +154 -0
  117. package/Contexts/CachedDimensionsContext/index.ts +17 -2
  118. package/Helpers/ComponentCellSelectionHelper/index.js +0 -6
  119. package/Helpers/index.js +0 -39
  120. package/Hooks/useEntryActionsExpander.ts +63 -0
  121. package/package.json +6 -5
  122. package/Components/OfflineHandler/NotificationView/NotificationView.lg.tsx +0 -112
  123. package/Components/OfflineHandler/NotificationView/NotificationView.samsung.tsx +0 -107
  124. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +0 -153
  125. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +0 -66
  126. package/Components/OfflineHandler/NotificationView/utils.ts +0 -34
  127. package/Components/Screen/utils.ts +0 -16
  128. package/Helpers/Analytics/index.js +0 -95
  129. /package/Components/BackgroundImage/{BackgroundImage.tv.android.tsx → BackgroundImage.android.tv.tsx} +0 -0
  130. /package/Components/BackgroundImage/{BackgroundImage.tv.ios.tsx → BackgroundImage.ios.tv.tsx} +0 -0
  131. /package/Components/MasterCell/DefaultComponents/ButtonContainerView/{index.tv.android.tsx → index.android.tv.tsx} +0 -0
  132. /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
+ };