@applicaster/zapp-react-native-ui-components 15.1.14 → 15.1.15-rc.0

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 (46) hide show
  1. package/Components/GeneralContentScreen/GeneralContentScreenHookAdapter.tsx +15 -0
  2. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +4 -1
  3. package/Components/ModalComponent/ActionsBottomSheet/ActionButton.tsx +56 -0
  4. package/Components/ModalComponent/ActionsBottomSheet/__tests__/openActionsBottomSheet.test.ts +38 -0
  5. package/Components/ModalComponent/ActionsBottomSheet/boundActionButton.tsx +24 -0
  6. package/Components/ModalComponent/ActionsBottomSheet/index.ts +9 -0
  7. package/Components/ModalComponent/ActionsBottomSheet/openActionsBottomSheet.ts +61 -0
  8. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +58 -97
  9. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +142 -142
  10. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +628 -0
  11. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +448 -175
  12. package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +94 -0
  13. package/Components/ModalComponent/AudioPlayer/Components/__tests__/Item.test.tsx +113 -0
  14. package/Components/ModalComponent/AudioPlayer/Components/index.ts +6 -4
  15. package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +248 -0
  16. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +386 -0
  17. package/Components/ModalComponent/BottomSheetModalContent.tsx +425 -66
  18. package/Components/ModalComponent/Button/index.tsx +5 -3
  19. package/Components/ModalComponent/Header/index.tsx +20 -7
  20. package/Components/ModalComponent/Header/utils.ts +1 -1
  21. package/Components/ModalComponent/ModalBlocksStyleContext.tsx +10 -0
  22. package/Components/ModalComponent/SortableList.tsx +157 -0
  23. package/Components/ModalComponent/SortableList.web.tsx +102 -0
  24. package/Components/ModalComponent/TextInputContent.tsx +112 -0
  25. package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +255 -0
  26. package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +156 -0
  27. package/Components/ModalComponent/__tests__/showTextInput.test.ts +95 -0
  28. package/Components/ModalComponent/index.tsx +18 -9
  29. package/Components/ModalComponent/presets/__tests__/getCell.test.ts +90 -0
  30. package/Components/ModalComponent/presets/dynamicCollectionCell.tsx +78 -0
  31. package/Components/ModalComponent/presets/index.ts +27 -0
  32. package/Components/ModalComponent/presets/selectableCell.tsx +28 -0
  33. package/Components/ModalComponent/presets/standardCell.tsx +41 -0
  34. package/Components/ModalComponent/presets/types.ts +35 -0
  35. package/Components/ModalComponent/showTextInput.ts +43 -0
  36. package/Components/ModalComponent/utils.ts +82 -21
  37. package/Components/River/ComponentsMap/ComponentsMap.tsx +87 -60
  38. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +139 -75
  39. package/Components/River/__tests__/componentsMap.test.js +8 -3
  40. package/Components/Screen/navigationHandler.ts +8 -8
  41. package/Contexts/ModalNavigationContext/__tests__/index.test.tsx +15 -0
  42. package/Contexts/ModalNavigationContext/index.tsx +2 -2
  43. package/Hooks/useEntryActionsExpander.ts +63 -0
  44. package/package.json +6 -5
  45. package/Components/ModalComponent/AudioPlayer/Components/Action.tsx +0 -368
  46. /package/Decorators/ZappPipesDataConnector/__tests__/{Hero.js → Hero.tsx} +0 -0
@@ -12,7 +12,14 @@
12
12
  */
13
13
 
14
14
  import React from "react";
15
- import { Image, Platform, StyleSheet, Text, View } from "react-native";
15
+ import { QBImage as Image } from "@applicaster/zapp-react-native-ui-components/Components/Image";
16
+ import { Platform, Pressable, StyleSheet, Text, View } from "react-native";
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";
16
23
 
