@applicaster/zapp-react-native-ui-components 16.0.0-rc.9 → 16.0.0-rc.90

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