@applicaster/zapp-react-native-ui-components 16.0.0-rc.4 → 16.0.0-rc.41

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 (100) hide show
  1. package/Components/BackgroundImage/BackgroundImage.android.tv.tsx +28 -0
  2. package/Components/BackgroundImage/BackgroundImage.ios.tv.tsx +24 -0
  3. package/Components/BackgroundImage/BackgroundImage.tsx +42 -0
  4. package/Components/BackgroundImage/index.ts +1 -0
  5. package/Components/CellRendererResolver/index.ts +2 -2
  6. package/Components/ComponentResolver/__tests__/componentResolver.test.js +1 -1
  7. package/Components/FocusableGroup/FocusableTvOS.tsx +11 -7
  8. package/Components/GeneralContentScreen/GeneralContentScreenHookAdapter.tsx +39 -0
  9. package/Components/GeneralContentScreen/__tests__/GeneralContentScreenHookAdapter.test.tsx +64 -0
  10. package/Components/GeneralContentScreen/__tests__/HookContentFocusGroup.web.test.tsx +91 -0
  11. package/Components/GeneralContentScreen/hookAdapter/__tests__/networkService.test.ts +74 -0
  12. package/Components/GeneralContentScreen/hookAdapter/__tests__/runInBackground.test.ts +139 -0
  13. package/Components/GeneralContentScreen/hookAdapter/__tests__/validationHelper.test.ts +124 -0
  14. package/Components/GeneralContentScreen/hookAdapter/logger.ts +6 -0
  15. package/Components/GeneralContentScreen/hookAdapter/networkService.ts +53 -0
  16. package/Components/GeneralContentScreen/hookAdapter/runInBackground.ts +48 -0
  17. package/Components/GeneralContentScreen/hookAdapter/validationHelper.ts +72 -0
  18. package/Components/GeneralContentScreen/hookFocus/index.tsx +13 -0
  19. package/Components/GeneralContentScreen/hookFocus/index.web.tsx +69 -0
  20. package/Components/GeneralContentScreen/index.ts +2 -0
  21. package/Components/Layout/FullWidthRow.tsx +38 -0
  22. package/Components/Layout/TV/ScreenContainer.tsx +5 -0
  23. package/Components/Layout/TV/__tests__/__snapshots__/index.test.tsx.snap +0 -1
  24. package/Components/Layout/TV/index.tsx +3 -4
  25. package/Components/Layout/TV/index.web.tsx +2 -3
  26. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +16 -5
  27. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/__tests__/placement.test.ts +47 -21
  28. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/placement.ts +34 -6
  29. package/Components/MasterCell/DefaultComponents/ButtonContainerView/index.tsx +1 -1
  30. package/Components/MasterCell/DefaultComponents/ImageContainer/index.tsx +5 -3
  31. package/Components/MasterCell/DefaultComponents/PressableView.tsx +29 -9
  32. package/Components/MasterCell/DefaultComponents/Text/hooks/useText.ts +4 -0
  33. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +154 -100
  34. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/AssetComponent.tsx +15 -4
  35. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Button.ts +56 -22
  36. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/Spacer.ts +6 -4
  37. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/TextLabelsContainer.ts +3 -1
  38. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +32 -21
  39. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/index.test.ts +15 -4
  40. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/index.ts +23 -16
  41. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/__tests__/insertButtons.test.ts +8 -3
  42. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/utils/index.ts +2 -2
  43. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/insertButtonsBetweenLabels.test.ts +45 -13
  44. package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +2 -2
  45. package/Components/MasterCell/contexts/PressedStateContext.ts +3 -0
  46. package/Components/MasterCell/dataAdapter.ts +15 -5
  47. package/Components/MasterCell/hooks/index.ts +2 -0
  48. package/Components/MasterCell/hooks/usePressedState.ts +4 -0
  49. package/Components/MasterCell/index.tsx +30 -5
  50. package/Components/MasterCell/utils/__tests__/resolveColor.test.js +169 -132
  51. package/Components/MasterCell/utils/__tests__/resolveColorForProp.test.js +92 -0
  52. package/Components/MasterCell/utils/index.ts +85 -46
  53. package/Components/ModalComponent/AudioPlayer/Components/Action.tsx +332 -0
  54. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +402 -0
  55. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +811 -0
  56. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +671 -0
  57. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +878 -0
  58. package/Components/ModalComponent/AudioPlayer/Components/index.ts +9 -0
  59. package/Components/ModalComponent/BottomSheetModalContent.tsx +34 -18
  60. package/Components/OfflineHandler/__tests__/__snapshots__/index.test.tsx.snap +4 -85
  61. package/Components/OfflineHandler/__tests__/index.test.tsx +78 -18
  62. package/Components/OfflineHandler/hooks.ts +123 -0
  63. package/Components/OfflineHandler/index.tsx +51 -73
  64. package/Components/OfflineHandler/utils/index.ts +1 -13
  65. package/Components/PlayerContainer/PlayerContainer.tsx +14 -10
  66. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +284 -0
  67. package/Components/PreloaderWrapper/index.tsx +1 -1
  68. package/Components/River/ComponentsMap/ComponentsMap.tsx +2 -2
  69. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +3 -15
  70. package/Components/Screen/TV/hooks/__tests__/useAfterPaint.test.ts +60 -0
  71. package/Components/Screen/TV/hooks/index.ts +2 -0
  72. package/Components/Screen/TV/hooks/useAfterPaint.ts +23 -0
  73. package/Components/Screen/TV/index.web.tsx +16 -7
  74. package/Components/Screen/index.tsx +8 -4
  75. package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +5 -3
  76. package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +1 -1
  77. package/Components/ScreenRevealManager/Overlay.tsx +34 -0
  78. package/Components/ScreenRevealManager/__tests__/Overlay.test.tsx +88 -0
  79. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +8 -19
  80. package/Components/TopCutoffOverlay/index.tsx +2 -2
  81. package/Components/Transitioner/Transitioner.tsx +28 -17
  82. package/Components/VideoLive/LiveImageManager.ts +56 -45
  83. package/Components/VideoLive/PlayerLiveImageComponent.tsx +4 -2
  84. package/Components/VideoModal/utils.ts +6 -1
  85. package/Components/ZappFrameworkComponents/BarView/BarView.tsx +12 -5
  86. package/Components/ZappFrameworkComponents/BarView/__tests__/BarView.test.tsx +2 -2
  87. package/Contexts/CachedDimensionsContext/__tests__/index.test.ts +154 -0
  88. package/Contexts/CachedDimensionsContext/index.ts +17 -2
  89. package/Helpers/ComponentCellSelectionHelper/index.js +0 -6
  90. package/Helpers/index.js +7 -40
  91. package/package.json +5 -5
  92. package/Components/Layout/TV/LayoutBackground.tsx +0 -31
  93. package/Components/OfflineHandler/NotificationView/NotificationView.lg.tsx +0 -112
  94. package/Components/OfflineHandler/NotificationView/NotificationView.samsung.tsx +0 -107
  95. package/Components/OfflineHandler/NotificationView/NotificationView.tsx +0 -153
  96. package/Components/OfflineHandler/NotificationView/__tests__/index.test.tsx +0 -66
  97. package/Components/OfflineHandler/NotificationView/utils.ts +0 -34
  98. package/Components/Screen/utils.ts +0 -16
  99. package/Helpers/Analytics/index.js +0 -95
  100. /package/Components/MasterCell/DefaultComponents/ButtonContainerView/{index.tv.android.tsx → index.android.tv.tsx} +0 -0