17
24
  const styles = StyleSheet.create({
18
25
  row: {
@@ -27,25 +34,6 @@ const styles = StyleSheet.create({
27
34
  flexDirection: "row",
28
35
  alignItems: "center",
29
36
  },
30
- demoScreen: {
31
- flex: 1,
32
- backgroundColor: "#1C1C1E",
33
- paddingTop: 64,
34
- paddingHorizontal: 0,
35
- gap: 16,
36
- },
37
- demoHeading: {
38
- color: "#FFFFFF",
39
- fontSize: 20,
40
- fontWeight: "600",
41
- marginBottom: 8,
42
- paddingHorizontal: 16,
43
- },
44
- demoLabel: {
45
- color: "#999999",
46
- fontSize: 13,
47
- paddingHorizontal: 16,
48
- },
49
37
  });
50
38
 
51
39
  // ---------------------------------------------------------------------------
@@ -62,6 +50,10 @@ export type BackgroundConfiguration = {
62
50
  defaultColor: string;
63
51
  focusedColor: string;
64
52
  cornerRadius: number;
53
+ paddingTop?: number;
54
+ paddingRight?: number;
55
+ paddingBottom?: number;
56
+ paddingLeft?: number;
65
57
  };
66
58
 
67
59
  export type ImageConfiguration = {
@@ -88,6 +80,20 @@ export type NowPlayingAssetConfiguration = {
88
80
  asset?: string;
89
81
  };
90
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
+
91
97
  export type ItemConfiguration = {
92
98
  background: BackgroundConfiguration;
93
99
  image: ImageConfiguration;
@@ -95,52 +101,10 @@ export type ItemConfiguration = {
95
101
  nowPlayingLabel: LabelConfiguration;
96
102
  textLabel1: LabelConfiguration;
97
103
  textLabel2: LabelConfiguration;
104
+ checkboxAssets?: CheckboxAssetsConfiguration;
105
+ actionButtonAssets?: ActionButtonAssetsConfiguration;
98
106
  };
99
107
 
100
- export interface Action {
101
- type: string;
102
- payload?: any;
103
- options?: any;
104
- }
105
-
106
- export interface Header {
107
- title: string;
108
- subtitle?: string;
109
- icon?: string;
110
- }
111
-
112
- export interface Content {
113
- title: string;
114
- stickyTitle?: boolean;
115
- items: MenuItem[];
116
- itemsUrl?: string;
117
- action?: Action;
118
- }
119
-
120
- export interface Menu {
121
- header?: Header;
122
- content: Content;
123
- closeButton?: boolean;
124
- }
125
-
126
- export interface PresentSubMenuAction extends Action {
127
- menu: Menu;
128
- }
129
-
130
- export interface SecondaryAction {
131
- action: Action | PresentSubMenuAction;
132
- icon?: string;
133
- }
134
-
135
- export interface MenuItem {
136
- title: string;
137
- summary?: string;
138
- icon?: string;
139
- isSelected?: boolean;
140
- action?: Action | PresentSubMenuAction;
141
- secondaryAction?: SecondaryAction;
142
- }
143
-
144
108
  export const DEFAULT_TEXT_LABEL_1_CONFIGURATION: LabelConfiguration = {
145
109
  fontColor: "#FFFFFF",
146
110
  focusedFontColor: "#FFFFFF",
@@ -191,14 +155,19 @@ export type ItemData = {
191
155
  leadingButton?: ItemButtonAction;
192
156
  /** Button 1 (right most) — hardcoded action decides icon. */
193
157
  trailingButton?: ItemButtonAction;
158
+ isSelected?: boolean;
194
159
  };
195
160
 
196
- export type ItemProps = {
161
+ export type AudioPlayerTrackItemProps = {
197
162
  configuration: ItemConfiguration;
198
163
  data: ItemData;
199
164
  focused?: boolean;
200
165
  focusedLeadingButton?: boolean;
201
166
  focusedTrailingButton?: boolean;
167
+ onPress?: () => void;
168
+ onLeadingButtonPress?: () => void;
169
+ onTrailingButtonPress?: () => void;
170
+ renderHandle?: (element: React.ReactElement) => React.ReactElement;
202
171
  };
203
172
 
204
173
  // ---------------------------------------------------------------------------
@@ -234,9 +203,9 @@ const CONTAINER_SPEC_LARGE_IMAGE: ContainerSpec = {
234
203
 
235
204
  const CONTAINER_SPEC_DEFAULT: ContainerSpec = {
236
205
  paddingTop: 8,
237
- paddingRight: 10,
206
+ paddingRight: 20,
238
207
  paddingBottom: 8,
239
- paddingLeft: 10,
208
+ paddingLeft: 20,
240
209
  horizontalGutter: 12,
241
210
  verticalGutter: 2,
242
211
  };
@@ -297,16 +266,6 @@ const DEFAULT_THUMB_ASSET =
297
266
  const DEFAULT_EQUALIZER_ASSET =
298
267
  "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAI0lEQVR42mNgGNTgPxTQzwB0DVQ3gKCBowbQwYAhkBIHBAAA9TvPMeOFaLMAAAAASUVORK5CYII=";
299
268
 
300
- const BUTTON_ASSETS: Record<ItemButtonAction, string> = {
301
- playPause:
302
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAYAAAAehFoBAAAAdklEQVR42u3ZwRUAERAEUfkn3TaF5TFTrSuCf3CgjZGg6ast7G8YaAlcB8JAj8F1MRR2G63CUNglNAqsRvmB1TAfsBoXcMABV4MFKEci4IADNrkAed2H80R66SHqvUvglh/ktoZcL5H7MHKBx/5xYH+RsP90t5t2UpF6+22a+AAAAABJRU5ErkJggg==",
303
- drag: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAIklEQVR42mNgGAWjYGiC/2SCwWPBaByMxsFoHIyCUTDYAAAqhk7AVXdZJwAAAABJRU5ErkJggg==",
304
- multiSelect:
305
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAWklEQVR42t2VUQ4AIAhCvf+lrQsULJJZbH7GM1cS0Uk5dd0Qqcz4GJSCSs0pSCkAHNoWBfkTgIxpgGK+ujyEKABqRGyn9FO1A+yfrf+qsGxTSx5YEs2WyU9oAKBJKg8Kfq4dAAAAAElFTkSuQmCC",
306
- remove:
307
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAUklEQVR42mNgGPbgPx5AVYOJlSPJcGqrJU8DKXooCVui9FIacXj1UyNl4DWDLhYgc8nA+INpeFgwtCOZ5vmALjmZ5mURXUpTutQHdKnRRgEMAACwjWWprU1B+AAAAABJRU5ErkJggg==",
308
- };
309
-
310
269
  function getTextTransformStyle(
311
270
  textTransform: TextTransform
312
271
  ): "none" | "lowercase" | "uppercase" | "capitalize" {
@@ -324,32 +283,50 @@ function resolveContainerSpec(image: ImageConfiguration): ContainerSpec {
324
283
  : CONTAINER_SPEC_DEFAULT;
325
284
  }
326
285
 
327
- function mapMenuItemToNowPlayingData(item: MenuItem): ItemData {
286
+ export function mapMenuItemToNowPlayingData(
287
+ item: MenuItem,
288
+ isPaused?: boolean,
289
+ nowPlayingLabel = "Now Playing"
290
+ ): ItemData {
328
291
  return {
329
292
  imageUri: item.icon,
330
293
  textLabel1: item.title,
331
294
  textLabel2: item.summary ?? "",
332
- nowPlayingLabel: "Now Playing",
295
+ nowPlayingLabel,
333
296
  trailingButton: "playPause",
297
+ isSelected: isPaused !== undefined ? !isPaused : item.isSelected,
334
298
  };
335
299
  }
336
300
 
337
- function mapMenuItemToQueuedData(item: MenuItem): ItemData {
301
+ export function mapMenuItemToQueuedData(item: MenuItem): ItemData {
338
302
  return {
339
303
  imageUri: item.icon,
340
304
  textLabel1: item.title,
341
305
  textLabel2: item.summary ?? "",
342
306
  leadingButton: "remove",
343
307
  trailingButton: "drag",
308
+ isSelected: item.isSelected,
344
309
  };
345
310
  }
346
311
 
347
- function mapMenuItemToPlaylistData(item: MenuItem): ItemData {
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
+
348
322
  return {
349
323
  imageUri: item.icon,
350
324
  textLabel1: item.title,
351
- textLabel2: item.summary ?? "",
352
- trailingButton: "multiSelect",
325
+ textLabel2: summary || "",
326
+ trailingButton: showSelectionWidget
327
+ ? item.trailingButton || ("multiSelect" as ItemButtonAction)
328
+ : undefined,
329
+ isSelected: item.isSelected,
353
330
  };
354
331
  }
355
332
 
@@ -362,6 +339,7 @@ type ItemContainerProps = {
362
339
  background: BackgroundConfiguration;
363
340
  focused: boolean;
364
341
  children: React.ReactNode;
342
+ onPress?: () => void;
365
343
  };
366
344
 
367
345
  function ItemContainer({
@@ -369,23 +347,25 @@ function ItemContainer({
369
347
  background,
370
348
  focused,
371
349
  children,
350
+ onPress,
372
351
  }: ItemContainerProps) {
373
352
  const container = resolveContainerSpec(image);
374
353
  const bg = focused ? background.focusedColor : background.defaultColor;
375
354
 
376
355
  return (
377
- <View
378
- style={{
379
- backgroundColor: bg,
356
+ <Pressable
357
+ onPress={onPress}
358
+ style={({ pressed }) => ({
359
+ backgroundColor: pressed ? background.focusedColor : bg,
380
360
  borderRadius: background.cornerRadius,
381
- paddingTop: container.paddingTop,
382
- paddingRight: container.paddingRight,
383
- paddingBottom: container.paddingBottom,
384
- paddingLeft: container.paddingLeft,
385
- }}
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
+ })}
386
366
  >
387
367
  {children}
388
- </View>
368
+ </Pressable>
389
369
  );
390
370
  }
391
371
 
@@ -417,6 +397,10 @@ type ItemLabelProps = {
417
397
  };
418
398
 
419
399
  function ItemLabel({ text, configuration, focused = false }: ItemLabelProps) {
400
+ if (!text) {
401
+ return null;
402
+ }
403
+
420
404
  const color =
421
405
  focused && configuration.focusedFontColor
422
406
  ? configuration.focusedFontColor
@@ -486,27 +470,272 @@ function NowPlayingRow({
486
470
  type ItemButtonProps = {
487
471
  action: ItemButtonAction;
488
472
  focused?: boolean;
473
+ onPress?: () => void;
474
+ isSelected?: boolean;
475
+ checkboxAssets?: CheckboxAssetsConfiguration;
476
+ actionButtonAssets?: ActionButtonAssetsConfiguration;
489
477
  };
490
478
 
491
- function ItemButton({ action, focused = false }: ItemButtonProps) {
479
+ function ItemButton({
480
+ action,
481
+ focused = false,
482
+ onPress,
483
+ isSelected = false,
484
+ checkboxAssets,
485
+ actionButtonAssets,
486
+ }: ItemButtonProps) {
492
487
  const layout = BUTTON_LAYOUT_SPEC[action];
493
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
+
494
724
  return (
495
- <View
496
- style={{
497
- paddingTop: layout.paddingTop,
498
- paddingRight: layout.paddingRight,
499
- paddingBottom: layout.paddingBottom,
500
- paddingLeft: layout.paddingLeft,
501
- opacity: focused ? 0.7 : 1,
725
+ <Pressable
726
+ onPress={(e: any) => {
727
+ e?.stopPropagation?.();
728
+ onPress();
502
729
  }}
730
+ style={({ pressed }) => [
731
+ buttonPadding,
732
+ {
733
+ opacity: pressed || focused ? 0.7 : 1,
734
+ },
735
+ ]}
503
736
  >
504
- <Image
505
- source={{ uri: BUTTON_ASSETS[action] }}
506
- style={{ width: layout.assetWidth, height: layout.assetHeight }}
507
- resizeMode="contain"
508
- />
509
- </View>
737
+ {renderIcon()}
738
+ </Pressable>
510
739
  );
511
740
  }
512
741
 
@@ -515,6 +744,12 @@ type ItemButtonsProps = {
515
744
  trailingButton?: ItemButtonAction;
516
745
  focusedLeadingButton?: boolean;
517
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;
518
753
  };
519
754
 
520
755
  function ItemButtons({
@@ -522,6 +757,12 @@ function ItemButtons({
522
757
  trailingButton,
523
758
  focusedLeadingButton = false,
524
759
  focusedTrailingButton = false,
760
+ onLeadingButtonPress,
761
+ onTrailingButtonPress,
762
+ isSelected = false,
763
+ renderHandle,
764
+ checkboxAssets,
765
+ actionButtonAssets,
525
766
  }: ItemButtonsProps) {
526
767
  if (!leadingButton && !trailingButton) {
527
768
  return null;
@@ -531,69 +772,145 @@ function ItemButtons({
531
772
  <View style={styles.buttonsRow}>
532
773
  {leadingButton ? (
533
774
  <>
534
- <ItemButton action={leadingButton} focused={focusedLeadingButton} />
775
+ <ItemButton
776
+ action={leadingButton}
777
+ focused={focusedLeadingButton}
778
+ onPress={onLeadingButtonPress}
779
+ isSelected={isSelected}
780
+ checkboxAssets={checkboxAssets}
781
+ actionButtonAssets={actionButtonAssets}
782
+ />
535
783
  <View style={{ width: BUTTONS_CONTAINER_HORIZONTAL_GUTTER }} />
536
784
  </>
537
785
  ) : null}
538
786
  {trailingButton ? (
539
- <ItemButton action={trailingButton} focused={focusedTrailingButton} />
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
+ )
540
819
  ) : null}
541
820
  </View>
542
821
  );
543
822
  }
544
823
 
545
824
  // ---------------------------------------------------------------------------
546
- // Item
825
+ // AudioPlayerTrackItem
547
826
  // ---------------------------------------------------------------------------
548
827
 
549
- export function Item({
828
+ export function AudioPlayerTrackItem({
550
829
  configuration,
551
830
  data,
552
831
  focused = false,
553
832
  focusedLeadingButton = false,
554
833
  focusedTrailingButton = false,
555
- }: ItemProps) {
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
+
556
851
  const {
557
852
  background,
558
853
  image,
559
- nowPlayingAsset,
560
- nowPlayingLabel,
561
854
  textLabel1,
562
855
  textLabel2,
563
- } = configuration;
856
+ nowPlayingLabel,
857
+ nowPlayingAsset,
858
+ checkboxAssets,
859
+ actionButtonAssets,
860
+ } = effectiveConfig;
564
861
 
565
862
  const container = resolveContainerSpec(image);
566
- const showNowPlaying = !!data.nowPlayingLabel;
863
+
864
+ const handleTrailingButtonPress =
865
+ onTrailingButtonPress ??
866
+ (data.trailingButton === "multiSelect" ? onPress : undefined);
567
867
 
568
868
  return (
569
- <ItemContainer image={image} background={background} focused={focused}>
869
+ <ItemContainer
870
+ image={image}
871
+ background={background}
872
+ focused={focused}
873
+ onPress={onPress}
874
+ >
570
875
  <View style={styles.row}>
571
876
  <ItemThumbnail configuration={image} imageUri={data.imageUri} />
572
877
  <View style={{ width: container.horizontalGutter }} />
573
878
  <View style={styles.contentColumn}>
574
879
  {showNowPlaying ? (
575
- <>
576
- <NowPlayingRow
577
- assetConfiguration={nowPlayingAsset}
578
- labelConfiguration={nowPlayingLabel}
579
- label={data.nowPlayingLabel}
580
- />
581
- <View style={{ height: container.verticalGutter }} />
582
- </>
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} />
583
901
  ) : null}
584
- <ItemLabel
585
- text={data.textLabel1}
586
- configuration={textLabel1}
587
- focused={focused}
588
- />
589
- <View style={{ height: container.verticalGutter }} />
590
- <ItemLabel text={data.textLabel2} configuration={textLabel2} />
591
902
  </View>
592
903
  <ItemButtons
593
904
  leadingButton={data.leadingButton}
594
905
  trailingButton={data.trailingButton}
595
906
  focusedLeadingButton={focusedLeadingButton}
596
907
  focusedTrailingButton={focusedTrailingButton}
908
+ onLeadingButtonPress={onLeadingButtonPress}
909
+ onTrailingButtonPress={handleTrailingButtonPress}
910
+ isSelected={data.isSelected}
911
+ renderHandle={renderHandle}
912
+ checkboxAssets={checkboxAssets}
913
+ actionButtonAssets={actionButtonAssets}
597
914
  />
598
915
  </View>
599
916
  </ItemContainer>
@@ -663,47 +980,3 @@ export const VARIANT_PLAYLIST_ITEM: ItemConfiguration = {
663
980
  textLabel1: DEFAULT_TEXT_LABEL_1_CONFIGURATION,
664
981
  textLabel2: DEFAULT_TEXT_LABEL_2_CONFIGURATION,
665
982
  };
666
-
667
- export default function App() {
668
- // Hardcoded core mock data objects matching user types framework
669
- const sharedMockItem: MenuItem = {
670
- title: "Content Title",
671
- summary: "Artist",
672
- icon: "https://picsum.photos/200",
673
- action: { type: "PLAY_AUDIO" },
674
- secondaryAction: { action: { type: "OPEN_CONTEXT_MENU" } },
675
- };
676
-
677
- const playlistMockItem: MenuItem = {
678
- title: "Playlist Name",
679
- summary: "12 Songs",
680
- icon: "https://picsum.photos/201",
681
- isSelected: true,
682
- action: { type: "NAVIGATE_PLAYLIST" },
683
- };
684
-
685
- return (
686
- <View style={styles.demoScreen}>
687
- <Text style={styles.demoHeading}>Modal Item Building Block</Text>
688
-
689
- <Text style={styles.demoLabel}>Now Playing</Text>
690
- <Item
691
- configuration={VARIANT_NOW_PLAYING}
692
- data={mapMenuItemToNowPlayingData(sharedMockItem)}
693
- />
694
-
695
- <Text style={styles.demoLabel}>Queued Item</Text>
696
- <Item
697
- configuration={VARIANT_QUEUED_ITEM}
698
- data={mapMenuItemToQueuedData(sharedMockItem)}
699
- focused
700
- />
701
-
702
- <Text style={styles.demoLabel}>Playlist Item</Text>
703
- <Item
704
- configuration={VARIANT_PLAYLIST_ITEM}
705
- data={mapMenuItemToPlaylistData(playlistMockItem)}
706
- />
707
- </View>
708
- );
709
- }