@applicaster/zapp-react-native-ui-components 16.0.0-rc.6 → 16.0.0-rc.61

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