@@ -0,0 +1,811 @@
1
+ /**
2
+ * Modal Building Block — Header
3
+ *
4
+ * Single-file Expo Snack demo for the Header building block spec.
5
+ *
6
+ * Green (configurable): title, closeButton (enable + asset), actionButtons styling
7
+ * White (hardcoded): container, action button layout/enable/actions, close dimensions,
8
+ * action button padding/radius, action labels
9
+ */
10
+
11
+ import React, { useCallback, useState } from "react";
12
+ import { Platform, Pressable, StyleSheet, Text, View } from "react-native";
13
+
14
+ const styles = StyleSheet.create({
15
+ row: {
16
+ flexDirection: "row",
17
+ alignItems: "center",
18
+ },
19
+ centerAlignedRow: {
20
+ flexDirection: "row",
21
+ alignItems: "center",
22
+ minHeight: 44,
23
+ position: "relative",
24
+ },
25
+ measureLayer: {
26
+ position: "absolute",
27
+ opacity: 0,
28
+ left: 0,
29
+ top: 0,
30
+ flexDirection: "row",
31
+ },
32
+ sideSlotFixed: {
33
+ justifyContent: "center",
34
+ minHeight: 44,
35
+ },
36
+ centerSpacer: {
37
+ flex: 1,
38
+ },
39
+ absoluteTitleLayer: {
40
+ ...StyleSheet.absoluteFillObject,
41
+ alignItems: "center",
42
+ justifyContent: "center",
43
+ },
44
+ closeButton: {
45
+ alignItems: "center",
46
+ justifyContent: "center",
47
+ },
48
+ demoScreen: {
49
+ flex: 1,
50
+ backgroundColor: "#1C1C1E",
51
+ paddingTop: 64,
52
+ paddingHorizontal: 0,
53
+ gap: 24,
54
+ },
55
+ demoBlock: {
56
+ gap: 8,
57
+ paddingHorizontal: 0,
58
+ },
59
+ demoHeading: {
60
+ color: "#FFFFFF",
61
+ fontSize: 20,
62
+ fontWeight: "600",
63
+ marginBottom: 8,
64
+ paddingHorizontal: 24,
65
+ },
66
+ demoLabel: {
67
+ color: "#999999",
68
+ fontSize: 13,
69
+ paddingHorizontal: 24,
70
+ },
71
+ });
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // Configuration (green / editable fields from spec)
75
+ // ---------------------------------------------------------------------------
76
+
77
+ export type HeaderActionType = "cancel" | "done" | "clear";
78
+
79
+ export type TextTransform =
80
+ | "default"
81
+ | "lowercase"
82
+ | "uppercase"
83
+ | "capitalize";
84
+
85
+ export type TitleAlignment = "left" | "center";
86
+
87
+ /** Selects hardcoded container + action layout (white fields). */
88
+ export type HeaderVariant =
89
+ | "singleLine"
90
+ | "headerWithActions"
91
+ | "headerWithActionsCancelOnly"
92
+ | "section";
93
+
94
+ export type TitleConfiguration = {
95
+ fontColor: string;
96
+ iosFontFamily: string;
97
+ androidFontFamily: string;
98
+ fontSize: number;
99
+ lineHeight: number;
100
+ iosLetterSpacing: number;
101
+ androidLetterSpacing: number;
102
+ textTransform: TextTransform;
103
+ alignment: TitleAlignment;
104
+ };
105
+
106
+ export type CloseButtonConfiguration = {
107
+ enabled: boolean;
108
+ asset?: string;
109
+ };
110
+
111
+ export type ActionButtonsStyleConfiguration = {
112
+ defaultBackgroundColor: string;
113
+ focusedBackgroundColor: string;
114
+ fontColor: string;
115
+ focusedFontColor: string;
116
+ iosFontFamily: string;
117
+ androidFontFamily: string;
118
+ fontSize: number;
119
+ lineHeight: number;
120
+ iosLetterSpacing: number;
121
+ androidLetterSpacing: number;
122
+ textTransform: TextTransform;
123
+ };
124
+
125
+ export type HeaderConfiguration = {
126
+ paddingTop: undefined;
127
+ variant: HeaderVariant;
128
+ title: TitleConfiguration;
129
+ closeButton: CloseButtonConfiguration;
130
+ actionButtons: ActionButtonsStyleConfiguration;
131
+ };
132
+
133
+ export const DEFAULT_TITLE_CONFIGURATION: TitleConfiguration = {
134
+ fontColor: "#FFFFFF",
135
+ iosFontFamily: "SFProText-Bold",
136
+ androidFontFamily: "Roboto-Bold",
137
+ fontSize: 22,
138
+ lineHeight: 28,
139
+ iosLetterSpacing: -0.6,
140
+ androidLetterSpacing: 0,
141
+ textTransform: "default",
142
+ alignment: "left",
143
+ };
144
+
145
+ export const DEFAULT_CLOSE_BUTTON_CONFIGURATION: CloseButtonConfiguration = {
146
+ enabled: true,
147
+ };
148
+
149
+ export const DEFAULT_ACTION_BUTTONS_STYLE_CONFIGURATION: ActionButtonsStyleConfiguration =
150
+ {
151
+ defaultBackgroundColor: "rgba(62, 62, 62, 1)",
152
+ focusedBackgroundColor: "rgba(78, 78, 78, 1)",
153
+ fontColor: "#FFFFFF",
154
+ focusedFontColor: "#FFFFFF",
155
+ iosFontFamily: "SFProText-Semibold",
156
+ androidFontFamily: "Roboto-Medium",
157
+ fontSize: 14,
158
+ lineHeight: 20,
159
+ iosLetterSpacing: 0,
160
+ androidLetterSpacing: 0,
161
+ textTransform: "default",
162
+ };
163
+
164
+ // ---------------------------------------------------------------------------
165
+ // Data (runtime / localization — not styling config)
166
+ // ---------------------------------------------------------------------------
167
+
168
+ export type HeaderData = {
169
+ title: string;
170
+ };
171
+
172
+ export type AudioPlayerHeaderProps = {
173
+ configuration: HeaderConfiguration;
174
+ data: HeaderData;
175
+ focusedCloseButton?: boolean;
176
+ focusedLeadingAction?: boolean;
177
+ focusedTrailingAction?: boolean;
178
+ onClose?: () => void;
179
+ onBack?: () => void;
180
+ onCancel?: () => void;
181
+ onDone?: () => void;
182
+ onClear?: () => void;
183
+ };
184
+
185
+ // ---------------------------------------------------------------------------
186
+ // Hardcoded spec values (white fields)
187
+ // ---------------------------------------------------------------------------
188
+
189
+ type ContainerSpec = {
190
+ horizontalGutter: number;
191
+ paddingTop: number;
192
+ paddingRight: number;
193
+ paddingBottom: number;
194
+ paddingLeft: number;
195
+ };
196
+
197
+ type ActionSlotSpec = {
198
+ enabled: boolean;
199
+ actionType?: HeaderActionType;
200
+ };
201
+
202
+ type ActionButtonsBehaviorSpec = {
203
+ sectionEnabled: boolean;
204
+ leading: ActionSlotSpec;
205
+ trailing: ActionSlotSpec;
206
+ };
207
+
208
+ const CONTAINER_SPEC: Record<HeaderVariant, ContainerSpec> = {
209
+ singleLine: {
210
+ horizontalGutter: 16,
211
+ paddingTop: 24,
212
+ paddingRight: 20,
213
+ paddingBottom: 12,
214
+ paddingLeft: 20,
215
+ },
216
+ headerWithActions: {
217
+ horizontalGutter: 12,
218
+ paddingTop: 0,
219
+ paddingRight: 20,
220
+ paddingBottom: 8,
221
+ paddingLeft: 20,
222
+ },
223
+ headerWithActionsCancelOnly: {
224
+ horizontalGutter: 12,
225
+ paddingTop: 0,
226
+ paddingRight: 20,
227
+ paddingBottom: 8,
228
+ paddingLeft: 20,
229
+ },
230
+ section: {
231
+ horizontalGutter: 12,
232
+ paddingTop: 0,
233
+ paddingRight: 20,
234
+ paddingBottom: 0,
235
+ paddingLeft: 20,
236
+ },
237
+ };
238
+
239
+ const ACTION_BUTTONS_BEHAVIOR: Record<
240
+ HeaderVariant,
241
+ ActionButtonsBehaviorSpec
242
+ > = {
243
+ singleLine: {
244
+ sectionEnabled: false,
245
+ leading: { enabled: false },
246
+ trailing: { enabled: false },
247
+ },
248
+ headerWithActions: {
249
+ sectionEnabled: true,
250
+ leading: { enabled: true, actionType: "cancel" },
251
+ trailing: { enabled: true, actionType: "done" },
252
+ },
253
+ headerWithActionsCancelOnly: {
254
+ sectionEnabled: true,
255
+ leading: { enabled: true, actionType: "cancel" },
256
+ trailing: { enabled: false },
257
+ },
258
+ section: {
259
+ sectionEnabled: true,
260
+ leading: { enabled: false },
261
+ trailing: { enabled: true, actionType: "clear" },
262
+ },
263
+ };
264
+
265
+ const CLOSE_BUTTON_SPEC = {
266
+ width: 44,
267
+ height: 44,
268
+ paddingTop: 0,
269
+ paddingRight: 0,
270
+ paddingBottom: 0,
271
+ paddingLeft: 0,
272
+ };
273
+
274
+ const ACTION_BUTTON_LAYOUT_SPEC = {
275
+ paddingTop: 6,
276
+ paddingRight: 10,
277
+ paddingBottom: 6,
278
+ paddingLeft: 10,
279
+ cornerRadius: 16,
280
+ };
281
+
282
+ const ACTION_LABELS: Record<HeaderActionType, string> = {
283
+ cancel: "Cancel",
284
+ done: "Done",
285
+ clear: "Clear",
286
+ };
287
+
288
+ function getTextTransformStyle(
289
+ textTransform: TextTransform
290
+ ): "none" | "lowercase" | "uppercase" | "capitalize" {
291
+ return textTransform === "default" ? "none" : textTransform;
292
+ }
293
+
294
+ function shouldShowActionSlot(slot: ActionSlotSpec, sectionEnabled: boolean) {
295
+ return sectionEnabled && slot.enabled && !!slot.actionType;
296
+ }
297
+
298
+ // ---------------------------------------------------------------------------
299
+ // Inner components
300
+ // ---------------------------------------------------------------------------
301
+
302
+ type HeaderContainerProps = {
303
+ variant: HeaderVariant;
304
+ configuration: HeaderConfiguration;
305
+ children: React.ReactNode;
306
+ };
307
+
308
+ function HeaderContainer({
309
+ variant,
310
+ configuration,
311
+ children,
312
+ }: HeaderContainerProps) {
313
+ const container = CONTAINER_SPEC[variant];
314
+
315
+ const paddingTop =
316
+ configuration.paddingTop !== undefined
317
+ ? configuration.paddingTop
318
+ : container.paddingTop;
319
+
320
+ return (
321
+ <View
322
+ style={{
323
+ paddingTop: paddingTop,
324
+ paddingRight: container.paddingRight,
325
+ paddingBottom: container.paddingBottom,
326
+ paddingLeft: container.paddingLeft,
327
+ }}
328
+ >
329
+ {children}
330
+ </View>
331
+ );
332
+ }
333
+
334
+ type HeaderTitleProps = {
335
+ title: string;
336
+ configuration: TitleConfiguration;
337
+ centered: boolean;
338
+ };
339
+
340
+ function HeaderTitle({ title, configuration, centered }: HeaderTitleProps) {
341
+ const letterSpacing =
342
+ Platform.OS === "ios"
343
+ ? configuration.iosLetterSpacing
344
+ : configuration.androidLetterSpacing;
345
+
346
+ return (
347
+ <Text
348
+ style={{
349
+ flex: centered ? undefined : 1,
350
+ color: configuration.fontColor,
351
+ fontSize: configuration.fontSize,
352
+ lineHeight: configuration.lineHeight,
353
+ letterSpacing,
354
+ fontFamily: Platform.select({
355
+ ios: configuration.iosFontFamily,
356
+ android: configuration.androidFontFamily,
357
+ default: configuration.androidFontFamily,
358
+ }),
359
+ textTransform: getTextTransformStyle(configuration.textTransform),
360
+ textAlign: centered ? "center" : configuration.alignment,
361
+ }}
362
+ numberOfLines={1}
363
+ >
364
+ {title}
365
+ </Text>
366
+ );
367
+ }
368
+
369
+ type HeaderCloseButtonProps = {
370
+ configuration: CloseButtonConfiguration;
371
+ focused?: boolean;
372
+ onPress?: () => void;
373
+ };
374
+
375
+ function HeaderCloseButton({
376
+ configuration,
377
+ focused = false,
378
+ onPress,
379
+ }: HeaderCloseButtonProps) {
380
+ if (!configuration.enabled) {
381
+ return null;
382
+ }
383
+
384
+ const buttonSize = 32;
385
+
386
+ return (
387
+ <Pressable
388
+ onPress={onPress}
389
+ style={({ pressed }) => [
390
+ styles.closeButton,
391
+ {
392
+ width: buttonSize,
393
+ height: buttonSize,
394
+ borderRadius: buttonSize / 2,
395
+ borderWidth: 1.5,
396
+ borderColor: "rgba(255, 255, 255, 0.4)",
397
+ opacity: pressed || focused ? 0.7 : 1,
398
+ justifyContent: "center",
399
+ alignItems: "center",
400
+ },
401
+ ]}
402
+ >
403
+ <View
404
+ style={{
405
+ width: 14,
406
+ height: 14,
407
+ justifyContent: "center",
408
+ alignItems: "center",
409
+ }}
410
+ >
411
+ <View
412
+ style={{
413
+ position: "absolute",
414
+ width: 14,
415
+ height: 1.5,
416
+ backgroundColor: "#FFFFFF",
417
+ transform: [{ rotate: "45deg" }],
418
+ borderRadius: 1,
419
+ }}
420
+ />
421
+ <View
422
+ style={{
423
+ position: "absolute",
424
+ width: 14,
425
+ height: 1.5,
426
+ backgroundColor: "#FFFFFF",
427
+ transform: [{ rotate: "-45deg" }],
428
+ borderRadius: 1,
429
+ }}
430
+ />
431
+ </View>
432
+ </Pressable>
433
+ );
434
+ }
435
+
436
+ type HeaderBackButtonProps = {
437
+ focused?: boolean;
438
+ onPress?: () => void;
439
+ };
440
+
441
+ function HeaderBackButton({ focused = false, onPress }: HeaderBackButtonProps) {
442
+ return (
443
+ <Pressable
444
+ onPress={onPress}
445
+ style={({ pressed }) => [
446
+ styles.closeButton,
447
+ {
448
+ width: CLOSE_BUTTON_SPEC.width,
449
+ height: CLOSE_BUTTON_SPEC.height,
450
+ marginTop: CLOSE_BUTTON_SPEC.paddingTop,
451
+ marginRight: CLOSE_BUTTON_SPEC.paddingRight,
452
+ marginBottom: CLOSE_BUTTON_SPEC.paddingBottom,
453
+ marginLeft: CLOSE_BUTTON_SPEC.paddingLeft,
454
+ borderRadius: CLOSE_BUTTON_SPEC.width / 2,
455
+ borderWidth: 1,
456
+ borderColor: "#FFFFFF",
457
+ opacity: pressed || focused ? 0.7 : 1,
458
+ },
459
+ ]}
460
+ >
461
+ <Text style={{ color: "#FFFFFF", fontSize: 20, fontWeight: "bold" }}>
462
+
463
+ </Text>
464
+ </Pressable>
465
+ );
466
+ }
467
+
468
+ type HeaderActionButtonProps = {
469
+ actionType: HeaderActionType;
470
+ configuration: ActionButtonsStyleConfiguration;
471
+ focused?: boolean;
472
+ onPress?: () => void;
473
+ };
474
+
475
+ function HeaderActionButton({
476
+ actionType,
477
+ configuration,
478
+ focused = false,
479
+ onPress,
480
+ }: HeaderActionButtonProps) {
481
+ return (
482
+ <Pressable
483
+ onPress={onPress}
484
+ style={({ pressed }) => ({
485
+ backgroundColor:
486
+ pressed || focused
487
+ ? configuration.focusedBackgroundColor
488
+ : configuration.defaultBackgroundColor,
489
+ borderRadius: ACTION_BUTTON_LAYOUT_SPEC.cornerRadius,
490
+ paddingTop: ACTION_BUTTON_LAYOUT_SPEC.paddingTop,
491
+ paddingRight: ACTION_BUTTON_LAYOUT_SPEC.paddingRight,
492
+ paddingBottom: ACTION_BUTTON_LAYOUT_SPEC.paddingBottom,
493
+ paddingLeft: ACTION_BUTTON_LAYOUT_SPEC.paddingLeft,
494
+ })}
495
+ >
496
+ <Text
497
+ style={{
498
+ color: focused
499
+ ? configuration.focusedFontColor
500
+ : configuration.fontColor,
501
+ fontSize: configuration.fontSize,
502
+ lineHeight: configuration.lineHeight,
503
+ letterSpacing:
504
+ Platform.OS === "ios"
505
+ ? configuration.iosLetterSpacing
506
+ : configuration.androidLetterSpacing,
507
+ fontFamily: Platform.select({
508
+ ios: configuration.iosFontFamily,
509
+ android: configuration.androidFontFamily,
510
+ default: configuration.androidFontFamily,
511
+ }),
512
+ textTransform: getTextTransformStyle(configuration.textTransform),
513
+ }}
514
+ numberOfLines={1}
515
+ >
516
+ {ACTION_LABELS[actionType]}
517
+ </Text>
518
+ </Pressable>
519
+ );
520
+ }
521
+
522
+ type CenterAlignedHeaderLayoutProps = {
523
+ horizontalGutter: number;
524
+ title: TitleConfiguration;
525
+ titleText: string;
526
+ leadingContent: React.ReactNode;
527
+ trailingContent: React.ReactNode;
528
+ };
529
+
530
+ function CenterAlignedHeaderLayout({
531
+ horizontalGutter,
532
+ title,
533
+ titleText,
534
+ leadingContent,
535
+ trailingContent,
536
+ }: CenterAlignedHeaderLayoutProps) {
537
+ const [sideWidths, setSideWidths] = useState({ leading: 0, trailing: 0 });
538
+ const sideSlotWidth = Math.max(sideWidths.leading, sideWidths.trailing);
539
+
540
+ const updateSideWidth = useCallback(
541
+ (side: "leading" | "trailing", width: number) => {
542
+ setSideWidths((previous) => {
543
+ if (previous[side] === width) {
544
+ return previous;
545
+ }
546
+
547
+ return { ...previous, [side]: width };
548
+ });
549
+ },
550
+ []
551
+ );
552
+
553
+ return (
554
+ <View style={styles.centerAlignedRow}>
555
+ <View style={styles.measureLayer} pointerEvents="none">
556
+ {leadingContent ? (
557
+ <View
558
+ onLayout={(event) =>
559
+ updateSideWidth("leading", event.nativeEvent.layout.width)
560
+ }
561
+ >
562
+ {leadingContent}
563
+ </View>
564
+ ) : null}
565
+ {trailingContent ? (
566
+ <View
567
+ onLayout={(event) =>
568
+ updateSideWidth("trailing", event.nativeEvent.layout.width)
569
+ }
570
+ >
571
+ {trailingContent}
572
+ </View>
573
+ ) : null}
574
+ </View>
575
+
576
+ <View
577
+ style={[
578
+ styles.sideSlotFixed,
579
+ { width: sideSlotWidth, alignItems: "flex-start" },
580
+ ]}
581
+ >
582
+ {leadingContent}
583
+ </View>
584
+ <View style={{ width: horizontalGutter }} />
585
+ <View style={styles.centerSpacer} />
586
+ <View style={{ width: horizontalGutter }} />
587
+ <View
588
+ style={[
589
+ styles.sideSlotFixed,
590
+ { width: sideSlotWidth, alignItems: "flex-end" },
591
+ ]}
592
+ >
593
+ {trailingContent}
594
+ </View>
595
+
596
+ <View
597
+ pointerEvents="none"
598
+ style={[
599
+ styles.absoluteTitleLayer,
600
+ { paddingHorizontal: sideSlotWidth + horizontalGutter },
601
+ ]}
602
+ >
603
+ <HeaderTitle title={titleText} configuration={title} centered />
604
+ </View>
605
+ </View>
606
+ );
607
+ }
608
+
609
+ // ---------------------------------------------------------------------------
610
+ // AudioPlayerHeader
611
+ // ---------------------------------------------------------------------------
612
+
613
+ export function AudioPlayerHeader({
614
+ configuration,
615
+ data,
616
+ focusedCloseButton = false,
617
+ focusedLeadingAction = false,
618
+ focusedTrailingAction = false,
619
+ onClose,
620
+ onBack,
621
+ onCancel,
622
+ onDone,
623
+ onClear,
624
+ }: AudioPlayerHeaderProps) {
625
+ const { variant, title, closeButton, actionButtons } = configuration;
626
+ const { title: titleText } = data;
627
+
628
+ const container = CONTAINER_SPEC[variant];
629
+ const behavior = ACTION_BUTTONS_BEHAVIOR[variant];
630
+ const isCenterAligned = title.alignment === "center";
631
+
632
+ const showLeadingAction = shouldShowActionSlot(
633
+ behavior.leading,
634
+ behavior.sectionEnabled
635
+ );
636
+
637
+ const showTrailingAction = shouldShowActionSlot(
638
+ behavior.trailing,
639
+ behavior.sectionEnabled
640
+ );
641
+
642
+ const leadingContent = showLeadingAction ? (
643
+ <HeaderActionButton
644
+ actionType={behavior.leading.actionType}
645
+ configuration={actionButtons}
646
+ focused={focusedLeadingAction}
647
+ onPress={onCancel}
648
+ />
649
+ ) : onBack ? (
650
+ <HeaderBackButton focused={focusedCloseButton} onPress={onBack} />
651
+ ) : null;
652
+
653
+ const trailingContent = showTrailingAction ? (
654
+ <HeaderActionButton
655
+ actionType={behavior.trailing.actionType}
656
+ configuration={actionButtons}
657
+ focused={focusedTrailingAction}
658
+ onPress={behavior.trailing.actionType === "done" ? onDone : onClear}
659
+ />
660
+ ) : closeButton.enabled && !isCenterAligned ? (
661
+ <HeaderCloseButton
662
+ configuration={closeButton}
663
+ focused={focusedCloseButton}
664
+ onPress={onClose}
665
+ />
666
+ ) : null;
667
+
668
+ if (isCenterAligned) {
669
+ return (
670
+ <HeaderContainer variant={variant} configuration={configuration}>
671
+ <CenterAlignedHeaderLayout
672
+ horizontalGutter={container.horizontalGutter}
673
+ title={title}
674
+ titleText={titleText}
675
+ leadingContent={leadingContent}
676
+ trailingContent={trailingContent}
677
+ />
678
+ </HeaderContainer>
679
+ );
680
+ }
681
+
682
+ return (
683
+ <HeaderContainer variant={variant} configuration={configuration}>
684
+ <View style={styles.row}>
685
+ {leadingContent ? (
686
+ <>
687
+ {leadingContent}
688
+ <View style={{ width: container.horizontalGutter }} />
689
+ </>
690
+ ) : null}
691
+ <HeaderTitle title={titleText} configuration={title} centered={false} />
692
+ {trailingContent ? (
693
+ <>
694
+ <View style={{ width: container.horizontalGutter }} />
695
+ {trailingContent}
696
+ </>
697
+ ) : null}
698
+ </View>
699
+ </HeaderContainer>
700
+ );
701
+ }
702
+
703
+ // ---------------------------------------------------------------------------
704
+ // Variant configurations (green defaults per PDF column)
705
+ // ---------------------------------------------------------------------------
706
+
707
+ /** Default / Variant 1 — Single Line Header */
708
+ export const VARIANT_SINGLE_LINE_HEADER: HeaderConfiguration = {
709
+ variant: "singleLine",
710
+ title: DEFAULT_TITLE_CONFIGURATION,
711
+ closeButton: DEFAULT_CLOSE_BUTTON_CONFIGURATION,
712
+ actionButtons: DEFAULT_ACTION_BUTTONS_STYLE_CONFIGURATION,
713
+ };
714
+
715
+ /** Variant 2 — Header With Actions */
716
+ export const VARIANT_HEADER_WITH_ACTIONS: HeaderConfiguration = {
717
+ variant: "headerWithActions",
718
+ title: {
719
+ fontColor: "#FFFFFF",
720
+ iosFontFamily: "SFProText-Bold",
721
+ androidFontFamily: "Roboto-Bold",
722
+ fontSize: 22,
723
+ lineHeight: 28,
724
+ iosLetterSpacing: -0.6,
725
+ androidLetterSpacing: 0,
726
+ textTransform: "default",
727
+ alignment: "center",
728
+ },
729
+ closeButton: { enabled: false },
730
+ actionButtons: DEFAULT_ACTION_BUTTONS_STYLE_CONFIGURATION,
731
+ paddingTop: 16,
732
+ };
733
+
734
+ /** Variant 2 — single side action (layout rule demo) */
735
+ export const VARIANT_HEADER_WITH_CANCEL_ONLY: HeaderConfiguration = {
736
+ variant: "headerWithActionsCancelOnly",
737
+ title: {
738
+ fontColor: "#FFFFFF",
739
+ iosFontFamily: "SFProText-Bold",
740
+ androidFontFamily: "Roboto-Bold",
741
+ fontSize: 22,
742
+ lineHeight: 28,
743
+ iosLetterSpacing: -0.6,
744
+ androidLetterSpacing: 0,
745
+ textTransform: "default",
746
+ alignment: "center",
747
+ },
748
+ closeButton: { enabled: false },
749
+ actionButtons: DEFAULT_ACTION_BUTTONS_STYLE_CONFIGURATION,
750
+ paddingTop: 16,
751
+ };
752
+
753
+ /** Variant 3 — Section Header */
754
+ export const VARIANT_SECTION_HEADER: HeaderConfiguration = {
755
+ variant: "section",
756
+ title: {
757
+ fontColor: "#FFFFFF",
758
+ iosFontFamily: "SFProText-Bold",
759
+ androidFontFamily: "Roboto-Bold",
760
+ fontSize: 13,
761
+ lineHeight: 32,
762
+ iosLetterSpacing: -0.1,
763
+ androidLetterSpacing: 0,
764
+ textTransform: "default",
765
+ alignment: "left",
766
+ },
767
+ closeButton: { enabled: false },
768
+ actionButtons: DEFAULT_ACTION_BUTTONS_STYLE_CONFIGURATION,
769
+ };
770
+
771
+ export default function App() {
772
+ return (
773
+ <View style={styles.demoScreen}>
774
+ <Text style={styles.demoHeading}>Modal Header Building Block</Text>
775
+
776
+ <View style={styles.demoBlock}>
777
+ <Text style={styles.demoLabel}>Single Line Header</Text>
778
+ <AudioPlayerHeader
779
+ configuration={VARIANT_SINGLE_LINE_HEADER}
780
+ data={{ title: "Playback Speed" }}
781
+ />
782
+ </View>
783
+
784
+ <View style={styles.demoBlock}>
785
+ <Text style={styles.demoLabel}>Header With Actions</Text>
786
+ <AudioPlayerHeader
787
+ configuration={VARIANT_HEADER_WITH_ACTIONS}
788
+ data={{ title: "Playback Speed" }}
789
+ />
790
+ </View>
791
+
792
+ <View style={styles.demoBlock}>
793
+ <Text style={styles.demoLabel}>
794
+ Header With Actions — single side action
795
+ </Text>
796
+ <AudioPlayerHeader
797
+ configuration={VARIANT_HEADER_WITH_CANCEL_ONLY}
798
+ data={{ title: "Playback Speed" }}
799
+ />
800
+ </View>
801
+
802
+ <View style={styles.demoBlock}>
803
+ <Text style={styles.demoLabel}>Section Header</Text>
804
+ <AudioPlayerHeader
805
+ configuration={VARIANT_SECTION_HEADER}
806
+ data={{ title: "Next in Queue" }}
807
+ />
808
+ </View>
809
+ </View>
810
+ );
811
+ }